From ae83a617cfa5380088c7f7a18a22ad4ebec19b30 Mon Sep 17 00:00:00 2001 From: sebastien Date: Thu, 29 Feb 2024 15:18:03 +0100 Subject: [PATCH 01/18] refactor site loading, extract codegen to separate file + add basic perf logging --- packages/docusaurus/src/server/codegen.ts | 190 ++++++++++++++++++++++ packages/docusaurus/src/server/index.ts | 137 +++++----------- packages/docusaurus/src/server/routes.ts | 2 +- 3 files changed, 228 insertions(+), 101 deletions(-) create mode 100644 packages/docusaurus/src/server/codegen.ts diff --git a/packages/docusaurus/src/server/codegen.ts b/packages/docusaurus/src/server/codegen.ts new file mode 100644 index 000000000000..e83fab20b9a6 --- /dev/null +++ b/packages/docusaurus/src/server/codegen.ts @@ -0,0 +1,190 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { + generate, + escapePath, + DEFAULT_CONFIG_FILE_NAME, +} from '@docusaurus/utils'; +import type { + CodeTranslations, + DocusaurusConfig, + GlobalData, + I18n, + SiteMetadata, +} from '@docusaurus/types'; +import type {LoadedRoutes} from './routes'; + +const genWarning = ({generatedFilesDir}: {generatedFilesDir: string}) => + generate( + generatedFilesDir, + // cSpell:ignore DONT + 'DONT-EDIT-THIS-FOLDER', + `This folder stores temp files that Docusaurus' client bundler accesses. + +DO NOT hand-modify files in this folder because they will be overwritten in the +next build. You can clear all build artifacts (including this folder) with the +\`docusaurus clear\` command. +`, + ); + +const genSiteConfig = ({ + generatedFilesDir, + siteConfig, +}: { + generatedFilesDir: string; + siteConfig: DocusaurusConfig; +}) => + generate( + generatedFilesDir, + `${DEFAULT_CONFIG_FILE_NAME}.mjs`, + `/* + * AUTOGENERATED - DON'T EDIT + * Your edits in this file will be overwritten in the next build! + * Modify the docusaurus.config.js file at your site's root instead. + */ +export default ${JSON.stringify(siteConfig, null, 2)}; +`, + ); + +const genClientModules = ({ + generatedFilesDir, + clientModules, +}: { + generatedFilesDir: string; + clientModules: string[]; +}) => + generate( + generatedFilesDir, + 'client-modules.js', + `export default [ +${clientModules + // Use `require()` because `import()` is async but client modules can have CSS + // and the order matters for loading CSS. + .map((clientModule) => ` require("${escapePath(clientModule)}"),`) + .join('\n')} +]; +`, + ); + +const genRegistry = ({ + generatedFilesDir, + registry, +}: { + generatedFilesDir: string; + registry: LoadedRoutes['registry']; +}) => + generate( + generatedFilesDir, + 'registry.js', + `export default { +${Object.entries(registry) + .sort((a, b) => a[0].localeCompare(b[0])) + .map( + ([chunkName, modulePath]) => + // modulePath is already escaped by escapePath + ` "${chunkName}": [() => import(/* webpackChunkName: "${chunkName}" */ "${modulePath}"), "${modulePath}", require.resolveWeak("${modulePath}")],`, + ) + .join('\n')}}; +`, + ); + +const genRoutesChunkNames = ({ + generatedFilesDir, + routesChunkNames, +}: { + generatedFilesDir: string; + routesChunkNames: LoadedRoutes['routesChunkNames']; +}) => + generate( + generatedFilesDir, + 'routesChunkNames.json', + JSON.stringify(routesChunkNames, null, 2), + ); + +const genRoutes = ({ + generatedFilesDir, + routesConfig, +}: { + generatedFilesDir: string; + routesConfig: LoadedRoutes['routesConfig']; +}) => generate(generatedFilesDir, 'routes.js', routesConfig); + +const genGlobalData = ({ + generatedFilesDir, + globalData, +}: { + generatedFilesDir: string; + globalData: GlobalData; +}) => + generate( + generatedFilesDir, + 'globalData.json', + JSON.stringify(globalData, null, 2), + ); + +const genI18n = ({ + generatedFilesDir, + i18n, +}: { + generatedFilesDir: string; + i18n: I18n; +}) => generate(generatedFilesDir, 'i18n.json', JSON.stringify(i18n, null, 2)); + +const genCodeTranslations = ({ + generatedFilesDir, + codeTranslations, +}: { + generatedFilesDir: string; + codeTranslations: CodeTranslations; +}) => + generate( + generatedFilesDir, + 'codeTranslations.json', + JSON.stringify(codeTranslations, null, 2), + ); + +const genSiteMetadata = ({ + generatedFilesDir, + siteMetadata, +}: { + generatedFilesDir: string; + siteMetadata: SiteMetadata; +}) => + generate( + generatedFilesDir, + 'site-metadata.json', + JSON.stringify(siteMetadata, null, 2), + ); + +type CodegenParams = { + generatedFilesDir: string; + siteConfig: DocusaurusConfig; + clientModules: string[]; + registry: LoadedRoutes['registry']; + routesChunkNames: LoadedRoutes['routesChunkNames']; + routesConfig: LoadedRoutes['routesConfig']; + globalData: GlobalData; + i18n: I18n; + codeTranslations: CodeTranslations; + siteMetadata: SiteMetadata; +}; + +export async function generateSiteCode(params: CodegenParams): Promise { + await Promise.all([ + genWarning(params), + genClientModules(params), + genSiteConfig(params), + genRegistry(params), + genRoutesChunkNames(params), + genRoutes(params), + genGlobalData(params), + genSiteMetadata(params), + genI18n(params), + genCodeTranslations(params), + ]); +} diff --git a/packages/docusaurus/src/server/index.ts b/packages/docusaurus/src/server/index.ts index ab50c1883d31..fc0459e3dda2 100644 --- a/packages/docusaurus/src/server/index.ts +++ b/packages/docusaurus/src/server/index.ts @@ -8,11 +8,8 @@ import path from 'path'; import _ from 'lodash'; import { - generate, - escapePath, localizePath, DEFAULT_BUILD_DIR_NAME, - DEFAULT_CONFIG_FILE_NAME, GENERATED_FILES_DIR_NAME, } from '@docusaurus/utils'; import {loadSiteConfig} from './config'; @@ -26,6 +23,8 @@ import { readCodeTranslationFileContent, getPluginsDefaultCodeTranslationMessages, } from './translations/translations'; +import {PerfLogger} from '../utils'; +import {generateSiteCode} from './codegen'; import type {DocusaurusConfig, LoadContext, Props} from '@docusaurus/types'; export type LoadContextOptions = { @@ -121,7 +120,11 @@ export async function loadContext( */ export async function load(options: LoadContextOptions): Promise { const {siteDir} = options; + + PerfLogger.start('Load - loadContext'); const context = await loadContext(options); + PerfLogger.end('Load - loadContext'); + const { generatedFilesDir, siteConfig, @@ -132,118 +135,52 @@ export async function load(options: LoadContextOptions): Promise { localizationDir, codeTranslations: siteCodeTranslations, } = context; + + PerfLogger.start('Load - loadPlugins'); const {plugins, pluginsRouteConfigs, globalData} = await loadPlugins(context); + PerfLogger.end('Load - loadPlugins'); + + PerfLogger.start('Load - loadClientModules'); const clientModules = loadClientModules(plugins); + PerfLogger.end('Load - loadClientModules'); + + PerfLogger.start('Load - loadHtmlTags'); const {headTags, preBodyTags, postBodyTags} = loadHtmlTags(plugins); + PerfLogger.end('Load - loadHtmlTags'); + + PerfLogger.start('Load - loadRoutes'); const {registry, routesChunkNames, routesConfig, routesPaths} = loadRoutes( pluginsRouteConfigs, baseUrl, siteConfig.onDuplicateRoutes, ); + PerfLogger.end('Load - loadRoutes'); + + PerfLogger.start('Load - load codeTranslations'); const codeTranslations = { ...(await getPluginsDefaultCodeTranslationMessages(plugins)), ...siteCodeTranslations, }; - const siteMetadata = await loadSiteMetadata({plugins, siteDir}); - - // === Side-effects part === - - const genWarning = generate( - generatedFilesDir, - // cSpell:ignore DONT - 'DONT-EDIT-THIS-FOLDER', - `This folder stores temp files that Docusaurus' client bundler accesses. + PerfLogger.end('Load - load codeTranslations'); -DO NOT hand-modify files in this folder because they will be overwritten in the -next build. You can clear all build artifacts (including this folder) with the -\`docusaurus clear\` command. -`, - ); - - const genSiteConfig = generate( - generatedFilesDir, - `${DEFAULT_CONFIG_FILE_NAME}.mjs`, - `/* - * AUTOGENERATED - DON'T EDIT - * Your edits in this file will be overwritten in the next build! - * Modify the docusaurus.config.js file at your site's root instead. - */ -export default ${JSON.stringify(siteConfig, null, 2)}; -`, - ); - - const genClientModules = generate( - generatedFilesDir, - 'client-modules.js', - `export default [ -${clientModules - // Use `require()` because `import()` is async but client modules can have CSS - // and the order matters for loading CSS. - .map((clientModule) => ` require("${escapePath(clientModule)}"),`) - .join('\n')} -]; -`, - ); - - const genRegistry = generate( - generatedFilesDir, - 'registry.js', - `export default { -${Object.entries(registry) - .sort((a, b) => a[0].localeCompare(b[0])) - .map( - ([chunkName, modulePath]) => - // modulePath is already escaped by escapePath - ` "${chunkName}": [() => import(/* webpackChunkName: "${chunkName}" */ "${modulePath}"), "${modulePath}", require.resolveWeak("${modulePath}")],`, - ) - .join('\n')}}; -`, - ); - - const genRoutesChunkNames = generate( - generatedFilesDir, - 'routesChunkNames.json', - JSON.stringify(routesChunkNames, null, 2), - ); - - const genRoutes = generate(generatedFilesDir, 'routes.js', routesConfig); - - const genGlobalData = generate( - generatedFilesDir, - 'globalData.json', - JSON.stringify(globalData, null, 2), - ); - - const genI18n = generate( - generatedFilesDir, - 'i18n.json', - JSON.stringify(i18n, null, 2), - ); - - const genCodeTranslations = generate( - generatedFilesDir, - 'codeTranslations.json', - JSON.stringify(codeTranslations, null, 2), - ); + PerfLogger.start('Load - loadSiteMetadata'); + const siteMetadata = await loadSiteMetadata({plugins, siteDir}); + PerfLogger.end('Load - loadSiteMetadata'); - const genSiteMetadata = generate( + PerfLogger.start('Load - generateSiteCode'); + await generateSiteCode({ generatedFilesDir, - 'site-metadata.json', - JSON.stringify(siteMetadata, null, 2), - ); - - await Promise.all([ - genWarning, - genClientModules, - genSiteConfig, - genRegistry, - genRoutesChunkNames, - genRoutes, - genGlobalData, - genSiteMetadata, - genI18n, - genCodeTranslations, - ]); + clientModules, + siteConfig, + siteMetadata, + i18n, + codeTranslations, + globalData, + routesChunkNames, + routesConfig, + registry, + }); + PerfLogger.end('Load - generateSiteCode'); return { siteConfig, diff --git a/packages/docusaurus/src/server/routes.ts b/packages/docusaurus/src/server/routes.ts index 907f77816b8f..d18d814156a2 100644 --- a/packages/docusaurus/src/server/routes.ts +++ b/packages/docusaurus/src/server/routes.ts @@ -24,7 +24,7 @@ import type { ReportingSeverity, } from '@docusaurus/types'; -type LoadedRoutes = { +export type LoadedRoutes = { /** Serialized routes config that can be directly emitted into temp file. */ routesConfig: string; /** @see {ChunkNames} */ From 3ee05964297cea0355a0cd77844dff43f6f7aa2a Mon Sep 17 00:00:00 2001 From: sebastien Date: Thu, 29 Feb 2024 15:26:05 +0100 Subject: [PATCH 02/18] move all core codegen to subfolder --- .../{ => codegen}/__tests__/__snapshots__/routes.test.ts.snap | 0 .../src/server/{ => codegen}/__tests__/routes.test.ts | 0 packages/docusaurus/src/server/{ => codegen}/codegen.ts | 0 packages/docusaurus/src/server/{ => codegen}/routes.ts | 2 +- packages/docusaurus/src/server/index.ts | 4 ++-- 5 files changed, 3 insertions(+), 3 deletions(-) rename packages/docusaurus/src/server/{ => codegen}/__tests__/__snapshots__/routes.test.ts.snap (100%) rename packages/docusaurus/src/server/{ => codegen}/__tests__/routes.test.ts (100%) rename packages/docusaurus/src/server/{ => codegen}/codegen.ts (100%) rename packages/docusaurus/src/server/{ => codegen}/routes.ts (99%) diff --git a/packages/docusaurus/src/server/__tests__/__snapshots__/routes.test.ts.snap b/packages/docusaurus/src/server/codegen/__tests__/__snapshots__/routes.test.ts.snap similarity index 100% rename from packages/docusaurus/src/server/__tests__/__snapshots__/routes.test.ts.snap rename to packages/docusaurus/src/server/codegen/__tests__/__snapshots__/routes.test.ts.snap diff --git a/packages/docusaurus/src/server/__tests__/routes.test.ts b/packages/docusaurus/src/server/codegen/__tests__/routes.test.ts similarity index 100% rename from packages/docusaurus/src/server/__tests__/routes.test.ts rename to packages/docusaurus/src/server/codegen/__tests__/routes.test.ts diff --git a/packages/docusaurus/src/server/codegen.ts b/packages/docusaurus/src/server/codegen/codegen.ts similarity index 100% rename from packages/docusaurus/src/server/codegen.ts rename to packages/docusaurus/src/server/codegen/codegen.ts diff --git a/packages/docusaurus/src/server/routes.ts b/packages/docusaurus/src/server/codegen/routes.ts similarity index 99% rename from packages/docusaurus/src/server/routes.ts rename to packages/docusaurus/src/server/codegen/routes.ts index d18d814156a2..a27b8c614d75 100644 --- a/packages/docusaurus/src/server/routes.ts +++ b/packages/docusaurus/src/server/codegen/routes.ts @@ -14,7 +14,7 @@ import { simpleHash, escapePath, } from '@docusaurus/utils'; -import {getAllFinalRoutes} from './utils'; +import {getAllFinalRoutes} from '../utils'; import type { Module, RouteConfig, diff --git a/packages/docusaurus/src/server/index.ts b/packages/docusaurus/src/server/index.ts index fc0459e3dda2..48f9baf189fb 100644 --- a/packages/docusaurus/src/server/index.ts +++ b/packages/docusaurus/src/server/index.ts @@ -15,7 +15,7 @@ import { import {loadSiteConfig} from './config'; import {loadClientModules} from './clientModules'; import {loadPlugins} from './plugins'; -import {loadRoutes} from './routes'; +import {loadRoutes} from './codegen/routes'; import {loadHtmlTags} from './htmlTags'; import {loadSiteMetadata} from './siteMetadata'; import {loadI18n} from './i18n'; @@ -24,7 +24,7 @@ import { getPluginsDefaultCodeTranslationMessages, } from './translations/translations'; import {PerfLogger} from '../utils'; -import {generateSiteCode} from './codegen'; +import {generateSiteCode} from './codegen/codegen'; import type {DocusaurusConfig, LoadContext, Props} from '@docusaurus/types'; export type LoadContextOptions = { From 762e9693b87f9fde4670831cff0e50d998c788b3 Mon Sep 17 00:00:00 2001 From: sebastien Date: Thu, 29 Feb 2024 17:04:02 +0100 Subject: [PATCH 03/18] more refactorings + add useful perf loggers --- .../src/server/__tests__/routes.test.ts | 83 +++++++++++ .../src/server/__tests__/utils.test.ts | 33 ---- packages/docusaurus/src/server/brokenLinks.ts | 2 +- ...est.ts.snap => codegenRoutes.test.ts.snap} | 13 -- .../{routes.test.ts => codegenRoutes.test.ts} | 62 ++------ .../docusaurus/src/server/codegen/codegen.ts | 107 +++++-------- .../codegen/{routes.ts => codegenRoutes.ts} | 141 +++++++++--------- packages/docusaurus/src/server/index.ts | 67 ++++----- .../docusaurus/src/server/plugins/index.ts | 15 ++ packages/docusaurus/src/server/routes.ts | 66 ++++++++ .../src/server/translations/translations.ts | 12 ++ packages/docusaurus/src/server/utils.ts | 9 -- packages/docusaurus/src/utils.ts | 27 +++- 13 files changed, 345 insertions(+), 292 deletions(-) create mode 100644 packages/docusaurus/src/server/__tests__/routes.test.ts delete mode 100644 packages/docusaurus/src/server/__tests__/utils.test.ts rename packages/docusaurus/src/server/codegen/__tests__/__snapshots__/{routes.test.ts.snap => codegenRoutes.test.ts.snap} (95%) rename packages/docusaurus/src/server/codegen/__tests__/{routes.test.ts => codegenRoutes.test.ts} (78%) rename packages/docusaurus/src/server/codegen/{routes.ts => codegenRoutes.ts} (76%) create mode 100644 packages/docusaurus/src/server/routes.ts diff --git a/packages/docusaurus/src/server/__tests__/routes.test.ts b/packages/docusaurus/src/server/__tests__/routes.test.ts new file mode 100644 index 000000000000..278fbf9583fe --- /dev/null +++ b/packages/docusaurus/src/server/__tests__/routes.test.ts @@ -0,0 +1,83 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import {jest} from '@jest/globals'; +import {getAllFinalRoutes, handleDuplicateRoutes} from '../routes'; +import type {RouteConfig} from '@docusaurus/types'; + +describe('getAllFinalRoutes', () => { + it('gets final routes correctly', () => { + const routes: RouteConfig[] = [ + { + path: '/docs', + component: '', + routes: [ + {path: '/docs/someDoc', component: ''}, + {path: '/docs/someOtherDoc', component: ''}, + ], + }, + { + path: '/community', + component: '', + }, + ]; + expect(getAllFinalRoutes(routes)).toEqual([ + routes[0]!.routes![0], + routes[0]!.routes![1], + routes[1], + ]); + }); +}); + +describe('handleDuplicateRoutes', () => { + const routes: RouteConfig[] = [ + { + path: '/', + component: '', + routes: [ + {path: '/search', component: ''}, + {path: '/sameDoc', component: ''}, + ], + }, + { + path: '/', + component: '', + routes: [ + {path: '/search', component: ''}, + {path: '/sameDoc', component: ''}, + {path: '/uniqueDoc', component: ''}, + ], + }, + { + path: '/', + component: '', + }, + { + path: '/', + component: '', + }, + { + path: '/', + component: '', + }, + ]; + it('works', () => { + expect(() => { + handleDuplicateRoutes(routes, 'throw'); + }).toThrowErrorMatchingInlineSnapshot(` + "Duplicate routes found! + - Attempting to create page at /search, but a page already exists at this route. + - Attempting to create page at /sameDoc, but a page already exists at this route. + - Attempting to create page at /, but a page already exists at this route. + - Attempting to create page at /, but a page already exists at this route. + This could lead to non-deterministic routing behavior." + `); + const consoleMock = jest.spyOn(console, 'log').mockImplementation(() => {}); + handleDuplicateRoutes(routes, 'ignore'); + expect(consoleMock).toHaveBeenCalledTimes(0); + }); +}); diff --git a/packages/docusaurus/src/server/__tests__/utils.test.ts b/packages/docusaurus/src/server/__tests__/utils.test.ts deleted file mode 100644 index a93adea41f69..000000000000 --- a/packages/docusaurus/src/server/__tests__/utils.test.ts +++ /dev/null @@ -1,33 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -import {getAllFinalRoutes} from '../utils'; -import type {RouteConfig} from '@docusaurus/types'; - -describe('getAllFinalRoutes', () => { - it('gets final routes correctly', () => { - const routes: RouteConfig[] = [ - { - path: '/docs', - component: '', - routes: [ - {path: '/docs/someDoc', component: ''}, - {path: '/docs/someOtherDoc', component: ''}, - ], - }, - { - path: '/community', - component: '', - }, - ]; - expect(getAllFinalRoutes(routes)).toEqual([ - routes[0]!.routes![0], - routes[0]!.routes![1], - routes[1], - ]); - }); -}); diff --git a/packages/docusaurus/src/server/brokenLinks.ts b/packages/docusaurus/src/server/brokenLinks.ts index c91c2f1faecc..acdc016a1ff4 100644 --- a/packages/docusaurus/src/server/brokenLinks.ts +++ b/packages/docusaurus/src/server/brokenLinks.ts @@ -15,7 +15,7 @@ import { serializeURLPath, type URLPath, } from '@docusaurus/utils'; -import {getAllFinalRoutes} from './utils'; +import {getAllFinalRoutes} from './routes'; import type {RouteConfig, ReportingSeverity} from '@docusaurus/types'; function matchRoutes(routeConfig: RouteConfig[], pathname: string) { diff --git a/packages/docusaurus/src/server/codegen/__tests__/__snapshots__/routes.test.ts.snap b/packages/docusaurus/src/server/codegen/__tests__/__snapshots__/codegenRoutes.test.ts.snap similarity index 95% rename from packages/docusaurus/src/server/codegen/__tests__/__snapshots__/routes.test.ts.snap rename to packages/docusaurus/src/server/codegen/__tests__/__snapshots__/codegenRoutes.test.ts.snap index 9fe2fabb5ff4..80644fa961ae 100644 --- a/packages/docusaurus/src/server/codegen/__tests__/__snapshots__/routes.test.ts.snap +++ b/packages/docusaurus/src/server/codegen/__tests__/__snapshots__/codegenRoutes.test.ts.snap @@ -49,10 +49,6 @@ export default [ }, ]; ", - "routesPaths": [ - "/404.html", - "/blog", - ], } `; @@ -122,11 +118,6 @@ export default [ }, ]; ", - "routesPaths": [ - "/404.html", - "/docs/hello", - "docs/foo/baz", - ], } `; @@ -154,9 +145,5 @@ export default [ }, ]; ", - "routesPaths": [ - "/404.html", - "", - ], } `; diff --git a/packages/docusaurus/src/server/codegen/__tests__/routes.test.ts b/packages/docusaurus/src/server/codegen/__tests__/codegenRoutes.test.ts similarity index 78% rename from packages/docusaurus/src/server/codegen/__tests__/routes.test.ts rename to packages/docusaurus/src/server/codegen/__tests__/codegenRoutes.test.ts index f302b3c4506d..bfcfe123470b 100644 --- a/packages/docusaurus/src/server/codegen/__tests__/routes.test.ts +++ b/packages/docusaurus/src/server/codegen/__tests__/codegenRoutes.test.ts @@ -5,8 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -import {jest} from '@jest/globals'; -import {loadRoutes, handleDuplicateRoutes, genChunkName} from '../routes'; +import {generateRoutesCode, genChunkName} from '../codegenRoutes'; import type {RouteConfig} from '@docusaurus/types'; describe('genChunkName', () => { @@ -92,48 +91,6 @@ describe('genChunkName', () => { }); }); -describe('handleDuplicateRoutes', () => { - const routes: RouteConfig[] = [ - { - path: '/', - component: '', - routes: [ - {path: '/search', component: ''}, - {path: '/sameDoc', component: ''}, - ], - }, - { - path: '/', - component: '', - routes: [ - {path: '/search', component: ''}, - {path: '/sameDoc', component: ''}, - {path: '/uniqueDoc', component: ''}, - ], - }, - { - path: '/', - component: '', - }, - { - path: '/', - component: '', - }, - { - path: '/', - component: '', - }, - ]; - it('works', () => { - expect(() => { - handleDuplicateRoutes(routes, 'throw'); - }).toThrowErrorMatchingSnapshot(); - const consoleMock = jest.spyOn(console, 'log').mockImplementation(() => {}); - handleDuplicateRoutes(routes, 'ignore'); - expect(consoleMock).toHaveBeenCalledTimes(0); - }); -}); - describe('loadRoutes', () => { it('loads nested route config', () => { const nestedRouteConfig: RouteConfig = { @@ -175,7 +132,9 @@ describe('loadRoutes', () => { }, ], }; - expect(loadRoutes([nestedRouteConfig], '/', 'ignore')).toMatchSnapshot(); + expect( + generateRoutesCode([nestedRouteConfig], '/', 'ignore'), + ).toMatchSnapshot(); }); it('loads flat route config', () => { @@ -207,7 +166,9 @@ describe('loadRoutes', () => { ], }, }; - expect(loadRoutes([flatRouteConfig], '/', 'ignore')).toMatchSnapshot(); + expect( + generateRoutesCode([flatRouteConfig], '/', 'ignore'), + ).toMatchSnapshot(); }); it('rejects invalid route config', () => { @@ -215,7 +176,7 @@ describe('loadRoutes', () => { component: 'hello/world.js', } as RouteConfig; - expect(() => loadRoutes([routeConfigWithoutPath], '/', 'ignore')) + expect(() => generateRoutesCode([routeConfigWithoutPath], '/', 'ignore')) .toThrowErrorMatchingInlineSnapshot(` "Invalid route config: path must be a string and component is required. {"component":"hello/world.js"}" @@ -225,8 +186,9 @@ describe('loadRoutes', () => { path: '/hello/world', } as RouteConfig; - expect(() => loadRoutes([routeConfigWithoutComponent], '/', 'ignore')) - .toThrowErrorMatchingInlineSnapshot(` + expect(() => + generateRoutesCode([routeConfigWithoutComponent], '/', 'ignore'), + ).toThrowErrorMatchingInlineSnapshot(` "Invalid route config: path must be a string and component is required. {"path":"/hello/world"}" `); @@ -238,6 +200,6 @@ describe('loadRoutes', () => { component: 'hello/world.js', } as RouteConfig; - expect(loadRoutes([routeConfig], '/', 'ignore')).toMatchSnapshot(); + expect(generateRoutesCode([routeConfig], '/', 'ignore')).toMatchSnapshot(); }); }); diff --git a/packages/docusaurus/src/server/codegen/codegen.ts b/packages/docusaurus/src/server/codegen/codegen.ts index e83fab20b9a6..2cc8ad3adafd 100644 --- a/packages/docusaurus/src/server/codegen/codegen.ts +++ b/packages/docusaurus/src/server/codegen/codegen.ts @@ -10,17 +10,18 @@ import { escapePath, DEFAULT_CONFIG_FILE_NAME, } from '@docusaurus/utils'; +import {generateRouteFiles} from './codegenRoutes'; import type { CodeTranslations, DocusaurusConfig, GlobalData, I18n, + RouteConfig, SiteMetadata, } from '@docusaurus/types'; -import type {LoadedRoutes} from './routes'; -const genWarning = ({generatedFilesDir}: {generatedFilesDir: string}) => - generate( +function genWarning({generatedFilesDir}: {generatedFilesDir: string}) { + return generate( generatedFilesDir, // cSpell:ignore DONT 'DONT-EDIT-THIS-FOLDER', @@ -31,15 +32,16 @@ next build. You can clear all build artifacts (including this folder) with the \`docusaurus clear\` command. `, ); +} -const genSiteConfig = ({ +function genSiteConfig({ generatedFilesDir, siteConfig, }: { generatedFilesDir: string; siteConfig: DocusaurusConfig; -}) => - generate( +}) { + return generate( generatedFilesDir, `${DEFAULT_CONFIG_FILE_NAME}.mjs`, `/* @@ -50,15 +52,16 @@ const genSiteConfig = ({ export default ${JSON.stringify(siteConfig, null, 2)}; `, ); +} -const genClientModules = ({ +function genClientModules({ generatedFilesDir, clientModules, }: { generatedFilesDir: string; clientModules: string[]; -}) => - generate( +}) { + return generate( generatedFilesDir, 'client-modules.js', `export default [ @@ -70,118 +73,82 @@ ${clientModules ]; `, ); +} -const genRegistry = ({ - generatedFilesDir, - registry, -}: { - generatedFilesDir: string; - registry: LoadedRoutes['registry']; -}) => - generate( - generatedFilesDir, - 'registry.js', - `export default { -${Object.entries(registry) - .sort((a, b) => a[0].localeCompare(b[0])) - .map( - ([chunkName, modulePath]) => - // modulePath is already escaped by escapePath - ` "${chunkName}": [() => import(/* webpackChunkName: "${chunkName}" */ "${modulePath}"), "${modulePath}", require.resolveWeak("${modulePath}")],`, - ) - .join('\n')}}; -`, - ); - -const genRoutesChunkNames = ({ - generatedFilesDir, - routesChunkNames, -}: { - generatedFilesDir: string; - routesChunkNames: LoadedRoutes['routesChunkNames']; -}) => - generate( - generatedFilesDir, - 'routesChunkNames.json', - JSON.stringify(routesChunkNames, null, 2), - ); - -const genRoutes = ({ - generatedFilesDir, - routesConfig, -}: { - generatedFilesDir: string; - routesConfig: LoadedRoutes['routesConfig']; -}) => generate(generatedFilesDir, 'routes.js', routesConfig); - -const genGlobalData = ({ +function genGlobalData({ generatedFilesDir, globalData, }: { generatedFilesDir: string; globalData: GlobalData; -}) => - generate( +}) { + return generate( generatedFilesDir, 'globalData.json', JSON.stringify(globalData, null, 2), ); +} -const genI18n = ({ +function genI18n({ generatedFilesDir, i18n, }: { generatedFilesDir: string; i18n: I18n; -}) => generate(generatedFilesDir, 'i18n.json', JSON.stringify(i18n, null, 2)); +}) { + return generate( + generatedFilesDir, + 'i18n.json', + JSON.stringify(i18n, null, 2), + ); +} -const genCodeTranslations = ({ +function genCodeTranslations({ generatedFilesDir, codeTranslations, }: { generatedFilesDir: string; codeTranslations: CodeTranslations; -}) => - generate( +}) { + return generate( generatedFilesDir, 'codeTranslations.json', JSON.stringify(codeTranslations, null, 2), ); +} -const genSiteMetadata = ({ +function genSiteMetadata({ generatedFilesDir, siteMetadata, }: { generatedFilesDir: string; siteMetadata: SiteMetadata; -}) => - generate( +}) { + return generate( generatedFilesDir, 'site-metadata.json', JSON.stringify(siteMetadata, null, 2), ); +} type CodegenParams = { generatedFilesDir: string; siteConfig: DocusaurusConfig; + baseUrl: string; clientModules: string[]; - registry: LoadedRoutes['registry']; - routesChunkNames: LoadedRoutes['routesChunkNames']; - routesConfig: LoadedRoutes['routesConfig']; globalData: GlobalData; i18n: I18n; codeTranslations: CodeTranslations; siteMetadata: SiteMetadata; + routeConfigs: RouteConfig[]; }; -export async function generateSiteCode(params: CodegenParams): Promise { +export async function generateSiteFiles(params: CodegenParams): Promise { await Promise.all([ genWarning(params), genClientModules(params), genSiteConfig(params), - genRegistry(params), - genRoutesChunkNames(params), - genRoutes(params), + generateRouteFiles(params), genGlobalData(params), genSiteMetadata(params), genI18n(params), diff --git a/packages/docusaurus/src/server/codegen/routes.ts b/packages/docusaurus/src/server/codegen/codegenRoutes.ts similarity index 76% rename from packages/docusaurus/src/server/codegen/routes.ts rename to packages/docusaurus/src/server/codegen/codegenRoutes.ts index a27b8c614d75..c0fa0c415af0 100644 --- a/packages/docusaurus/src/server/codegen/routes.ts +++ b/packages/docusaurus/src/server/codegen/codegenRoutes.ts @@ -7,24 +7,16 @@ import query from 'querystring'; import _ from 'lodash'; -import logger from '@docusaurus/logger'; -import { - docuHash, - normalizeUrl, - simpleHash, - escapePath, -} from '@docusaurus/utils'; -import {getAllFinalRoutes} from '../utils'; +import {docuHash, simpleHash, escapePath, generate} from '@docusaurus/utils'; import type { Module, RouteConfig, RouteModules, ChunkNames, RouteChunkNames, - ReportingSeverity, } from '@docusaurus/types'; -export type LoadedRoutes = { +type RoutesCode = { /** Serialized routes config that can be directly emitted into temp file. */ routesConfig: string; /** @see {ChunkNames} */ @@ -36,13 +28,6 @@ export type LoadedRoutes = { registry: { [chunkName: string]: string; }; - /** - * Collect all page paths for injecting it later in the plugin lifecycle. - * This is useful for plugins like sitemaps, redirects etc... Only collects - * "actual" pages, i.e. those without subroutes, because if a route has - * subroutes, it is probably a wrapper. - */ - routesPaths: string[]; }; /** Indents every line of `str` by one level. */ @@ -172,19 +157,19 @@ function genChunkNames( routeModule: RouteModules, prefix: string, name: string, - res: LoadedRoutes, + res: RoutesCode, ): ChunkNames; function genChunkNames( routeModule: RouteModules | RouteModules[] | Module, prefix: string, name: string, - res: LoadedRoutes, + res: RoutesCode, ): ChunkNames | ChunkNames[] | string; function genChunkNames( routeModule: RouteModules | RouteModules[] | Module, prefix: string, name: string, - res: LoadedRoutes, + res: RoutesCode, ): string | ChunkNames | ChunkNames[] { if (isModule(routeModule)) { // This is a leaf node, no need to recurse @@ -201,41 +186,12 @@ function genChunkNames( return _.mapValues(routeModule, (v, key) => genChunkNames(v, key, name, res)); } -export function handleDuplicateRoutes( - pluginsRouteConfigs: RouteConfig[], - onDuplicateRoutes: ReportingSeverity, -): void { - if (onDuplicateRoutes === 'ignore') { - return; - } - const allRoutes: string[] = getAllFinalRoutes(pluginsRouteConfigs).map( - (routeConfig) => routeConfig.path, - ); - const seenRoutes = new Set(); - const duplicatePaths = allRoutes.filter((route) => { - if (seenRoutes.has(route)) { - return true; - } - seenRoutes.add(route); - return false; - }); - if (duplicatePaths.length > 0) { - logger.report( - onDuplicateRoutes, - )`Duplicate routes found!${duplicatePaths.map( - (duplicateRoute) => - logger.interpolate`Attempting to create page at url=${duplicateRoute}, but a page already exists at this route.`, - )} -This could lead to non-deterministic routing behavior.`; - } -} - /** * This is the higher level overview of route code generation. For each route * config node, it returns the node's serialized form, and mutates `registry`, * `routesPaths`, and `routesChunkNames` accordingly. */ -function genRouteCode(routeConfig: RouteConfig, res: LoadedRoutes): string { +function genRouteCode(routeConfig: RouteConfig, res: RoutesCode): string { const { path: routePath, component, @@ -254,10 +210,6 @@ ${JSON.stringify(routeConfig)}`, ); } - if (!subroutes) { - res.routesPaths.push(routePath); - } - const routeHash = simpleHash(JSON.stringify(routeConfig), 3); res.routesChunkNames[`${routePath}-${routeHash}`] = { // Avoid clash with a prop called "component" @@ -276,15 +228,6 @@ ${JSON.stringify(routeConfig)}`, }); } -/** - * Old stuff - * As far as I understand, this is what permits to SSG the 404.html file - * This is rendered through the catch-all ComponentCreator("*") route - * Note CDNs only understand the 404.html file by convention - * The extension probably permits to avoid emitting "/404/index.html" - */ -const NotFoundRoutePath = '/404.html'; - /** * Routes are prepared into three temp files: * @@ -294,18 +237,12 @@ const NotFoundRoutePath = '/404.html'; * chunk names. * - `registry`, a mapping from chunk names to options for react-loadable. */ -export function loadRoutes( - routeConfigs: RouteConfig[], - baseUrl: string, - onDuplicateRoutes: ReportingSeverity, -): LoadedRoutes { - handleDuplicateRoutes(routeConfigs, onDuplicateRoutes); - const res: LoadedRoutes = { +export function generateRoutesCode(routeConfigs: RouteConfig[]): RoutesCode { + const res: RoutesCode = { // To be written by `genRouteCode` routesConfig: '', routesChunkNames: {}, registry: {}, - routesPaths: [normalizeUrl([baseUrl, NotFoundRoutePath])], }; // `genRouteCode` would mutate `res` @@ -327,3 +264,65 @@ ${indent(routeConfigSerialized)}, return res; } + +const genRegistry = ({ + generatedFilesDir, + registry, +}: { + generatedFilesDir: string; + registry: RoutesCode['registry']; +}) => + generate( + generatedFilesDir, + 'registry.js', + `export default { +${Object.entries(registry) + .sort((a, b) => a[0].localeCompare(b[0])) + .map( + ([chunkName, modulePath]) => + // modulePath is already escaped by escapePath + ` "${chunkName}": [() => import(/* webpackChunkName: "${chunkName}" */ "${modulePath}"), "${modulePath}", require.resolveWeak("${modulePath}")],`, + ) + .join('\n')}}; +`, + ); + +const genRoutesChunkNames = ({ + generatedFilesDir, + routesChunkNames, +}: { + generatedFilesDir: string; + routesChunkNames: RoutesCode['routesChunkNames']; +}) => + generate( + generatedFilesDir, + 'routesChunkNames.json', + JSON.stringify(routesChunkNames, null, 2), + ); + +const genRoutes = ({ + generatedFilesDir, + routesConfig, +}: { + generatedFilesDir: string; + routesConfig: RoutesCode['routesConfig']; +}) => generate(generatedFilesDir, 'routes.js', routesConfig); + +type GenerateRouteFilesParams = { + generatedFilesDir: string; + routeConfigs: RouteConfig[]; + baseUrl: string; +}; + +export async function generateRouteFiles({ + generatedFilesDir, + routeConfigs, +}: GenerateRouteFilesParams): Promise { + const {registry, routesChunkNames, routesConfig} = + generateRoutesCode(routeConfigs); + await Promise.all([ + genRegistry({generatedFilesDir, registry}), + genRoutesChunkNames({generatedFilesDir, routesChunkNames}), + genRoutes({generatedFilesDir, routesConfig}), + ]); +} diff --git a/packages/docusaurus/src/server/index.ts b/packages/docusaurus/src/server/index.ts index 48f9baf189fb..42bcff3ab512 100644 --- a/packages/docusaurus/src/server/index.ts +++ b/packages/docusaurus/src/server/index.ts @@ -6,25 +6,25 @@ */ import path from 'path'; -import _ from 'lodash'; import { localizePath, DEFAULT_BUILD_DIR_NAME, GENERATED_FILES_DIR_NAME, } from '@docusaurus/utils'; +import combinePromises from 'combine-promises'; import {loadSiteConfig} from './config'; import {loadClientModules} from './clientModules'; import {loadPlugins} from './plugins'; -import {loadRoutes} from './codegen/routes'; import {loadHtmlTags} from './htmlTags'; import {loadSiteMetadata} from './siteMetadata'; import {loadI18n} from './i18n'; import { - readCodeTranslationFileContent, + loadSiteCodeTranslations, getPluginsDefaultCodeTranslationMessages, } from './translations/translations'; import {PerfLogger} from '../utils'; -import {generateSiteCode} from './codegen/codegen'; +import {generateSiteFiles} from './codegen/codegen'; +import {getRoutesPaths, handleDuplicateRoutes} from './routes'; import type {DocusaurusConfig, LoadContext, Props} from '@docusaurus/types'; export type LoadContextOptions = { @@ -81,23 +81,15 @@ export async function loadContext( options, pathType: 'fs', }); - - const siteConfig: DocusaurusConfig = {...initialSiteConfig, baseUrl}; - const localizationDir = path.resolve( siteDir, i18n.path, i18n.localeConfigs[i18n.currentLocale]!.path, ); - const codeTranslationFileContent = - (await readCodeTranslationFileContent({localizationDir})) ?? {}; + const siteConfig: DocusaurusConfig = {...initialSiteConfig, baseUrl}; - // We only need key->message for code translations - const codeTranslations = _.mapValues( - codeTranslationFileContent, - (value) => value.message, - ); + const codeTranslations = await loadSiteCodeTranslations({localizationDir}); return { siteDir, @@ -140,35 +132,24 @@ export async function load(options: LoadContextOptions): Promise { const {plugins, pluginsRouteConfigs, globalData} = await loadPlugins(context); PerfLogger.end('Load - loadPlugins'); - PerfLogger.start('Load - loadClientModules'); - const clientModules = loadClientModules(plugins); - PerfLogger.end('Load - loadClientModules'); - - PerfLogger.start('Load - loadHtmlTags'); const {headTags, preBodyTags, postBodyTags} = loadHtmlTags(plugins); - PerfLogger.end('Load - loadHtmlTags'); - - PerfLogger.start('Load - loadRoutes'); - const {registry, routesChunkNames, routesConfig, routesPaths} = loadRoutes( - pluginsRouteConfigs, - baseUrl, - siteConfig.onDuplicateRoutes, - ); - PerfLogger.end('Load - loadRoutes'); - - PerfLogger.start('Load - load codeTranslations'); - const codeTranslations = { - ...(await getPluginsDefaultCodeTranslationMessages(plugins)), - ...siteCodeTranslations, - }; - PerfLogger.end('Load - load codeTranslations'); + const clientModules = loadClientModules(plugins); - PerfLogger.start('Load - loadSiteMetadata'); - const siteMetadata = await loadSiteMetadata({plugins, siteDir}); - PerfLogger.end('Load - loadSiteMetadata'); + const {codeTranslations, siteMetadata} = await combinePromises({ + codeTranslations: PerfLogger.async( + 'Load - loadCodeTranslations', + async () => ({ + ...(await getPluginsDefaultCodeTranslationMessages(plugins)), + ...siteCodeTranslations, + }), + ), + siteMetadata: PerfLogger.async('Load - loadSiteMetadata', () => + loadSiteMetadata({plugins, siteDir}), + ), + }); PerfLogger.start('Load - generateSiteCode'); - await generateSiteCode({ + await generateSiteFiles({ generatedFilesDir, clientModules, siteConfig, @@ -176,12 +157,14 @@ export async function load(options: LoadContextOptions): Promise { i18n, codeTranslations, globalData, - routesChunkNames, - routesConfig, - registry, + routeConfigs: pluginsRouteConfigs, + baseUrl, }); PerfLogger.end('Load - generateSiteCode'); + handleDuplicateRoutes(pluginsRouteConfigs, siteConfig.onDuplicateRoutes); + const routesPaths = getRoutesPaths(pluginsRouteConfigs, baseUrl); + return { siteConfig, siteConfigPath, diff --git a/packages/docusaurus/src/server/plugins/index.ts b/packages/docusaurus/src/server/plugins/index.ts index 73fba212b289..64a58ed0c2ca 100644 --- a/packages/docusaurus/src/server/plugins/index.ts +++ b/packages/docusaurus/src/server/plugins/index.ts @@ -12,6 +12,7 @@ import {initPlugins} from './init'; import {createBootstrapPlugin, createMDXFallbackPlugin} from './synthetic'; import {localizePluginTranslationFile} from '../translations/translations'; import {applyRouteTrailingSlash, sortConfig} from './routeConfig'; +import {PerfLogger} from '../../utils'; import type { LoadContext, PluginContentLoadedActions, @@ -35,7 +36,9 @@ export async function loadPlugins(context: LoadContext): Promise<{ globalData: GlobalData; }> { // 1. Plugin Lifecycle - Initialization/Constructor. + PerfLogger.start('Plugins - initPlugins'); const plugins: InitializedPlugin[] = await initPlugins(context); + PerfLogger.end('Plugins - initPlugins'); plugins.push( createBootstrapPlugin(context), @@ -48,9 +51,17 @@ export async function loadPlugins(context: LoadContext): Promise<{ // need to run in certain order or depend on others for data. // This would also translate theme config and content upfront, given the // translation files that the plugin declares. + PerfLogger.start(`Plugins - loadContent`); const loadedPlugins: LoadedPlugin[] = await Promise.all( plugins.map(async (plugin) => { + PerfLogger.start( + `Plugins - loadContent - ${plugin.name}@${plugin.options.id}`, + ); const content = await plugin.loadContent?.(); + PerfLogger.end( + `Plugins - loadContent - ${plugin.name}@${plugin.options.id}`, + ); + const rawTranslationFiles = (await plugin.getTranslationFiles?.({content})) ?? []; const translationFiles = await Promise.all( @@ -62,6 +73,7 @@ export async function loadPlugins(context: LoadContext): Promise<{ }), ), ); + const translatedContent = plugin.translateContent?.({content, translationFiles}) ?? content; const translatedThemeConfigSlice = plugin.translateThemeConfig?.({ @@ -75,6 +87,7 @@ export async function loadPlugins(context: LoadContext): Promise<{ return {...plugin, content: translatedContent}; }), ); + PerfLogger.end(`Plugins - loadContent`); const allContent: AllContent = _.chain(loadedPlugins) .groupBy((item) => item.name) @@ -90,6 +103,7 @@ export async function loadPlugins(context: LoadContext): Promise<{ const pluginsRouteConfigs: RouteConfig[] = []; const globalData: GlobalData = {}; + PerfLogger.start(`Plugins - contentLoaded`); await Promise.all( loadedPlugins.map(async ({content, ...plugin}) => { if (!plugin.contentLoaded) { @@ -144,6 +158,7 @@ export async function loadPlugins(context: LoadContext): Promise<{ await plugin.contentLoaded({content, actions, allContent}); }), ); + PerfLogger.end(`Plugins - contentLoaded`); // Sort the route config. This ensures that route with nested // routes are always placed last. diff --git a/packages/docusaurus/src/server/routes.ts b/packages/docusaurus/src/server/routes.ts new file mode 100644 index 000000000000..f4d8d670a0b9 --- /dev/null +++ b/packages/docusaurus/src/server/routes.ts @@ -0,0 +1,66 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import logger from '@docusaurus/logger'; +import {normalizeUrl} from '@docusaurus/utils'; +import type {RouteConfig, ReportingSeverity} from '@docusaurus/types'; + +// Recursively get the final routes (routes with no subroutes) +export function getAllFinalRoutes(routeConfig: RouteConfig[]): RouteConfig[] { + function getFinalRoutes(route: RouteConfig): RouteConfig[] { + return route.routes ? route.routes.flatMap(getFinalRoutes) : [route]; + } + return routeConfig.flatMap(getFinalRoutes); +} + +export function handleDuplicateRoutes( + pluginsRouteConfigs: RouteConfig[], + onDuplicateRoutes: ReportingSeverity, +): void { + if (onDuplicateRoutes === 'ignore') { + return; + } + const allRoutes: string[] = getAllFinalRoutes(pluginsRouteConfigs).map( + (routeConfig) => routeConfig.path, + ); + const seenRoutes = new Set(); + const duplicatePaths = allRoutes.filter((route) => { + if (seenRoutes.has(route)) { + return true; + } + seenRoutes.add(route); + return false; + }); + if (duplicatePaths.length > 0) { + logger.report( + onDuplicateRoutes, + )`Duplicate routes found!${duplicatePaths.map( + (duplicateRoute) => + logger.interpolate`Attempting to create page at url=${duplicateRoute}, but a page already exists at this route.`, + )} +This could lead to non-deterministic routing behavior.`; + } +} + +/** + * Old stuff + * As far as I understand, this is what permits to SSG the 404.html file + * This is rendered through the catch-all ComponentCreator("*") route + * Note CDNs only understand the 404.html file by convention + * The extension probably permits to avoid emitting "/404/index.html" + */ +const NotFoundRoutePath = '/404.html'; + +export function getRoutesPaths( + routeConfigs: RouteConfig[], + baseUrl: string, +): string[] { + return [ + normalizeUrl([baseUrl, NotFoundRoutePath]), + ...getAllFinalRoutes(routeConfigs).map((r) => r.path), + ]; +} diff --git a/packages/docusaurus/src/server/translations/translations.ts b/packages/docusaurus/src/server/translations/translations.ts index 83d19495bffc..ceb4efc105ae 100644 --- a/packages/docusaurus/src/server/translations/translations.ts +++ b/packages/docusaurus/src/server/translations/translations.ts @@ -279,3 +279,15 @@ Please report this Docusaurus issue. name=${unusedDefaultCodeMessages}`; }), ); } + +export async function loadSiteCodeTranslations({ + localizationDir, +}: { + localizationDir: string; +}): Promise { + const codeTranslationFileContent = + (await readCodeTranslationFileContent({localizationDir})) ?? {}; + + // We only need key->message for code translations + return _.mapValues(codeTranslationFileContent, (value) => value.message); +} diff --git a/packages/docusaurus/src/server/utils.ts b/packages/docusaurus/src/server/utils.ts index e0e67454c7ea..d6c09dc468e1 100644 --- a/packages/docusaurus/src/server/utils.ts +++ b/packages/docusaurus/src/server/utils.ts @@ -7,15 +7,6 @@ import path from 'path'; import {posixPath, Globby} from '@docusaurus/utils'; -import type {RouteConfig} from '@docusaurus/types'; - -// Recursively get the final routes (routes with no subroutes) -export function getAllFinalRoutes(routeConfig: RouteConfig[]): RouteConfig[] { - function getFinalRoutes(route: RouteConfig): RouteConfig[] { - return route.routes ? route.routes.flatMap(getFinalRoutes) : [route]; - } - return routeConfig.flatMap(getFinalRoutes); -} // Globby that fix Windows path patterns // See https://github.com/facebook/docusaurus/pull/4222#issuecomment-795517329 diff --git a/packages/docusaurus/src/utils.ts b/packages/docusaurus/src/utils.ts index 044e22d3fe70..9fe1c616a523 100644 --- a/packages/docusaurus/src/utils.ts +++ b/packages/docusaurus/src/utils.ts @@ -15,6 +15,10 @@ type PerfLoggerAPI = { start: (label: string) => void; end: (label: string) => void; log: (message: string) => void; + async: ( + label: string, + asyncFn: () => Result | Promise, + ) => Promise; }; function createPerfLogger(): PerfLoggerAPI { @@ -24,14 +28,31 @@ function createPerfLogger(): PerfLoggerAPI { start: noop, end: noop, log: noop, + async: async (_label, asyncFn) => asyncFn(), }; } const prefix = logger.yellow(`[PERF] `); + + const start: PerfLoggerAPI['start'] = (label) => console.time(prefix + label); + + const end: PerfLoggerAPI['end'] = (label) => console.timeEnd(prefix + label); + + const log: PerfLoggerAPI['log'] = (label: string) => + console.log(prefix + label); + + const async: PerfLoggerAPI['async'] = async (label, asyncFn) => { + start(label); + const result = await asyncFn(); + end(label); + return result; + }; + return { - start: (label) => console.time(prefix + label), - end: (label) => console.timeEnd(prefix + label), - log: (label) => console.log(prefix + label), + start, + end, + log, + async, }; } From 841c7d6a8c800fc7d83ce64735e792de4c48be51 Mon Sep 17 00:00:00 2001 From: sebastien Date: Thu, 29 Feb 2024 20:02:18 +0100 Subject: [PATCH 04/18] refactor start command --- packages/docusaurus/src/commands/start.ts | 321 ------------------ .../docusaurus/src/commands/start/start.ts | 105 ++++++ .../docusaurus/src/commands/start/utils.ts | 37 ++ .../docusaurus/src/commands/start/watcher.ts | 135 ++++++++ .../docusaurus/src/commands/start/webpack.ts | 179 ++++++++++ packages/docusaurus/src/index.ts | 2 +- 6 files changed, 457 insertions(+), 322 deletions(-) delete mode 100644 packages/docusaurus/src/commands/start.ts create mode 100644 packages/docusaurus/src/commands/start/start.ts create mode 100644 packages/docusaurus/src/commands/start/utils.ts create mode 100644 packages/docusaurus/src/commands/start/watcher.ts create mode 100644 packages/docusaurus/src/commands/start/webpack.ts diff --git a/packages/docusaurus/src/commands/start.ts b/packages/docusaurus/src/commands/start.ts deleted file mode 100644 index 7527df41ddb9..000000000000 --- a/packages/docusaurus/src/commands/start.ts +++ /dev/null @@ -1,321 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -import fs from 'fs-extra'; -import path from 'path'; -import _ from 'lodash'; -import logger from '@docusaurus/logger'; -import {normalizeUrl, posixPath} from '@docusaurus/utils'; -import chokidar from 'chokidar'; -import openBrowser from 'react-dev-utils/openBrowser'; -import {prepareUrls} from 'react-dev-utils/WebpackDevServerUtils'; -import evalSourceMapMiddleware from 'react-dev-utils/evalSourceMapMiddleware'; -import webpack from 'webpack'; -import WebpackDevServer from 'webpack-dev-server'; -import merge from 'webpack-merge'; -import {load, type LoadContextOptions} from '../server'; -import {createStartClientConfig} from '../webpack/client'; -import { - getHttpsConfig, - formatStatsErrorMessage, - printStatsWarnings, - executePluginsConfigurePostCss, - executePluginsConfigureWebpack, -} from '../webpack/utils'; -import {getHostPort, type HostPortOptions} from '../server/getHostPort'; -import {PerfLogger} from '../utils'; -import type {Compiler} from 'webpack'; -import type {Props} from '@docusaurus/types'; - -export type StartCLIOptions = HostPortOptions & - Pick & { - hotOnly?: boolean; - open?: boolean; - poll?: boolean | number; - minify?: boolean; - }; - -export async function start( - siteDirParam: string = '.', - cliOptions: Partial = {}, -): Promise { - // Temporary workaround to unlock the ability to translate the site config - // We'll remove it if a better official API can be designed - // See https://github.com/facebook/docusaurus/issues/4542 - process.env.DOCUSAURUS_CURRENT_LOCALE = cliOptions.locale; - - const siteDir = await fs.realpath(siteDirParam); - - logger.info('Starting the development server...'); - - async function loadSite() { - PerfLogger.start('Loading site'); - const result = await load({ - siteDir, - config: cliOptions.config, - locale: cliOptions.locale, - localizePath: undefined, // Should this be configurable? - }); - PerfLogger.end('Loading site'); - return result; - } - - // Process all related files as a prop. - const props = await loadSite(); - - const {host, port, getOpenUrl} = await createUrlUtils({cliOptions}); - const openUrl = getOpenUrl({baseUrl: props.baseUrl}); - - logger.success`Docusaurus website is running at: url=${openUrl}`; - - // Reload files processing. - const reload = _.debounce(() => { - loadSite() - .then(({baseUrl: newBaseUrl}) => { - const newOpenUrl = getOpenUrl({baseUrl: newBaseUrl}); - if (newOpenUrl !== openUrl) { - logger.success`Docusaurus website is running at: url=${newOpenUrl}`; - } - }) - .catch((err: Error) => { - logger.error(err.stack); - }); - }, 500); - - // TODO this is historically not optimized! - // When any site file changes, we reload absolutely everything :/ - // At least we should try to reload only one plugin individually? - setupFileWatchers({ - props, - cliOptions, - onFileChange: () => { - reload(); - }, - }); - - const config = await getStartClientConfig({ - props, - minify: cliOptions.minify ?? true, - poll: cliOptions.poll, - }); - - const compiler = webpack(config); - registerE2ETestHook(compiler); - - const defaultDevServerConfig = await createDevServerConfig({ - cliOptions, - props, - host, - port, - }); - - // Allow plugin authors to customize/override devServer config - const devServerConfig: WebpackDevServer.Configuration = merge( - [defaultDevServerConfig, config.devServer].filter(Boolean), - ); - - const devServer = new WebpackDevServer(devServerConfig, compiler); - devServer.startCallback(() => { - if (cliOptions.open) { - openBrowser(openUrl); - } - }); - - ['SIGINT', 'SIGTERM'].forEach((sig) => { - process.on(sig, () => { - devServer.stop(); - process.exit(); - }); - }); -} - -function createPollingOptions({cliOptions}: {cliOptions: StartCLIOptions}) { - return { - usePolling: !!cliOptions.poll, - interval: Number.isInteger(cliOptions.poll) - ? (cliOptions.poll as number) - : undefined, - }; -} - -function setupFileWatchers({ - props, - cliOptions, - onFileChange, -}: { - props: Props; - cliOptions: StartCLIOptions; - onFileChange: () => void; -}) { - const {siteDir} = props; - const pathsToWatch = getPathsToWatch({props}); - - const pollingOptions = createPollingOptions({cliOptions}); - const fsWatcher = chokidar.watch(pathsToWatch, { - cwd: siteDir, - ignoreInitial: true, - ...{pollingOptions}, - }); - - ['add', 'change', 'unlink', 'addDir', 'unlinkDir'].forEach((event) => - fsWatcher.on(event, onFileChange), - ); -} - -function getPathsToWatch({props}: {props: Props}): string[] { - const {siteDir, siteConfigPath, plugins, localizationDir} = props; - - const normalizeToSiteDir = (filepath: string) => { - if (filepath && path.isAbsolute(filepath)) { - return posixPath(path.relative(siteDir, filepath)); - } - return posixPath(filepath); - }; - - const pluginsPaths = plugins - .flatMap((plugin) => plugin.getPathsToWatch?.() ?? []) - .filter(Boolean) - .map(normalizeToSiteDir); - - return [...pluginsPaths, siteConfigPath, localizationDir]; -} - -async function createUrlUtils({cliOptions}: {cliOptions: StartCLIOptions}) { - const protocol: string = process.env.HTTPS === 'true' ? 'https' : 'http'; - - const {host, port} = await getHostPort(cliOptions); - if (port === null) { - return process.exit(); - } - - const getOpenUrl = ({baseUrl}: {baseUrl: string}) => { - const urls = prepareUrls(protocol, host, port); - return normalizeUrl([urls.localUrlForBrowser, baseUrl]); - }; - - return {host, port, getOpenUrl}; -} - -async function createDevServerConfig({ - cliOptions, - props, - host, - port, -}: { - cliOptions: StartCLIOptions; - props: Props; - host: string; - port: number; -}): Promise { - const {baseUrl, siteDir, siteConfig} = props; - - const pollingOptions = createPollingOptions({cliOptions}); - - const httpsConfig = await getHttpsConfig(); - - // https://webpack.js.org/configuration/dev-server - return { - hot: cliOptions.hotOnly ? 'only' : true, - liveReload: false, - client: { - progress: true, - overlay: { - warnings: false, - errors: true, - }, - webSocketURL: { - hostname: '0.0.0.0', - port: 0, - }, - }, - headers: { - 'access-control-allow-origin': '*', - }, - devMiddleware: { - publicPath: baseUrl, - // Reduce log verbosity, see https://github.com/facebook/docusaurus/pull/5420#issuecomment-906613105 - stats: 'summary', - }, - static: siteConfig.staticDirectories.map((dir) => ({ - publicPath: baseUrl, - directory: path.resolve(siteDir, dir), - watch: { - // Useful options for our own monorepo using symlinks! - // See https://github.com/webpack/webpack/issues/11612#issuecomment-879259806 - followSymlinks: true, - ignored: /node_modules\/(?!@docusaurus)/, - ...{pollingOptions}, - }, - })), - ...(httpsConfig && { - server: - typeof httpsConfig === 'object' - ? { - type: 'https', - options: httpsConfig, - } - : 'https', - }), - historyApiFallback: { - rewrites: [{from: /\/*/, to: baseUrl}], - }, - allowedHosts: 'all', - host, - port, - setupMiddlewares: (middlewares, devServer) => { - // This lets us fetch source contents from webpack for the error overlay. - middlewares.unshift(evalSourceMapMiddleware(devServer)); - return middlewares; - }, - }; -} - -// E2E_TEST=true docusaurus start -// Makes "docusaurus start" exit immediately on success/error, for E2E test -function registerE2ETestHook(compiler: Compiler) { - compiler.hooks.done.tap('done', (stats) => { - const errorsWarnings = stats.toJson('errors-warnings'); - const statsErrorMessage = formatStatsErrorMessage(errorsWarnings); - if (statsErrorMessage) { - console.error(statsErrorMessage); - } - printStatsWarnings(errorsWarnings); - if (process.env.E2E_TEST) { - if (stats.hasErrors()) { - logger.error('E2E_TEST: Project has compiler errors.'); - process.exit(1); - } - logger.success('E2E_TEST: Project can compile.'); - process.exit(0); - } - }); -} - -async function getStartClientConfig({ - props, - minify, - poll, -}: { - props: Props; - minify: boolean; - poll: number | boolean | undefined; -}) { - const {plugins, siteConfig} = props; - let {clientConfig: config} = await createStartClientConfig({ - props, - minify, - poll, - }); - config = executePluginsConfigurePostCss({plugins, config}); - config = executePluginsConfigureWebpack({ - plugins, - config, - isServer: false, - jsLoader: siteConfig.webpack?.jsLoader, - }); - return config; -} diff --git a/packages/docusaurus/src/commands/start/start.ts b/packages/docusaurus/src/commands/start/start.ts new file mode 100644 index 000000000000..1e7f5636c3c7 --- /dev/null +++ b/packages/docusaurus/src/commands/start/start.ts @@ -0,0 +1,105 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import fs from 'fs-extra'; +import _ from 'lodash'; +import logger from '@docusaurus/logger'; +import openBrowser from 'react-dev-utils/openBrowser'; +import {load, type LoadContextOptions} from '../../server'; +import {type HostPortOptions} from '../../server/getHostPort'; +import {PerfLogger} from '../../utils'; +import {setupSiteFileWatchers} from './watcher'; +import {createWebpackDevServer} from './webpack'; +import {createOpenUrlContext} from './utils'; +import type {LoadedPlugin} from '@docusaurus/types'; + +export type StartCLIOptions = HostPortOptions & + Pick & { + hotOnly?: boolean; + open?: boolean; + poll?: boolean | number; + minify?: boolean; + }; + +export async function start( + siteDirParam: string = '.', + cliOptions: Partial = {}, +): Promise { + // Temporary workaround to unlock the ability to translate the site config + // We'll remove it if a better official API can be designed + // See https://github.com/facebook/docusaurus/issues/4542 + process.env.DOCUSAURUS_CURRENT_LOCALE = cliOptions.locale; + const siteDir = await fs.realpath(siteDirParam); + logger.info('Starting the development server...'); + + async function loadSite() { + PerfLogger.start('Loading site'); + const result = await load({ + siteDir, + config: cliOptions.config, + locale: cliOptions.locale, + localizePath: undefined, // Should this be configurable? + }); + PerfLogger.end('Loading site'); + return result; + } + + // Process all related files as a prop. + const props = await loadSite(); + + const openUrlContext = await createOpenUrlContext({cliOptions}); + const openUrl = openUrlContext.getOpenUrl({baseUrl: props.baseUrl}); + + logger.success`Docusaurus website is running at: url=${openUrl}`; + + const reloadSite = _.debounce(() => { + loadSite() + .then(({baseUrl: newBaseUrl}) => { + const newOpenUrl = openUrlContext.getOpenUrl({baseUrl: newBaseUrl}); + if (newOpenUrl !== openUrl) { + logger.success`Docusaurus website is running at: url=${newOpenUrl}`; + } + }) + .catch((err: Error) => { + logger.error(err.stack); + }); + }, 500); + + const reloadPlugin = (plugin: LoadedPlugin) => { + console.log('reload plugin', plugin); + // TODO this is historically not optimized! + // When any site file changes, we reload absolutely everything :/ + // At least we should try to reload only one plugin individually? + reloadSite(); + }; + + setupSiteFileWatchers({props, cliOptions}, ({plugin}) => { + if (plugin) { + reloadPlugin(plugin); + } else { + reloadSite(); + } + }); + + const devServer = await createWebpackDevServer({ + props, + cliOptions, + openUrlContext, + }); + + ['SIGINT', 'SIGTERM'].forEach((sig) => { + process.on(sig, () => { + devServer.stop(); + process.exit(); + }); + }); + + await devServer.start(); + if (cliOptions.open) { + openBrowser(openUrlContext.getOpenUrl({baseUrl: props.baseUrl})); + } +} diff --git a/packages/docusaurus/src/commands/start/utils.ts b/packages/docusaurus/src/commands/start/utils.ts new file mode 100644 index 000000000000..bb3398072fc3 --- /dev/null +++ b/packages/docusaurus/src/commands/start/utils.ts @@ -0,0 +1,37 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import {prepareUrls} from 'react-dev-utils/WebpackDevServerUtils'; +import {normalizeUrl} from '@docusaurus/utils'; +import {getHostPort} from '../../server/getHostPort'; +import type {StartCLIOptions} from './start'; + +export type OpenUrlContext = { + host: string; + port: number; + getOpenUrl: ({baseUrl}: {baseUrl: string}) => string; +}; + +export async function createOpenUrlContext({ + cliOptions, +}: { + cliOptions: StartCLIOptions; +}): Promise { + const protocol: string = process.env.HTTPS === 'true' ? 'https' : 'http'; + + const {host, port} = await getHostPort(cliOptions); + if (port === null) { + return process.exit(); + } + + const getOpenUrl: OpenUrlContext['getOpenUrl'] = ({baseUrl}) => { + const urls = prepareUrls(protocol, host, port); + return normalizeUrl([urls.localUrlForBrowser, baseUrl]); + }; + + return {host, port, getOpenUrl}; +} diff --git a/packages/docusaurus/src/commands/start/watcher.ts b/packages/docusaurus/src/commands/start/watcher.ts new file mode 100644 index 000000000000..db74726bb272 --- /dev/null +++ b/packages/docusaurus/src/commands/start/watcher.ts @@ -0,0 +1,135 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import path from 'path'; +import chokidar from 'chokidar'; +import {posixPath} from '@docusaurus/utils'; +import type {StartCLIOptions} from './start'; +import type {LoadedPlugin, Props} from '@docusaurus/types'; + +type PollingOptions = { + usePolling: boolean; + interval: number | undefined; +}; + +export function createPollingOptions( + cliOptions: StartCLIOptions, +): PollingOptions { + return { + usePolling: !!cliOptions.poll, + interval: Number.isInteger(cliOptions.poll) + ? (cliOptions.poll as number) + : undefined, + }; +} + +export type FileWatchEventName = + | 'add' + | 'addDir' + | 'change' + | 'unlink' + | 'unlinkDir'; + +export type FileWatchEvent = { + name: FileWatchEventName; + path: string; +}; + +type WatchParams = { + pathsToWatch: string[]; + siteDir: string; +} & PollingOptions; + +/** + * Watch file system paths for changes and emit events + * Returns an async handle to stop watching + */ +export function watch( + params: WatchParams, + callback: (event: FileWatchEvent) => void, +): () => Promise { + const {pathsToWatch, siteDir, ...options} = params; + + const fsWatcher = chokidar.watch(pathsToWatch, { + cwd: siteDir, + ignoreInitial: true, + ...options, + }); + + fsWatcher.on('all', (name, eventPath) => callback({name, path: eventPath})); + + return () => fsWatcher.close(); +} + +export function getSitePathsToWatch({props}: {props: Props}): string[] { + return [ + // TODO we should also watch all imported modules! + // Use https://github.com/vercel/nft ? + props.siteConfigPath, + props.localizationDir, + ]; +} + +export function getPluginPathsToWatch({ + siteDir, + plugin, +}: { + siteDir: string; + plugin: LoadedPlugin; +}): string[] { + const normalizeToSiteDir = (filepath: string) => { + if (filepath && path.isAbsolute(filepath)) { + return posixPath(path.relative(siteDir, filepath)); + } + return posixPath(filepath); + }; + + return (plugin.getPathsToWatch?.() ?? []) + .filter(Boolean) + .map(normalizeToSiteDir); +} + +export function setupSiteFileWatchers( + { + props, + cliOptions, + }: { + props: Props; + cliOptions: StartCLIOptions; + }, + callback: (params: { + plugin: LoadedPlugin | null; + event: FileWatchEvent; + }) => void, +): void { + const {siteDir} = props; + const pollingOptions = createPollingOptions(cliOptions); + + // TODO on config / or local plugin updates, + // the getFilePathsToWatch lifecycle code might get updated + // so we should probably reset the watchers? + + watch( + { + pathsToWatch: getSitePathsToWatch({props}), + siteDir: props.siteDir, + ...pollingOptions, + }, + (event) => callback({plugin: null, event}), + ); + + props.plugins.forEach((plugin) => { + watch( + { + pathsToWatch: getPluginPathsToWatch({plugin, siteDir}), + siteDir, + ...pollingOptions, + }, + (event) => callback({plugin, event}), + ); + }); +} diff --git a/packages/docusaurus/src/commands/start/webpack.ts b/packages/docusaurus/src/commands/start/webpack.ts new file mode 100644 index 000000000000..4a9d8fa8413e --- /dev/null +++ b/packages/docusaurus/src/commands/start/webpack.ts @@ -0,0 +1,179 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import path from 'path'; +import merge from 'webpack-merge'; +import webpack from 'webpack'; +import logger from '@docusaurus/logger'; +import WebpackDevServer from 'webpack-dev-server'; +import evalSourceMapMiddleware from 'react-dev-utils/evalSourceMapMiddleware'; +import {createPollingOptions} from './watcher'; +import { + executePluginsConfigurePostCss, + executePluginsConfigureWebpack, + formatStatsErrorMessage, + getHttpsConfig, + printStatsWarnings, +} from '../../webpack/utils'; +import {createStartClientConfig} from '../../webpack/client'; +import type {StartCLIOptions} from './start'; +import type {Props} from '@docusaurus/types'; +import type {Compiler} from 'webpack'; +import type {OpenUrlContext} from './utils'; + +// E2E_TEST=true docusaurus start +// Makes "docusaurus start" exit immediately on success/error, for E2E test +function registerWebpackE2ETestHook(compiler: Compiler) { + compiler.hooks.done.tap('done', (stats) => { + const errorsWarnings = stats.toJson('errors-warnings'); + const statsErrorMessage = formatStatsErrorMessage(errorsWarnings); + if (statsErrorMessage) { + console.error(statsErrorMessage); + } + printStatsWarnings(errorsWarnings); + if (process.env.E2E_TEST) { + if (stats.hasErrors()) { + logger.error('E2E_TEST: Project has compiler errors.'); + process.exit(1); + } + logger.success('E2E_TEST: Project can compile.'); + process.exit(0); + } + }); +} + +async function createDevServerConfig({ + cliOptions, + props, + host, + port, +}: { + cliOptions: StartCLIOptions; + props: Props; + host: string; + port: number; +}): Promise { + const {baseUrl, siteDir, siteConfig} = props; + + const pollingOptions = createPollingOptions(cliOptions); + + const httpsConfig = await getHttpsConfig(); + + // https://webpack.js.org/configuration/dev-server + return { + hot: cliOptions.hotOnly ? 'only' : true, + liveReload: false, + client: { + progress: true, + overlay: { + warnings: false, + errors: true, + }, + webSocketURL: { + hostname: '0.0.0.0', + port: 0, + }, + }, + headers: { + 'access-control-allow-origin': '*', + }, + devMiddleware: { + publicPath: baseUrl, + // Reduce log verbosity, see https://github.com/facebook/docusaurus/pull/5420#issuecomment-906613105 + stats: 'summary', + }, + static: siteConfig.staticDirectories.map((dir) => ({ + publicPath: baseUrl, + directory: path.resolve(siteDir, dir), + watch: { + // Useful options for our own monorepo using symlinks! + // See https://github.com/webpack/webpack/issues/11612#issuecomment-879259806 + followSymlinks: true, + ignored: /node_modules\/(?!@docusaurus)/, + ...{pollingOptions}, + }, + })), + ...(httpsConfig && { + server: + typeof httpsConfig === 'object' + ? { + type: 'https', + options: httpsConfig, + } + : 'https', + }), + historyApiFallback: { + rewrites: [{from: /\/*/, to: baseUrl}], + }, + allowedHosts: 'all', + host, + port, + setupMiddlewares: (middlewares, devServer) => { + // This lets us fetch source contents from webpack for the error overlay. + middlewares.unshift(evalSourceMapMiddleware(devServer)); + return middlewares; + }, + }; +} + +async function getStartClientConfig({ + props, + minify, + poll, +}: { + props: Props; + minify: boolean; + poll: number | boolean | undefined; +}) { + const {plugins, siteConfig} = props; + let {clientConfig: config} = await createStartClientConfig({ + props, + minify, + poll, + }); + config = executePluginsConfigurePostCss({plugins, config}); + config = executePluginsConfigureWebpack({ + plugins, + config, + isServer: false, + jsLoader: siteConfig.webpack?.jsLoader, + }); + return config; +} + +export async function createWebpackDevServer({ + props, + cliOptions, + openUrlContext, +}: { + props: Props; + cliOptions: StartCLIOptions; + openUrlContext: OpenUrlContext; +}): Promise { + const config = await getStartClientConfig({ + props, + minify: cliOptions.minify ?? true, + poll: cliOptions.poll, + }); + + const compiler = webpack(config); + registerWebpackE2ETestHook(compiler); + + const defaultDevServerConfig = await createDevServerConfig({ + cliOptions, + props, + host: openUrlContext.host, + port: openUrlContext.port, + }); + + // Allow plugin authors to customize/override devServer config + const devServerConfig: WebpackDevServer.Configuration = merge( + [defaultDevServerConfig, config.devServer].filter(Boolean), + ); + + return new WebpackDevServer(devServerConfig, compiler); +} diff --git a/packages/docusaurus/src/index.ts b/packages/docusaurus/src/index.ts index 097eaad0e077..82ba70af2e93 100644 --- a/packages/docusaurus/src/index.ts +++ b/packages/docusaurus/src/index.ts @@ -10,7 +10,7 @@ export {clear} from './commands/clear'; export {deploy} from './commands/deploy'; export {externalCommand} from './commands/external'; export {serve} from './commands/serve'; -export {start} from './commands/start'; +export {start} from './commands/start/start'; export {swizzle} from './commands/swizzle'; export {writeHeadingIds} from './commands/writeHeadingIds'; export {writeTranslations} from './commands/writeTranslations'; From dcbb0d677f37668a605166f5cb8a70022471ddd9 Mon Sep 17 00:00:00 2001 From: slorber Date: Thu, 29 Feb 2024 19:10:44 +0000 Subject: [PATCH 05/18] refactor: apply lint autofix --- project-words.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/project-words.txt b/project-words.txt index cf88f27ad900..bc12986f72ae 100644 --- a/project-words.txt +++ b/project-words.txt @@ -47,6 +47,8 @@ changefreq Chedeau chedeau Clément +Codegen +codegen codesandbox Codespaces commonmark From 86777219e9479a4b0f9ed6503ff7fc83e528326d Mon Sep 17 00:00:00 2001 From: sebastien Date: Fri, 1 Mar 2024 15:24:13 +0100 Subject: [PATCH 06/18] Refactor site loading --- packages/docusaurus-types/src/context.d.ts | 4 +++ packages/docusaurus-types/src/index.d.ts | 1 + packages/docusaurus/src/commands/build.ts | 6 ++-- .../docusaurus/src/commands/start/start.ts | 28 ++++++++-------- .../src/server/__tests__/index.test.ts | 12 +++---- .../src/server/__tests__/testUtils.ts | 8 ++--- packages/docusaurus/src/server/index.ts | 15 +++++++-- .../docusaurus/src/server/plugins/index.ts | 14 +++++--- .../src/webpack/__tests__/client.test.ts | 32 ++++++++++++++----- .../src/webpack/__tests__/server.test.ts | 4 +-- 10 files changed, 80 insertions(+), 44 deletions(-) diff --git a/packages/docusaurus-types/src/context.d.ts b/packages/docusaurus-types/src/context.d.ts index e05f8a9a3256..9ede89e3e6f4 100644 --- a/packages/docusaurus-types/src/context.d.ts +++ b/packages/docusaurus-types/src/context.d.ts @@ -60,3 +60,7 @@ export type Props = LoadContext & { routesPaths: string[]; plugins: LoadedPlugin[]; }; + +export type Site = { + props: Props; +}; diff --git a/packages/docusaurus-types/src/index.d.ts b/packages/docusaurus-types/src/index.d.ts index 257ec57811de..db58944a099c 100644 --- a/packages/docusaurus-types/src/index.d.ts +++ b/packages/docusaurus-types/src/index.d.ts @@ -21,6 +21,7 @@ export { GlobalData, LoadContext, Props, + Site, } from './context'; export {ClientModule} from './clientModule'; diff --git a/packages/docusaurus/src/commands/build.ts b/packages/docusaurus/src/commands/build.ts index fc086a0098de..15bb6b8ee74b 100644 --- a/packages/docusaurus/src/commands/build.ts +++ b/packages/docusaurus/src/commands/build.ts @@ -10,7 +10,7 @@ import path from 'path'; import _ from 'lodash'; import logger from '@docusaurus/logger'; import {DOCUSAURUS_VERSION, mapAsyncSequential} from '@docusaurus/utils'; -import {load, loadContext, type LoadContextOptions} from '../server'; +import {loadSite, loadContext, type LoadContextOptions} from '../server'; import {handleBrokenLinks} from '../server/brokenLinks'; import {createBuildClientConfig} from '../webpack/client'; @@ -161,7 +161,7 @@ async function buildLocale({ logger.info`name=${`[${locale}]`} Creating an optimized production build...`; PerfLogger.start('Loading site'); - const props: Props = await load({ + const site = await loadSite({ siteDir, outDir: cliOptions.outDir, config: cliOptions.config, @@ -170,7 +170,7 @@ async function buildLocale({ }); PerfLogger.end('Loading site'); - // Apply user webpack config. + const {props} = site; const {outDir, plugins} = props; // We can build the 2 configs in parallel diff --git a/packages/docusaurus/src/commands/start/start.ts b/packages/docusaurus/src/commands/start/start.ts index 1e7f5636c3c7..c51ee825d648 100644 --- a/packages/docusaurus/src/commands/start/start.ts +++ b/packages/docusaurus/src/commands/start/start.ts @@ -9,7 +9,7 @@ import fs from 'fs-extra'; import _ from 'lodash'; import logger from '@docusaurus/logger'; import openBrowser from 'react-dev-utils/openBrowser'; -import {load, type LoadContextOptions} from '../../server'; +import {loadSite, type LoadContextOptions} from '../../server'; import {type HostPortOptions} from '../../server/getHostPort'; import {PerfLogger} from '../../utils'; import {setupSiteFileWatchers} from './watcher'; @@ -36,30 +36,32 @@ export async function start( const siteDir = await fs.realpath(siteDirParam); logger.info('Starting the development server...'); - async function loadSite() { + async function doLoadSite() { PerfLogger.start('Loading site'); - const result = await load({ + const site = await loadSite({ siteDir, config: cliOptions.config, locale: cliOptions.locale, localizePath: undefined, // Should this be configurable? }); PerfLogger.end('Loading site'); - return result; + return site; } // Process all related files as a prop. - const props = await loadSite(); + const site = await doLoadSite(); const openUrlContext = await createOpenUrlContext({cliOptions}); - const openUrl = openUrlContext.getOpenUrl({baseUrl: props.baseUrl}); + const openUrl = openUrlContext.getOpenUrl({baseUrl: site.props.baseUrl}); logger.success`Docusaurus website is running at: url=${openUrl}`; const reloadSite = _.debounce(() => { - loadSite() - .then(({baseUrl: newBaseUrl}) => { - const newOpenUrl = openUrlContext.getOpenUrl({baseUrl: newBaseUrl}); + doLoadSite() + .then((newSite) => { + const newOpenUrl = openUrlContext.getOpenUrl({ + baseUrl: newSite.props.baseUrl, + }); if (newOpenUrl !== openUrl) { logger.success`Docusaurus website is running at: url=${newOpenUrl}`; } @@ -70,14 +72,14 @@ export async function start( }, 500); const reloadPlugin = (plugin: LoadedPlugin) => { - console.log('reload plugin', plugin); + console.log(`Reload plugin ${plugin.name}@${plugin.options.id}`); // TODO this is historically not optimized! // When any site file changes, we reload absolutely everything :/ // At least we should try to reload only one plugin individually? reloadSite(); }; - setupSiteFileWatchers({props, cliOptions}, ({plugin}) => { + setupSiteFileWatchers({props: site.props, cliOptions}, ({plugin}) => { if (plugin) { reloadPlugin(plugin); } else { @@ -86,7 +88,7 @@ export async function start( }); const devServer = await createWebpackDevServer({ - props, + props: site.props, cliOptions, openUrlContext, }); @@ -100,6 +102,6 @@ export async function start( await devServer.start(); if (cliOptions.open) { - openBrowser(openUrlContext.getOpenUrl({baseUrl: props.baseUrl})); + openBrowser(openUrlContext.getOpenUrl({baseUrl: site.props.baseUrl})); } } diff --git a/packages/docusaurus/src/server/__tests__/index.test.ts b/packages/docusaurus/src/server/__tests__/index.test.ts index 28bdc3f265e3..235060c3e445 100644 --- a/packages/docusaurus/src/server/__tests__/index.test.ts +++ b/packages/docusaurus/src/server/__tests__/index.test.ts @@ -13,15 +13,15 @@ import type {DeepPartial} from 'utility-types'; describe('load', () => { it('loads props for site with custom i18n path', async () => { - const props = await loadSetup('custom-i18n-site'); - expect(props).toMatchSnapshot(); - const props2 = await loadSetup('custom-i18n-site', {locale: 'zh-Hans'}); - expect(props2).toEqual( + const site = await loadSetup('custom-i18n-site'); + expect(site.props).toMatchSnapshot(); + const site2 = await loadSetup('custom-i18n-site', {locale: 'zh-Hans'}); + expect(site2.props).toEqual( mergeWithCustomize>({ customizeArray(a, b, key) { return ['routesPaths', 'plugins'].includes(key) ? b : undefined; }, - })(props, { + })(site.props, { baseUrl: '/zh-Hans/', i18n: { currentLocale: 'zh-Hans', @@ -38,7 +38,7 @@ describe('load', () => { siteConfig: { baseUrl: '/zh-Hans/', }, - plugins: props2.plugins, + plugins: site2.props.plugins, }), ); }); diff --git a/packages/docusaurus/src/server/__tests__/testUtils.ts b/packages/docusaurus/src/server/__tests__/testUtils.ts index c295f05dad51..65caa6e6c8ec 100644 --- a/packages/docusaurus/src/server/__tests__/testUtils.ts +++ b/packages/docusaurus/src/server/__tests__/testUtils.ts @@ -6,14 +6,14 @@ */ import path from 'path'; -import {load, type LoadContextOptions} from '../index'; -import type {Props} from '@docusaurus/types'; +import {loadSite, type LoadContextOptions} from '../index'; +import type {Site} from '@docusaurus/types'; // Helper methods to setup dummy/fake projects. export async function loadSetup( name: string, options?: Partial, -): Promise { +): Promise { const fixtures = path.join(__dirname, '__fixtures__'); - return load({siteDir: path.join(fixtures, name), ...options}); + return loadSite({siteDir: path.join(fixtures, name), ...options}); } diff --git a/packages/docusaurus/src/server/index.ts b/packages/docusaurus/src/server/index.ts index 42bcff3ab512..d1a4023899a4 100644 --- a/packages/docusaurus/src/server/index.ts +++ b/packages/docusaurus/src/server/index.ts @@ -25,7 +25,12 @@ import { import {PerfLogger} from '../utils'; import {generateSiteFiles} from './codegen/codegen'; import {getRoutesPaths, handleDuplicateRoutes} from './routes'; -import type {DocusaurusConfig, LoadContext, Props} from '@docusaurus/types'; +import type { + DocusaurusConfig, + LoadContext, + Props, + Site, +} from '@docusaurus/types'; export type LoadContextOptions = { /** Usually the CWD; can be overridden with command argument. */ @@ -110,7 +115,7 @@ export async function loadContext( * lifecycles to generate content and other data. It is side-effect-ful because * it generates temp files in the `.docusaurus` folder for the bundler. */ -export async function load(options: LoadContextOptions): Promise { +export async function loadSite(options: LoadContextOptions): Promise { const {siteDir} = options; PerfLogger.start('Load - loadContext'); @@ -165,7 +170,7 @@ export async function load(options: LoadContextOptions): Promise { handleDuplicateRoutes(pluginsRouteConfigs, siteConfig.onDuplicateRoutes); const routesPaths = getRoutesPaths(pluginsRouteConfigs, baseUrl); - return { + const props: Props = { siteConfig, siteConfigPath, siteMetadata, @@ -183,4 +188,8 @@ export async function load(options: LoadContextOptions): Promise { postBodyTags, codeTranslations, }; + + return { + props, + }; } diff --git a/packages/docusaurus/src/server/plugins/index.ts b/packages/docusaurus/src/server/plugins/index.ts index 64a58ed0c2ca..e259f6a7883e 100644 --- a/packages/docusaurus/src/server/plugins/index.ts +++ b/packages/docusaurus/src/server/plugins/index.ts @@ -54,12 +54,9 @@ export async function loadPlugins(context: LoadContext): Promise<{ PerfLogger.start(`Plugins - loadContent`); const loadedPlugins: LoadedPlugin[] = await Promise.all( plugins.map(async (plugin) => { - PerfLogger.start( - `Plugins - loadContent - ${plugin.name}@${plugin.options.id}`, - ); - const content = await plugin.loadContent?.(); - PerfLogger.end( + const content = await PerfLogger.async( `Plugins - loadContent - ${plugin.name}@${plugin.options.id}`, + () => plugin.loadContent?.(), ); const rawTranslationFiles = @@ -109,6 +106,10 @@ export async function loadPlugins(context: LoadContext): Promise<{ if (!plugin.contentLoaded) { return; } + PerfLogger.start( + `Plugins - contentLoaded - ${plugin.name}@${plugin.options.id}`, + ); + const pluginId = plugin.options.id; // Plugins data files are namespaced by pluginName/pluginId const dataDir = path.join( @@ -156,6 +157,9 @@ export async function loadPlugins(context: LoadContext): Promise<{ }; await plugin.contentLoaded({content, actions, allContent}); + PerfLogger.end( + `Plugins - contentLoaded - ${plugin.name}@${plugin.options.id}`, + ); }), ); PerfLogger.end(`Plugins - contentLoaded`); diff --git a/packages/docusaurus/src/webpack/__tests__/client.test.ts b/packages/docusaurus/src/webpack/__tests__/client.test.ts index 19ba00193b69..20b8447fac0f 100644 --- a/packages/docusaurus/src/webpack/__tests__/client.test.ts +++ b/packages/docusaurus/src/webpack/__tests__/client.test.ts @@ -12,26 +12,42 @@ import {loadSetup} from '../../server/__tests__/testUtils'; describe('webpack dev config', () => { it('simple start', async () => { - const props = await loadSetup('simple-site'); - const {clientConfig} = await createStartClientConfig({props}); + const {props} = await loadSetup('simple-site'); + const {clientConfig} = await createStartClientConfig({ + props, + minify: false, + poll: false, + }); webpack.validate(clientConfig); }); it('simple build', async () => { - const props = await loadSetup('simple-site'); - const {config} = await createBuildClientConfig({props}); + const {props} = await loadSetup('simple-site'); + const {config} = await createBuildClientConfig({ + props, + minify: false, + bundleAnalyzer: false, + }); webpack.validate(config); }); it('custom start', async () => { - const props = await loadSetup('custom-site'); - const {clientConfig} = await createStartClientConfig({props}); + const {props} = await loadSetup('custom-site'); + const {clientConfig} = await createStartClientConfig({ + props, + minify: false, + poll: false, + }); webpack.validate(clientConfig); }); it('custom build', async () => { - const props = await loadSetup('custom-site'); - const {config} = await createBuildClientConfig({props}); + const {props} = await loadSetup('custom-site'); + const {config} = await createBuildClientConfig({ + props, + minify: false, + bundleAnalyzer: false, + }); webpack.validate(config); }); }); diff --git a/packages/docusaurus/src/webpack/__tests__/server.test.ts b/packages/docusaurus/src/webpack/__tests__/server.test.ts index 9eec2824fda5..5ef069861d5b 100644 --- a/packages/docusaurus/src/webpack/__tests__/server.test.ts +++ b/packages/docusaurus/src/webpack/__tests__/server.test.ts @@ -14,7 +14,7 @@ import {loadSetup} from '../../server/__tests__/testUtils'; describe('webpack production config', () => { it('simple', async () => { jest.spyOn(console, 'log').mockImplementation(() => {}); - const props = await loadSetup('simple-site'); + const {props} = await loadSetup('simple-site'); const {config} = await createServerConfig({ props, }); @@ -23,7 +23,7 @@ describe('webpack production config', () => { it('custom', async () => { jest.spyOn(console, 'log').mockImplementation(() => {}); - const props = await loadSetup('custom-site'); + const {props} = await loadSetup('custom-site'); const {config} = await createServerConfig({ props, }); From 515b6e0ea19da049f7c920642c51275e9a7d7c84 Mon Sep 17 00:00:00 2001 From: sebastien Date: Fri, 1 Mar 2024 15:26:54 +0100 Subject: [PATCH 07/18] empty From d5758c53c5bed29e0346a338ee266196a73d7dbd Mon Sep 17 00:00:00 2001 From: sebastien Date: Fri, 1 Mar 2024 16:40:06 +0100 Subject: [PATCH 08/18] Make start command site reloadable --- packages/docusaurus-types/src/context.d.ts | 4 - packages/docusaurus-types/src/index.d.ts | 1 - packages/docusaurus/src/commands/build.ts | 4 +- packages/docusaurus/src/commands/deploy.ts | 4 +- packages/docusaurus/src/commands/serve.ts | 4 +- .../docusaurus/src/commands/start/start.ts | 148 +++++++++++------- .../src/commands/writeTranslations.ts | 4 +- .../src/server/__tests__/testUtils.ts | 4 +- packages/docusaurus/src/server/i18n.ts | 4 +- packages/docusaurus/src/server/index.ts | 43 +++-- .../src/server/plugins/__tests__/init.test.ts | 4 +- 11 files changed, 137 insertions(+), 87 deletions(-) diff --git a/packages/docusaurus-types/src/context.d.ts b/packages/docusaurus-types/src/context.d.ts index 9ede89e3e6f4..e05f8a9a3256 100644 --- a/packages/docusaurus-types/src/context.d.ts +++ b/packages/docusaurus-types/src/context.d.ts @@ -60,7 +60,3 @@ export type Props = LoadContext & { routesPaths: string[]; plugins: LoadedPlugin[]; }; - -export type Site = { - props: Props; -}; diff --git a/packages/docusaurus-types/src/index.d.ts b/packages/docusaurus-types/src/index.d.ts index db58944a099c..257ec57811de 100644 --- a/packages/docusaurus-types/src/index.d.ts +++ b/packages/docusaurus-types/src/index.d.ts @@ -21,7 +21,6 @@ export { GlobalData, LoadContext, Props, - Site, } from './context'; export {ClientModule} from './clientModule'; diff --git a/packages/docusaurus/src/commands/build.ts b/packages/docusaurus/src/commands/build.ts index 15bb6b8ee74b..156e669a7e5e 100644 --- a/packages/docusaurus/src/commands/build.ts +++ b/packages/docusaurus/src/commands/build.ts @@ -10,7 +10,7 @@ import path from 'path'; import _ from 'lodash'; import logger from '@docusaurus/logger'; import {DOCUSAURUS_VERSION, mapAsyncSequential} from '@docusaurus/utils'; -import {loadSite, loadContext, type LoadContextOptions} from '../server'; +import {loadSite, loadContext, type LoadContextParams} from '../server'; import {handleBrokenLinks} from '../server/brokenLinks'; import {createBuildClientConfig} from '../webpack/client'; @@ -32,7 +32,7 @@ import type {LoadedPlugin, Props} from '@docusaurus/types'; import type {SiteCollectedData} from '../common'; export type BuildCLIOptions = Pick< - LoadContextOptions, + LoadContextParams, 'config' | 'locale' | 'outDir' > & { bundleAnalyzer?: boolean; diff --git a/packages/docusaurus/src/commands/deploy.ts b/packages/docusaurus/src/commands/deploy.ts index 9904b8585ea9..3d1b46132dcb 100644 --- a/packages/docusaurus/src/commands/deploy.ts +++ b/packages/docusaurus/src/commands/deploy.ts @@ -11,11 +11,11 @@ import os from 'os'; import logger from '@docusaurus/logger'; import shell from 'shelljs'; import {hasSSHProtocol, buildSshUrl, buildHttpsUrl} from '@docusaurus/utils'; -import {loadContext, type LoadContextOptions} from '../server'; +import {loadContext, type LoadContextParams} from '../server'; import {build} from './build'; export type DeployCLIOptions = Pick< - LoadContextOptions, + LoadContextParams, 'config' | 'locale' | 'outDir' > & { skipBuild?: boolean; diff --git a/packages/docusaurus/src/commands/serve.ts b/packages/docusaurus/src/commands/serve.ts index f41acd1245bd..1253528961a3 100644 --- a/packages/docusaurus/src/commands/serve.ts +++ b/packages/docusaurus/src/commands/serve.ts @@ -15,10 +15,10 @@ import openBrowser from 'react-dev-utils/openBrowser'; import {loadSiteConfig} from '../server/config'; import {build} from './build'; import {getHostPort, type HostPortOptions} from '../server/getHostPort'; -import type {LoadContextOptions} from '../server'; +import type {LoadContextParams} from '../server'; export type ServeCLIOptions = HostPortOptions & - Pick & { + Pick & { dir?: string; build?: boolean; open?: boolean; diff --git a/packages/docusaurus/src/commands/start/start.ts b/packages/docusaurus/src/commands/start/start.ts index c51ee825d648..e4ef23578683 100644 --- a/packages/docusaurus/src/commands/start/start.ts +++ b/packages/docusaurus/src/commands/start/start.ts @@ -9,88 +9,120 @@ import fs from 'fs-extra'; import _ from 'lodash'; import logger from '@docusaurus/logger'; import openBrowser from 'react-dev-utils/openBrowser'; -import {loadSite, type LoadContextOptions} from '../../server'; -import {type HostPortOptions} from '../../server/getHostPort'; +import {loadSite, reloadSite, reloadSitePlugin} from '../../server'; import {PerfLogger} from '../../utils'; import {setupSiteFileWatchers} from './watcher'; import {createWebpackDevServer} from './webpack'; import {createOpenUrlContext} from './utils'; +import type {LoadContextParams, LoadSiteParams} from '../../server'; +import type {HostPortOptions} from '../../server/getHostPort'; import type {LoadedPlugin} from '@docusaurus/types'; export type StartCLIOptions = HostPortOptions & - Pick & { + Pick & { hotOnly?: boolean; open?: boolean; poll?: boolean | number; minify?: boolean; }; +async function createLoadSiteParams({ + siteDirParam, + cliOptions, +}: StartParams): Promise { + const siteDir = await fs.realpath(siteDirParam); + return { + siteDir, + config: cliOptions.config, + locale: cliOptions.locale, + localizePath: undefined, // Should this be configurable? + }; +} + +async function createReloadableSite(startParams: StartParams) { + const openUrlContext = await createOpenUrlContext(startParams); + + let site = await PerfLogger.async('Loading site', async () => { + const params = await createLoadSiteParams(startParams); + return loadSite(params); + }); + + const get = () => site; + + const getOpenUrl = () => + openUrlContext.getOpenUrl({ + baseUrl: site.props.baseUrl, + }); + + const printOpenUrlMessage = () => { + logger.success`Docusaurus website is running at: url=${getOpenUrl()}`; + }; + printOpenUrlMessage(); + + const reloadBase = async () => { + try { + const oldSite = site; + site = await PerfLogger.async('Reloading site', () => reloadSite(site)); + if (oldSite.props.baseUrl !== site.props.baseUrl) { + printOpenUrlMessage(); + } + } catch (e) { + logger.error('Site reload failure'); + console.error(e); + } + }; + + // TODO instead of debouncing we should rather add AbortController support + const reload = _.debounce(reloadBase, 500); + + const reloadPlugin = async (plugin: LoadedPlugin) => { + try { + site = await PerfLogger.async( + `Reloading site plugin ${plugin.name}@${plugin.options.id}`, + () => reloadSitePlugin(site, plugin), + ); + } catch (e) { + logger.error( + `Site plugin reload failure - Plugin ${plugin.name}@${plugin.options.id}`, + ); + console.error(e); + } + }; + + return {get, getOpenUrl, reload, reloadPlugin, openUrlContext}; +} + +type StartParams = { + siteDirParam: string; + cliOptions: Partial; +}; + export async function start( siteDirParam: string = '.', cliOptions: Partial = {}, ): Promise { + logger.info('Starting the development server...'); // Temporary workaround to unlock the ability to translate the site config // We'll remove it if a better official API can be designed // See https://github.com/facebook/docusaurus/issues/4542 process.env.DOCUSAURUS_CURRENT_LOCALE = cliOptions.locale; - const siteDir = await fs.realpath(siteDirParam); - logger.info('Starting the development server...'); - - async function doLoadSite() { - PerfLogger.start('Loading site'); - const site = await loadSite({ - siteDir, - config: cliOptions.config, - locale: cliOptions.locale, - localizePath: undefined, // Should this be configurable? - }); - PerfLogger.end('Loading site'); - return site; - } - // Process all related files as a prop. - const site = await doLoadSite(); - - const openUrlContext = await createOpenUrlContext({cliOptions}); - const openUrl = openUrlContext.getOpenUrl({baseUrl: site.props.baseUrl}); - - logger.success`Docusaurus website is running at: url=${openUrl}`; - - const reloadSite = _.debounce(() => { - doLoadSite() - .then((newSite) => { - const newOpenUrl = openUrlContext.getOpenUrl({ - baseUrl: newSite.props.baseUrl, - }); - if (newOpenUrl !== openUrl) { - logger.success`Docusaurus website is running at: url=${newOpenUrl}`; - } - }) - .catch((err: Error) => { - logger.error(err.stack); - }); - }, 500); - - const reloadPlugin = (plugin: LoadedPlugin) => { - console.log(`Reload plugin ${plugin.name}@${plugin.options.id}`); - // TODO this is historically not optimized! - // When any site file changes, we reload absolutely everything :/ - // At least we should try to reload only one plugin individually? - reloadSite(); - }; - - setupSiteFileWatchers({props: site.props, cliOptions}, ({plugin}) => { - if (plugin) { - reloadPlugin(plugin); - } else { - reloadSite(); - } - }); + const reloadableSite = await createReloadableSite({siteDirParam, cliOptions}); + setupSiteFileWatchers( + {props: reloadableSite.get().props, cliOptions}, + ({plugin}) => { + if (plugin) { + reloadableSite.reloadPlugin(plugin); + } else { + reloadableSite.reload(); + } + }, + ); const devServer = await createWebpackDevServer({ - props: site.props, + props: reloadableSite.get().props, cliOptions, - openUrlContext, + openUrlContext: reloadableSite.openUrlContext, }); ['SIGINT', 'SIGTERM'].forEach((sig) => { @@ -102,6 +134,6 @@ export async function start( await devServer.start(); if (cliOptions.open) { - openBrowser(openUrlContext.getOpenUrl({baseUrl: site.props.baseUrl})); + openBrowser(reloadableSite.getOpenUrl()); } } diff --git a/packages/docusaurus/src/commands/writeTranslations.ts b/packages/docusaurus/src/commands/writeTranslations.ts index 6247f04902dc..c58465c41df0 100644 --- a/packages/docusaurus/src/commands/writeTranslations.ts +++ b/packages/docusaurus/src/commands/writeTranslations.ts @@ -7,7 +7,7 @@ import fs from 'fs-extra'; import path from 'path'; -import {loadContext, type LoadContextOptions} from '../server'; +import {loadContext, type LoadContextParams} from '../server'; import {initPlugins} from '../server/plugins/init'; import { writePluginTranslations, @@ -24,7 +24,7 @@ import {getCustomBabelConfigFilePath, getBabelOptions} from '../webpack/utils'; import type {InitializedPlugin} from '@docusaurus/types'; export type WriteTranslationsCLIOptions = Pick< - LoadContextOptions, + LoadContextParams, 'config' | 'locale' > & WriteTranslationsOptions; diff --git a/packages/docusaurus/src/server/__tests__/testUtils.ts b/packages/docusaurus/src/server/__tests__/testUtils.ts index 65caa6e6c8ec..4491e4b5d097 100644 --- a/packages/docusaurus/src/server/__tests__/testUtils.ts +++ b/packages/docusaurus/src/server/__tests__/testUtils.ts @@ -6,13 +6,13 @@ */ import path from 'path'; -import {loadSite, type LoadContextOptions} from '../index'; +import {loadSite, type LoadContextParams} from '../index'; import type {Site} from '@docusaurus/types'; // Helper methods to setup dummy/fake projects. export async function loadSetup( name: string, - options?: Partial, + options?: Partial, ): Promise { const fixtures = path.join(__dirname, '__fixtures__'); return loadSite({siteDir: path.join(fixtures, name), ...options}); diff --git a/packages/docusaurus/src/server/i18n.ts b/packages/docusaurus/src/server/i18n.ts index 21f1b5ebd3b6..dc54c6d54e06 100644 --- a/packages/docusaurus/src/server/i18n.ts +++ b/packages/docusaurus/src/server/i18n.ts @@ -8,7 +8,7 @@ import logger from '@docusaurus/logger'; import {getLangDir} from 'rtl-detect'; import type {I18n, DocusaurusConfig, I18nLocaleConfig} from '@docusaurus/types'; -import type {LoadContextOptions} from './index'; +import type {LoadContextParams} from './index'; function getDefaultLocaleLabel(locale: string) { const languageName = new Intl.DisplayNames(locale, {type: 'language'}).of( @@ -55,7 +55,7 @@ export function getDefaultLocaleConfig(locale: string): I18nLocaleConfig { export async function loadI18n( config: DocusaurusConfig, - options: Pick, + options: Pick, ): Promise { const {i18n: i18nConfig} = config; diff --git a/packages/docusaurus/src/server/index.ts b/packages/docusaurus/src/server/index.ts index d1a4023899a4..39e8986cb44f 100644 --- a/packages/docusaurus/src/server/index.ts +++ b/packages/docusaurus/src/server/index.ts @@ -28,11 +28,11 @@ import {getRoutesPaths, handleDuplicateRoutes} from './routes'; import type { DocusaurusConfig, LoadContext, + LoadedPlugin, Props, - Site, } from '@docusaurus/types'; -export type LoadContextOptions = { +export type LoadContextParams = { /** Usually the CWD; can be overridden with command argument. */ siteDir: string; /** Custom output directory. Can be customized with `--out-dir` option */ @@ -50,21 +50,28 @@ export type LoadContextOptions = { localizePath?: boolean; }; +export type LoadSiteParams = LoadContextParams; + +export type Site = { + props: Props; + params: LoadSiteParams; +}; + /** - * Loading context is the very first step in site building. Its options are + * Loading context is the very first step in site building. Its params are * directly acquired from CLI options. It mainly loads `siteConfig` and the i18n * context (which includes code translations). The `LoadContext` will be passed * to plugin constructors. */ export async function loadContext( - options: LoadContextOptions, + params: LoadContextParams, ): Promise { const { siteDir, outDir: baseOutDir = DEFAULT_BUILD_DIR_NAME, locale, config: customConfigFilePath, - } = options; + } = params; const generatedFilesDir = path.resolve(siteDir, GENERATED_FILES_DIR_NAME); const {siteConfig: initialSiteConfig, siteConfigPath} = await loadSiteConfig({ @@ -77,13 +84,13 @@ export async function loadContext( const baseUrl = localizePath({ path: initialSiteConfig.baseUrl, i18n, - options, + options: params, pathType: 'url', }); const outDir = localizePath({ path: path.resolve(siteDir, baseOutDir), i18n, - options, + options: params, pathType: 'fs', }); const localizationDir = path.resolve( @@ -115,11 +122,11 @@ export async function loadContext( * lifecycles to generate content and other data. It is side-effect-ful because * it generates temp files in the `.docusaurus` folder for the bundler. */ -export async function loadSite(options: LoadContextOptions): Promise { - const {siteDir} = options; +export async function loadSite(params: LoadContextParams): Promise { + const {siteDir} = params; PerfLogger.start('Load - loadContext'); - const context = await loadContext(options); + const context = await loadContext(params); PerfLogger.end('Load - loadContext'); const { @@ -191,5 +198,21 @@ export async function loadSite(options: LoadContextOptions): Promise { return { props, + params, }; } + +export async function reloadSite(site: Site): Promise { + // TODO this can be optimized, for example: + // - plugins loading same data as before should not recreate routes/bundles + // - codegen does not need to re-run if nothing changed + return loadSite(site.params); +} + +export async function reloadSitePlugin( + site: Site, + plugin: LoadedPlugin, +): Promise { + console.log(`reloadSitePlugin ${plugin.name}`); + return loadSite(site.params); +} diff --git a/packages/docusaurus/src/server/plugins/__tests__/init.test.ts b/packages/docusaurus/src/server/plugins/__tests__/init.test.ts index 4e07fb686def..7ef1f5e3e41e 100644 --- a/packages/docusaurus/src/server/plugins/__tests__/init.test.ts +++ b/packages/docusaurus/src/server/plugins/__tests__/init.test.ts @@ -7,11 +7,11 @@ import path from 'path'; -import {loadContext, type LoadContextOptions} from '../../index'; +import {loadContext, type LoadContextParams} from '../../index'; import {initPlugins} from '../init'; describe('initPlugins', () => { - async function loadSite(options: Omit = {}) { + async function loadSite(options: Omit = {}) { const siteDir = path.join(__dirname, '__fixtures__', 'site-with-plugin'); const context = await loadContext({...options, siteDir}); const plugins = await initPlugins(context); From a14b4f7dff719411d785a66dcb89a5bdcb8e0def Mon Sep 17 00:00:00 2001 From: sebastien Date: Fri, 1 Mar 2024 16:42:22 +0100 Subject: [PATCH 09/18] delete old snapshot --- .../__tests__/__snapshots__/codegenRoutes.test.ts.snap | 9 --------- 1 file changed, 9 deletions(-) diff --git a/packages/docusaurus/src/server/codegen/__tests__/__snapshots__/codegenRoutes.test.ts.snap b/packages/docusaurus/src/server/codegen/__tests__/__snapshots__/codegenRoutes.test.ts.snap index 80644fa961ae..2a6e4784b523 100644 --- a/packages/docusaurus/src/server/codegen/__tests__/__snapshots__/codegenRoutes.test.ts.snap +++ b/packages/docusaurus/src/server/codegen/__tests__/__snapshots__/codegenRoutes.test.ts.snap @@ -1,14 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`handleDuplicateRoutes works 1`] = ` -"Duplicate routes found! -- Attempting to create page at /search, but a page already exists at this route. -- Attempting to create page at /sameDoc, but a page already exists at this route. -- Attempting to create page at /, but a page already exists at this route. -- Attempting to create page at /, but a page already exists at this route. -This could lead to non-deterministic routing behavior." -`; - exports[`loadRoutes loads flat route config 1`] = ` { "registry": { From c26e0bcbeab49905c848aada32dd0ec3417bbb74 Mon Sep 17 00:00:00 2001 From: slorber Date: Fri, 1 Mar 2024 15:51:47 +0000 Subject: [PATCH 10/18] refactor: apply lint autofix --- project-words.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/project-words.txt b/project-words.txt index bc12986f72ae..0923fb73c5e3 100644 --- a/project-words.txt +++ b/project-words.txt @@ -285,6 +285,8 @@ redwoodjs refactorings Rehype rehype +Reloadable +reloadable renderable REPONAME Retrocompatibility From b26596e5e55554b427b25966c6748b15cff0bb3f Mon Sep 17 00:00:00 2001 From: sebastien Date: Fri, 1 Mar 2024 17:30:10 +0100 Subject: [PATCH 11/18] stable refactor of plugin lifecycles execution --- .../src/__tests__/index.test.ts | 4 +- .../__snapshots__/routeConfig.test.ts.snap | 152 +++++++++- .../plugins/__tests__/routeConfig.test.ts | 10 +- .../docusaurus/src/server/plugins/index.ts | 281 +++++++++++------- .../src/server/plugins/routeConfig.ts | 4 +- 5 files changed, 333 insertions(+), 118 deletions(-) diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/index.test.ts b/packages/docusaurus-plugin-content-docs/src/__tests__/index.test.ts index bbff5e555353..dd1bce5e05f6 100644 --- a/packages/docusaurus-plugin-content-docs/src/__tests__/index.test.ts +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/index.test.ts @@ -14,7 +14,7 @@ import commander from 'commander'; import webpack from 'webpack'; import {loadContext} from '@docusaurus/core/src/server/index'; import {applyConfigureWebpack} from '@docusaurus/core/src/webpack/utils'; -import {sortConfig} from '@docusaurus/core/src/server/plugins/routeConfig'; +import {sortRoutes} from '@docusaurus/core/src/server/plugins/routeConfig'; import {posixPath} from '@docusaurus/utils'; import {normalizePluginOptions} from '@docusaurus/utils-validation'; @@ -109,7 +109,7 @@ Entries created: expectSnapshot: () => { // Sort the route config like in src/server/plugins/index.ts for // consistent snapshot ordering - sortConfig(routeConfigs); + sortRoutes(routeConfigs); expect(routeConfigs).not.toEqual([]); expect(routeConfigs).toMatchSnapshot('route config'); expect(dataContainer).toMatchSnapshot('data'); diff --git a/packages/docusaurus/src/server/plugins/__tests__/__snapshots__/routeConfig.test.ts.snap b/packages/docusaurus/src/server/plugins/__tests__/__snapshots__/routeConfig.test.ts.snap index 9d4fb06e800a..69eb51472e60 100644 --- a/packages/docusaurus/src/server/plugins/__tests__/__snapshots__/routeConfig.test.ts.snap +++ b/packages/docusaurus/src/server/plugins/__tests__/__snapshots__/routeConfig.test.ts.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`sortConfig sorts route config correctly 1`] = ` +exports[`sortRoutes sorts route config correctly 1`] = ` [ { "component": "", @@ -55,7 +55,7 @@ exports[`sortConfig sorts route config correctly 1`] = ` ] `; -exports[`sortConfig sorts route config given a baseURL 1`] = ` +exports[`sortRoutes sorts route config given a baseURL 1`] = ` [ { "component": "", @@ -104,7 +104,153 @@ exports[`sortConfig sorts route config given a baseURL 1`] = ` ] `; -exports[`sortConfig sorts route config recursively 1`] = ` +exports[`sortRoutes sorts route config recursively 1`] = ` +[ + { + "component": "", + "exact": true, + "path": "/some/page", + }, + { + "component": "", + "path": "/docs", + "routes": [ + { + "component": "", + "exact": true, + "path": "/docs/tags", + }, + { + "component": "", + "exact": true, + "path": "/docs/tags/someTag", + }, + { + "component": "", + "path": "/docs", + "routes": [ + { + "component": "", + "exact": true, + "path": "/docs/doc1", + }, + { + "component": "", + "exact": true, + "path": "/docs/doc2", + }, + ], + }, + ], + }, +] +`; + +exports[`sortRoutes sorts route config correctly 1`] = ` +[ + { + "component": "", + "path": "/community", + }, + { + "component": "", + "path": "/some-page", + }, + { + "component": "", + "path": "/docs", + "routes": [ + { + "component": "", + "path": "/docs/someDoc", + }, + { + "component": "", + "path": "/docs/someOtherDoc", + }, + ], + }, + { + "component": "", + "path": "/", + }, + { + "component": "", + "path": "/", + "routes": [ + { + "component": "", + "path": "/someDoc", + }, + { + "component": "", + "path": "/someOtherDoc", + }, + ], + }, + { + "component": "", + "path": "/", + "routes": [ + { + "component": "", + "path": "/subroute", + }, + ], + }, +] +`; + +exports[`sortRoutes sorts route config given a baseURL 1`] = ` +[ + { + "component": "", + "path": "/latest/community", + }, + { + "component": "", + "path": "/latest/example", + }, + { + "component": "", + "path": "/latest/some-page", + }, + { + "component": "", + "path": "/latest/docs", + "routes": [ + { + "component": "", + "path": "/latest/docs/someDoc", + }, + { + "component": "", + "path": "/latest/docs/someOtherDoc", + }, + ], + }, + { + "component": "", + "path": "/latest/", + }, + { + "component": "", + "path": "/latest/", + "routes": [ + { + "component": "", + "path": "/latest/someDoc", + }, + { + "component": "", + "path": "/latest/someOtherDoc", + }, + ], + }, +] +`; + +exports[`sortRoutes sorts route config recursively 1`] = ` [ { "component": "", diff --git a/packages/docusaurus/src/server/plugins/__tests__/routeConfig.test.ts b/packages/docusaurus/src/server/plugins/__tests__/routeConfig.test.ts index 64c8034eb520..da4345add7bb 100644 --- a/packages/docusaurus/src/server/plugins/__tests__/routeConfig.test.ts +++ b/packages/docusaurus/src/server/plugins/__tests__/routeConfig.test.ts @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -import {applyRouteTrailingSlash, sortConfig} from '../routeConfig'; +import {applyRouteTrailingSlash, sortRoutes} from '../routeConfig'; import type {RouteConfig} from '@docusaurus/types'; import type {ApplyTrailingSlashParams} from '@docusaurus/utils-common'; @@ -164,7 +164,7 @@ describe('applyRouteTrailingSlash', () => { }); }); -describe('sortConfig', () => { +describe('sortRoutes', () => { it('sorts route config correctly', () => { const routes: RouteConfig[] = [ { @@ -202,7 +202,7 @@ describe('sortConfig', () => { }, ]; - sortConfig(routes); + sortRoutes(routes); expect(routes).toMatchSnapshot(); }); @@ -248,7 +248,7 @@ describe('sortConfig', () => { }, ]; - sortConfig(routes); + sortRoutes(routes); expect(routes).toMatchSnapshot(); }); @@ -290,7 +290,7 @@ describe('sortConfig', () => { }, ]; - sortConfig(routes, baseURL); + sortRoutes(routes, baseURL); expect(routes).toMatchSnapshot(); }); diff --git a/packages/docusaurus/src/server/plugins/index.ts b/packages/docusaurus/src/server/plugins/index.ts index e259f6a7883e..38bc6d690af1 100644 --- a/packages/docusaurus/src/server/plugins/index.ts +++ b/packages/docusaurus/src/server/plugins/index.ts @@ -11,7 +11,7 @@ import {docuHash, generate} from '@docusaurus/utils'; import {initPlugins} from './init'; import {createBootstrapPlugin, createMDXFallbackPlugin} from './synthetic'; import {localizePluginTranslationFile} from '../translations/translations'; -import {applyRouteTrailingSlash, sortConfig} from './routeConfig'; +import {applyRouteTrailingSlash, sortRoutes} from './routeConfig'; import {PerfLogger} from '../../utils'; import type { LoadContext, @@ -24,6 +24,168 @@ import type { PluginRouteContext, } from '@docusaurus/types'; +async function loadPlugin({ + plugin, + context, +}: { + plugin: InitializedPlugin; + context: LoadContext; +}): Promise { + const content = await PerfLogger.async( + `Plugins - loadContent - ${plugin.name}@${plugin.options.id}`, + () => plugin.loadContent?.(), + ); + + const rawTranslationFiles = + (await plugin.getTranslationFiles?.({content})) ?? []; + const translationFiles = await Promise.all( + rawTranslationFiles.map((translationFile) => + localizePluginTranslationFile({ + localizationDir: context.localizationDir, + translationFile, + plugin, + }), + ), + ); + + const translatedContent = + plugin.translateContent?.({content, translationFiles}) ?? content; + const translatedThemeConfigSlice = plugin.translateThemeConfig?.({ + themeConfig: context.siteConfig.themeConfig, + translationFiles, + }); + + // Side-effect to merge theme config translations. A plugin should only + // translate its own slice of theme config and should make no assumptions + // about other plugins' keys, so this is safe to run in parallel. + Object.assign(context.siteConfig.themeConfig, translatedThemeConfigSlice); + return {...plugin, content: translatedContent}; +} + +function aggregateAllContent(loadedPlugins: LoadedPlugin[]): AllContent { + return _.chain(loadedPlugins) + .groupBy((item) => item.name) + .mapValues((nameItems) => + _.chain(nameItems) + .groupBy((item) => item.options.id) + .mapValues((idItems) => idItems[0]!.content) + .value(), + ) + .value(); +} + +// TODO refactor and make this side-effect-free +// If the function was pure, we could more easily compare previous/next values +// on site reloads, and bail-out of the reload process earlier +// createData() modules should rather be declarative +async function executePluginContentLoaded({ + plugin, + context, + allContent, +}: { + plugin: LoadedPlugin; + context: LoadContext; + allContent: AllContent; +}): Promise<{routes: RouteConfig[]; globalData: unknown}> { + if (!plugin.contentLoaded) { + return {routes: [], globalData: undefined}; + } + PerfLogger.start( + `Plugins - contentLoaded - ${plugin.name}@${plugin.options.id}`, + ); + + const pluginId = plugin.options.id; + // Plugins data files are namespaced by pluginName/pluginId + const dataDir = path.join(context.generatedFilesDir, plugin.name, pluginId); + const pluginRouteContextModulePath = path.join( + dataDir, + `${docuHash('pluginRouteContextModule')}.json`, + ); + const pluginRouteContext: PluginRouteContext['plugin'] = { + name: plugin.name, + id: pluginId, + }; + await generate( + '/', + pluginRouteContextModulePath, + JSON.stringify(pluginRouteContext, null, 2), + ); + + const routes: RouteConfig[] = []; + let globalData: unknown; + + const actions: PluginContentLoadedActions = { + addRoute(initialRouteConfig) { + // Trailing slash behavior is handled generically for all plugins + const finalRouteConfig = applyRouteTrailingSlash( + initialRouteConfig, + context.siteConfig, + ); + routes.push({ + ...finalRouteConfig, + context: { + ...(finalRouteConfig.context && {data: finalRouteConfig.context}), + plugin: pluginRouteContextModulePath, + }, + }); + }, + async createData(name, data) { + const modulePath = path.join(dataDir, name); + await generate(dataDir, name, data); + return modulePath; + }, + setGlobalData(data) { + globalData = data; + }, + }; + + await plugin.contentLoaded({content: plugin.content, actions, allContent}); + PerfLogger.end( + `Plugins - contentLoaded - ${plugin.name}@${plugin.options.id}`, + ); + + return {routes, globalData}; +} + +async function executePluginsContentLoaded({ + plugins, + context, + allContent, +}: { + plugins: LoadedPlugin[]; + context: LoadContext; + // TODO AllContent was injected to this lifecycle for the debug plugin + // this was likely a bad idea and prevent to start executing contentLoaded() + // until all plugins have finished loading all the data + // we'd rather remove this and find another way to implement the debug plugin + // A possible solution: make it a core feature instead of a plugin? + allContent: AllContent; +}): Promise<{routes: RouteConfig[]; globalData: GlobalData}> { + PerfLogger.start(`Plugins - contentLoaded`); + + const allRoutes: RouteConfig[] = []; + const allGlobalData: GlobalData = {}; + + await Promise.all( + plugins.map(async (plugin) => { + const {routes, globalData} = await executePluginContentLoaded({ + plugin, + context, + allContent, + }); + + allRoutes.push(...routes); + if (globalData !== undefined) { + allGlobalData[plugin.name] ??= {}; + allGlobalData[plugin.name]![plugin.options.id] = globalData; + } + }), + ); + PerfLogger.end(`Plugins - contentLoaded`); + + return {routes: allRoutes, globalData: allGlobalData}; +} + /** * Initializes the plugins, runs `loadContent`, `translateContent`, * `contentLoaded`, and `translateThemeConfig`. Because `contentLoaded` is @@ -53,120 +215,27 @@ export async function loadPlugins(context: LoadContext): Promise<{ // translation files that the plugin declares. PerfLogger.start(`Plugins - loadContent`); const loadedPlugins: LoadedPlugin[] = await Promise.all( - plugins.map(async (plugin) => { - const content = await PerfLogger.async( - `Plugins - loadContent - ${plugin.name}@${plugin.options.id}`, - () => plugin.loadContent?.(), - ); - - const rawTranslationFiles = - (await plugin.getTranslationFiles?.({content})) ?? []; - const translationFiles = await Promise.all( - rawTranslationFiles.map((translationFile) => - localizePluginTranslationFile({ - localizationDir: context.localizationDir, - translationFile, - plugin, - }), - ), - ); - - const translatedContent = - plugin.translateContent?.({content, translationFiles}) ?? content; - const translatedThemeConfigSlice = plugin.translateThemeConfig?.({ - themeConfig: context.siteConfig.themeConfig, - translationFiles, - }); - // Side-effect to merge theme config translations. A plugin should only - // translate its own slice of theme config and should make no assumptions - // about other plugins' keys, so this is safe to run in parallel. - Object.assign(context.siteConfig.themeConfig, translatedThemeConfigSlice); - return {...plugin, content: translatedContent}; - }), + plugins.map((plugin) => loadPlugin({plugin, context})), ); PerfLogger.end(`Plugins - loadContent`); - const allContent: AllContent = _.chain(loadedPlugins) - .groupBy((item) => item.name) - .mapValues((nameItems) => - _.chain(nameItems) - .groupBy((item) => item.options.id) - .mapValues((idItems) => idItems[0]!.content) - .value(), - ) - .value(); + const allContent = aggregateAllContent(loadedPlugins); // 3. Plugin Lifecycle - contentLoaded. - const pluginsRouteConfigs: RouteConfig[] = []; - const globalData: GlobalData = {}; - - PerfLogger.start(`Plugins - contentLoaded`); - await Promise.all( - loadedPlugins.map(async ({content, ...plugin}) => { - if (!plugin.contentLoaded) { - return; - } - PerfLogger.start( - `Plugins - contentLoaded - ${plugin.name}@${plugin.options.id}`, - ); - - const pluginId = plugin.options.id; - // Plugins data files are namespaced by pluginName/pluginId - const dataDir = path.join( - context.generatedFilesDir, - plugin.name, - pluginId, - ); - const pluginRouteContextModulePath = path.join( - dataDir, - `${docuHash('pluginRouteContextModule')}.json`, - ); - const pluginRouteContext: PluginRouteContext['plugin'] = { - name: plugin.name, - id: pluginId, - }; - await generate( - '/', - pluginRouteContextModulePath, - JSON.stringify(pluginRouteContext, null, 2), - ); - const actions: PluginContentLoadedActions = { - addRoute(initialRouteConfig) { - // Trailing slash behavior is handled generically for all plugins - const finalRouteConfig = applyRouteTrailingSlash( - initialRouteConfig, - context.siteConfig, - ); - pluginsRouteConfigs.push({ - ...finalRouteConfig, - context: { - ...(finalRouteConfig.context && {data: finalRouteConfig.context}), - plugin: pluginRouteContextModulePath, - }, - }); - }, - async createData(name, data) { - const modulePath = path.join(dataDir, name); - await generate(dataDir, name, data); - return modulePath; - }, - setGlobalData(data) { - globalData[plugin.name] ??= {}; - globalData[plugin.name]![pluginId] = data; - }, - }; - - await plugin.contentLoaded({content, actions, allContent}); - PerfLogger.end( - `Plugins - contentLoaded - ${plugin.name}@${plugin.options.id}`, - ); - }), + const {routes, globalData} = await PerfLogger.async( + 'Plugins - contentLoaded', + () => + executePluginsContentLoaded({ + plugins: loadedPlugins, + context, + allContent, + }), ); PerfLogger.end(`Plugins - contentLoaded`); // Sort the route config. This ensures that route with nested // routes are always placed last. - sortConfig(pluginsRouteConfigs, context.siteConfig.baseUrl); + sortRoutes(routes, context.siteConfig.baseUrl); - return {plugins: loadedPlugins, pluginsRouteConfigs, globalData}; + return {plugins: loadedPlugins, pluginsRouteConfigs: routes, globalData}; } diff --git a/packages/docusaurus/src/server/plugins/routeConfig.ts b/packages/docusaurus/src/server/plugins/routeConfig.ts index d757406bd09b..cd824f1f4ba6 100644 --- a/packages/docusaurus/src/server/plugins/routeConfig.ts +++ b/packages/docusaurus/src/server/plugins/routeConfig.ts @@ -27,7 +27,7 @@ export function applyRouteTrailingSlash( }; } -export function sortConfig( +export function sortRoutes( routeConfigs: RouteConfig[], baseUrl: string = '/', ): void { @@ -64,7 +64,7 @@ export function sortConfig( routeConfigs.forEach((routeConfig) => { if (routeConfig.routes) { - sortConfig(routeConfig.routes, baseUrl); + sortRoutes(routeConfig.routes, baseUrl); } }); } From e068ad6c9bdb0a21100139e7717bcd56e26a8847 Mon Sep 17 00:00:00 2001 From: sebastien Date: Fri, 1 Mar 2024 17:33:59 +0100 Subject: [PATCH 12/18] rename weird pluginsRouteConfigs name --- packages/docusaurus/src/server/index.ts | 10 +++++----- .../plugins/__tests__/__snapshots__/index.test.ts.snap | 2 +- packages/docusaurus/src/server/plugins/index.ts | 4 ++-- packages/docusaurus/src/server/routes.ts | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/docusaurus/src/server/index.ts b/packages/docusaurus/src/server/index.ts index 39e8986cb44f..b0ebb48a0414 100644 --- a/packages/docusaurus/src/server/index.ts +++ b/packages/docusaurus/src/server/index.ts @@ -141,7 +141,7 @@ export async function loadSite(params: LoadContextParams): Promise { } = context; PerfLogger.start('Load - loadPlugins'); - const {plugins, pluginsRouteConfigs, globalData} = await loadPlugins(context); + const {plugins, routes, globalData} = await loadPlugins(context); PerfLogger.end('Load - loadPlugins'); const {headTags, preBodyTags, postBodyTags} = loadHtmlTags(plugins); @@ -169,13 +169,13 @@ export async function loadSite(params: LoadContextParams): Promise { i18n, codeTranslations, globalData, - routeConfigs: pluginsRouteConfigs, + routeConfigs: routes, baseUrl, }); PerfLogger.end('Load - generateSiteCode'); - handleDuplicateRoutes(pluginsRouteConfigs, siteConfig.onDuplicateRoutes); - const routesPaths = getRoutesPaths(pluginsRouteConfigs, baseUrl); + handleDuplicateRoutes(routes, siteConfig.onDuplicateRoutes); + const routesPaths = getRoutesPaths(routes, baseUrl); const props: Props = { siteConfig, @@ -187,7 +187,7 @@ export async function loadSite(params: LoadContextParams): Promise { i18n, localizationDir, generatedFilesDir, - routes: pluginsRouteConfigs, + routes, routesPaths, plugins, headTags, diff --git a/packages/docusaurus/src/server/plugins/__tests__/__snapshots__/index.test.ts.snap b/packages/docusaurus/src/server/plugins/__tests__/__snapshots__/index.test.ts.snap index 76e6a9bee903..47295e904880 100644 --- a/packages/docusaurus/src/server/plugins/__tests__/__snapshots__/index.test.ts.snap +++ b/packages/docusaurus/src/server/plugins/__tests__/__snapshots__/index.test.ts.snap @@ -63,7 +63,7 @@ exports[`loadPlugins loads plugins 1`] = ` }, }, ], - "pluginsRouteConfigs": [ + "routes": [ { "component": "Comp", "context": { diff --git a/packages/docusaurus/src/server/plugins/index.ts b/packages/docusaurus/src/server/plugins/index.ts index 38bc6d690af1..ee4ed188063d 100644 --- a/packages/docusaurus/src/server/plugins/index.ts +++ b/packages/docusaurus/src/server/plugins/index.ts @@ -194,7 +194,7 @@ async function executePluginsContentLoaded({ */ export async function loadPlugins(context: LoadContext): Promise<{ plugins: LoadedPlugin[]; - pluginsRouteConfigs: RouteConfig[]; + routes: RouteConfig[]; globalData: GlobalData; }> { // 1. Plugin Lifecycle - Initialization/Constructor. @@ -237,5 +237,5 @@ export async function loadPlugins(context: LoadContext): Promise<{ // routes are always placed last. sortRoutes(routes, context.siteConfig.baseUrl); - return {plugins: loadedPlugins, pluginsRouteConfigs: routes, globalData}; + return {plugins: loadedPlugins, routes, globalData}; } diff --git a/packages/docusaurus/src/server/routes.ts b/packages/docusaurus/src/server/routes.ts index f4d8d670a0b9..95621d34dab3 100644 --- a/packages/docusaurus/src/server/routes.ts +++ b/packages/docusaurus/src/server/routes.ts @@ -18,13 +18,13 @@ export function getAllFinalRoutes(routeConfig: RouteConfig[]): RouteConfig[] { } export function handleDuplicateRoutes( - pluginsRouteConfigs: RouteConfig[], + routes: RouteConfig[], onDuplicateRoutes: ReportingSeverity, ): void { if (onDuplicateRoutes === 'ignore') { return; } - const allRoutes: string[] = getAllFinalRoutes(pluginsRouteConfigs).map( + const allRoutes: string[] = getAllFinalRoutes(routes).map( (routeConfig) => routeConfig.path, ); const seenRoutes = new Set(); From e3676daba06837eace1b54bce979c8d2d8728c1a Mon Sep 17 00:00:00 2001 From: sebastien Date: Fri, 1 Mar 2024 18:14:03 +0100 Subject: [PATCH 13/18] more plugins refactor --- ...ndex.test.ts.snap => plugins.test.ts.snap} | 0 .../{index.test.ts => plugins.test.ts} | 2 +- .../docusaurus/src/server/plugins/index.ts | 256 ++++++++++-------- 3 files changed, 144 insertions(+), 114 deletions(-) rename packages/docusaurus/src/server/plugins/__tests__/__snapshots__/{index.test.ts.snap => plugins.test.ts.snap} (100%) rename packages/docusaurus/src/server/plugins/__tests__/{index.test.ts => plugins.test.ts} (97%) diff --git a/packages/docusaurus/src/server/plugins/__tests__/__snapshots__/index.test.ts.snap b/packages/docusaurus/src/server/plugins/__tests__/__snapshots__/plugins.test.ts.snap similarity index 100% rename from packages/docusaurus/src/server/plugins/__tests__/__snapshots__/index.test.ts.snap rename to packages/docusaurus/src/server/plugins/__tests__/__snapshots__/plugins.test.ts.snap diff --git a/packages/docusaurus/src/server/plugins/__tests__/index.test.ts b/packages/docusaurus/src/server/plugins/__tests__/plugins.test.ts similarity index 97% rename from packages/docusaurus/src/server/plugins/__tests__/index.test.ts rename to packages/docusaurus/src/server/plugins/__tests__/plugins.test.ts index 0ace57b22cd9..21733e98af30 100644 --- a/packages/docusaurus/src/server/plugins/__tests__/index.test.ts +++ b/packages/docusaurus/src/server/plugins/__tests__/plugins.test.ts @@ -6,7 +6,7 @@ */ import path from 'path'; -import {loadPlugins} from '..'; +import {loadPlugins} from '../index'; import type {Plugin, Props} from '@docusaurus/types'; describe('loadPlugins', () => { diff --git a/packages/docusaurus/src/server/plugins/index.ts b/packages/docusaurus/src/server/plugins/index.ts index ee4ed188063d..8120592f54e1 100644 --- a/packages/docusaurus/src/server/plugins/index.ts +++ b/packages/docusaurus/src/server/plugins/index.ts @@ -24,20 +24,18 @@ import type { PluginRouteContext, } from '@docusaurus/types'; -async function loadPlugin({ +async function translatePlugin({ plugin, context, }: { - plugin: InitializedPlugin; + plugin: LoadedPlugin; context: LoadContext; }): Promise { - const content = await PerfLogger.async( - `Plugins - loadContent - ${plugin.name}@${plugin.options.id}`, - () => plugin.loadContent?.(), - ); + const {content} = plugin; const rawTranslationFiles = - (await plugin.getTranslationFiles?.({content})) ?? []; + (await plugin.getTranslationFiles?.({content: plugin.content})) ?? []; + const translationFiles = await Promise.all( rawTranslationFiles.map((translationFile) => localizePluginTranslationFile({ @@ -50,6 +48,7 @@ async function loadPlugin({ const translatedContent = plugin.translateContent?.({content, translationFiles}) ?? content; + const translatedThemeConfigSlice = plugin.translateThemeConfig?.({ themeConfig: context.siteConfig.themeConfig, translationFiles, @@ -62,6 +61,37 @@ async function loadPlugin({ return {...plugin, content: translatedContent}; } +async function executePluginLoadContent({ + plugin, + context, +}: { + plugin: InitializedPlugin; + context: LoadContext; +}): Promise { + return PerfLogger.async( + `Plugin - loadContent - ${plugin.name}@${plugin.options.id}`, + async () => { + const content = await plugin.loadContent?.(); + const loadedPlugin: LoadedPlugin = {...plugin, content}; + return translatePlugin({plugin: loadedPlugin, context}); + }, + ); +} + +async function executePluginsLoadContent({ + plugins, + context, +}: { + plugins: InitializedPlugin[]; + context: LoadContext; +}) { + return PerfLogger.async(`Plugins - loadContent`, () => + Promise.all( + plugins.map((plugin) => executePluginLoadContent({plugin, context})), + ), + ); +} + function aggregateAllContent(loadedPlugins: LoadedPlugin[]): AllContent { return _.chain(loadedPlugins) .groupBy((item) => item.name) @@ -87,64 +117,71 @@ async function executePluginContentLoaded({ context: LoadContext; allContent: AllContent; }): Promise<{routes: RouteConfig[]; globalData: unknown}> { - if (!plugin.contentLoaded) { - return {routes: [], globalData: undefined}; - } - PerfLogger.start( + return PerfLogger.async( `Plugins - contentLoaded - ${plugin.name}@${plugin.options.id}`, - ); + async () => { + if (!plugin.contentLoaded) { + return {routes: [], globalData: undefined}; + } - const pluginId = plugin.options.id; - // Plugins data files are namespaced by pluginName/pluginId - const dataDir = path.join(context.generatedFilesDir, plugin.name, pluginId); - const pluginRouteContextModulePath = path.join( - dataDir, - `${docuHash('pluginRouteContextModule')}.json`, - ); - const pluginRouteContext: PluginRouteContext['plugin'] = { - name: plugin.name, - id: pluginId, - }; - await generate( - '/', - pluginRouteContextModulePath, - JSON.stringify(pluginRouteContext, null, 2), - ); + const pluginId = plugin.options.id; + // Plugins data files are namespaced by pluginName/pluginId + const dataDir = path.join( + context.generatedFilesDir, + plugin.name, + pluginId, + ); + const pluginRouteContextModulePath = path.join( + dataDir, + `${docuHash('pluginRouteContextModule')}.json`, + ); + const pluginRouteContext: PluginRouteContext['plugin'] = { + name: plugin.name, + id: pluginId, + }; + await generate( + '/', + pluginRouteContextModulePath, + JSON.stringify(pluginRouteContext, null, 2), + ); - const routes: RouteConfig[] = []; - let globalData: unknown; + const routes: RouteConfig[] = []; + let globalData: unknown; - const actions: PluginContentLoadedActions = { - addRoute(initialRouteConfig) { - // Trailing slash behavior is handled generically for all plugins - const finalRouteConfig = applyRouteTrailingSlash( - initialRouteConfig, - context.siteConfig, - ); - routes.push({ - ...finalRouteConfig, - context: { - ...(finalRouteConfig.context && {data: finalRouteConfig.context}), - plugin: pluginRouteContextModulePath, + const actions: PluginContentLoadedActions = { + addRoute(initialRouteConfig) { + // Trailing slash behavior is handled generically for all plugins + const finalRouteConfig = applyRouteTrailingSlash( + initialRouteConfig, + context.siteConfig, + ); + routes.push({ + ...finalRouteConfig, + context: { + ...(finalRouteConfig.context && {data: finalRouteConfig.context}), + plugin: pluginRouteContextModulePath, + }, + }); + }, + async createData(name, data) { + const modulePath = path.join(dataDir, name); + await generate(dataDir, name, data); + return modulePath; }, + setGlobalData(data) { + globalData = data; + }, + }; + + await plugin.contentLoaded({ + content: plugin.content, + actions, + allContent, }); - }, - async createData(name, data) { - const modulePath = path.join(dataDir, name); - await generate(dataDir, name, data); - return modulePath; - }, - setGlobalData(data) { - globalData = data; - }, - }; - await plugin.contentLoaded({content: plugin.content, actions, allContent}); - PerfLogger.end( - `Plugins - contentLoaded - ${plugin.name}@${plugin.options.id}`, + return {routes, globalData}; + }, ); - - return {routes, globalData}; } async function executePluginsContentLoaded({ @@ -161,29 +198,28 @@ async function executePluginsContentLoaded({ // A possible solution: make it a core feature instead of a plugin? allContent: AllContent; }): Promise<{routes: RouteConfig[]; globalData: GlobalData}> { - PerfLogger.start(`Plugins - contentLoaded`); - - const allRoutes: RouteConfig[] = []; - const allGlobalData: GlobalData = {}; + return PerfLogger.async(`Plugins - contentLoaded`, async () => { + const allRoutes: RouteConfig[] = []; + const allGlobalData: GlobalData = {}; - await Promise.all( - plugins.map(async (plugin) => { - const {routes, globalData} = await executePluginContentLoaded({ - plugin, - context, - allContent, - }); + await Promise.all( + plugins.map(async (plugin) => { + const {routes, globalData} = await executePluginContentLoaded({ + plugin, + context, + allContent, + }); - allRoutes.push(...routes); - if (globalData !== undefined) { - allGlobalData[plugin.name] ??= {}; - allGlobalData[plugin.name]![plugin.options.id] = globalData; - } - }), - ); - PerfLogger.end(`Plugins - contentLoaded`); + allRoutes.push(...routes); + if (globalData !== undefined) { + allGlobalData[plugin.name] ??= {}; + allGlobalData[plugin.name]![plugin.options.id] = globalData; + } + }), + ); - return {routes: allRoutes, globalData: allGlobalData}; + return {routes: allRoutes, globalData: allGlobalData}; + }); } /** @@ -197,45 +233,39 @@ export async function loadPlugins(context: LoadContext): Promise<{ routes: RouteConfig[]; globalData: GlobalData; }> { - // 1. Plugin Lifecycle - Initialization/Constructor. - PerfLogger.start('Plugins - initPlugins'); - const plugins: InitializedPlugin[] = await initPlugins(context); - PerfLogger.end('Plugins - initPlugins'); - - plugins.push( - createBootstrapPlugin(context), - createMDXFallbackPlugin(context), - ); + return PerfLogger.async('Plugins - loadPlugins', async () => { + // 1. Plugin Lifecycle - Initialization/Constructor. + const plugins: InitializedPlugin[] = await PerfLogger.async( + 'Plugins - initPlugins', + () => initPlugins(context), + ); - // 2. Plugin Lifecycle - loadContent. - // Currently plugins run lifecycle methods in parallel and are not - // order-dependent. We could change this in future if there are plugins which - // need to run in certain order or depend on others for data. - // This would also translate theme config and content upfront, given the - // translation files that the plugin declares. - PerfLogger.start(`Plugins - loadContent`); - const loadedPlugins: LoadedPlugin[] = await Promise.all( - plugins.map((plugin) => loadPlugin({plugin, context})), - ); - PerfLogger.end(`Plugins - loadContent`); + plugins.push( + createBootstrapPlugin(context), + createMDXFallbackPlugin(context), + ); - const allContent = aggregateAllContent(loadedPlugins); + // 2. Plugin Lifecycle - loadContent. + // Currently plugins run lifecycle methods in parallel and are not + // order-dependent. We could change this in future if there are plugins which + // need to run in certain order or depend on others for data. + // This would also translate theme config and content upfront, given the + // translation files that the plugin declares. + const loadedPlugins = await executePluginsLoadContent({plugins, context}); - // 3. Plugin Lifecycle - contentLoaded. - const {routes, globalData} = await PerfLogger.async( - 'Plugins - contentLoaded', - () => - executePluginsContentLoaded({ - plugins: loadedPlugins, - context, - allContent, - }), - ); - PerfLogger.end(`Plugins - contentLoaded`); + const allContent = aggregateAllContent(loadedPlugins); + + // 3. Plugin Lifecycle - contentLoaded. + const {routes, globalData} = await executePluginsContentLoaded({ + plugins: loadedPlugins, + context, + allContent, + }); - // Sort the route config. This ensures that route with nested - // routes are always placed last. - sortRoutes(routes, context.siteConfig.baseUrl); + // Sort the route config. This ensures that route with nested + // routes are always placed last. + sortRoutes(routes, context.siteConfig.baseUrl); - return {plugins: loadedPlugins, routes, globalData}; + return {plugins: loadedPlugins, routes, globalData}; + }); } From 84d163a1271ce7795480d5d75a1920c723ad18dd Mon Sep 17 00:00:00 2001 From: sebastien Date: Fri, 1 Mar 2024 18:17:19 +0100 Subject: [PATCH 14/18] more plugins refactor --- packages/docusaurus/src/server/index.ts | 2 +- .../server/plugins/__tests__/plugins.test.ts | 2 +- .../server/plugins/{index.ts => plugins.ts} | 39 +++++++++---------- 3 files changed, 20 insertions(+), 23 deletions(-) rename packages/docusaurus/src/server/plugins/{index.ts => plugins.ts} (88%) diff --git a/packages/docusaurus/src/server/index.ts b/packages/docusaurus/src/server/index.ts index b0ebb48a0414..82311edad36f 100644 --- a/packages/docusaurus/src/server/index.ts +++ b/packages/docusaurus/src/server/index.ts @@ -14,7 +14,7 @@ import { import combinePromises from 'combine-promises'; import {loadSiteConfig} from './config'; import {loadClientModules} from './clientModules'; -import {loadPlugins} from './plugins'; +import {loadPlugins} from './plugins/plugins'; import {loadHtmlTags} from './htmlTags'; import {loadSiteMetadata} from './siteMetadata'; import {loadI18n} from './i18n'; diff --git a/packages/docusaurus/src/server/plugins/__tests__/plugins.test.ts b/packages/docusaurus/src/server/plugins/__tests__/plugins.test.ts index 21733e98af30..7729ef2bd05a 100644 --- a/packages/docusaurus/src/server/plugins/__tests__/plugins.test.ts +++ b/packages/docusaurus/src/server/plugins/__tests__/plugins.test.ts @@ -6,7 +6,7 @@ */ import path from 'path'; -import {loadPlugins} from '../index'; +import {loadPlugins} from '../plugins'; import type {Plugin, Props} from '@docusaurus/types'; describe('loadPlugins', () => { diff --git a/packages/docusaurus/src/server/plugins/index.ts b/packages/docusaurus/src/server/plugins/plugins.ts similarity index 88% rename from packages/docusaurus/src/server/plugins/index.ts rename to packages/docusaurus/src/server/plugins/plugins.ts index 8120592f54e1..ea810c427301 100644 --- a/packages/docusaurus/src/server/plugins/index.ts +++ b/packages/docusaurus/src/server/plugins/plugins.ts @@ -199,26 +199,32 @@ async function executePluginsContentLoaded({ allContent: AllContent; }): Promise<{routes: RouteConfig[]; globalData: GlobalData}> { return PerfLogger.async(`Plugins - contentLoaded`, async () => { - const allRoutes: RouteConfig[] = []; - const allGlobalData: GlobalData = {}; + const routes: RouteConfig[] = []; + const globalData: GlobalData = {}; await Promise.all( plugins.map(async (plugin) => { - const {routes, globalData} = await executePluginContentLoaded({ - plugin, - context, - allContent, - }); + const {routes: pluginRoutes, globalData: pluginGlobalData} = + await executePluginContentLoaded({ + plugin, + context, + allContent, + }); + + routes.push(...pluginRoutes); - allRoutes.push(...routes); - if (globalData !== undefined) { - allGlobalData[plugin.name] ??= {}; - allGlobalData[plugin.name]![plugin.options.id] = globalData; + if (pluginGlobalData !== undefined) { + globalData[plugin.name] ??= {}; + globalData[plugin.name]![plugin.options.id] = pluginGlobalData; } }), ); - return {routes: allRoutes, globalData: allGlobalData}; + // Sort the route config. + // This ensures that route with sub routes are always placed last. + sortRoutes(routes, context.siteConfig.baseUrl); + + return {routes, globalData}; }); } @@ -246,11 +252,6 @@ export async function loadPlugins(context: LoadContext): Promise<{ ); // 2. Plugin Lifecycle - loadContent. - // Currently plugins run lifecycle methods in parallel and are not - // order-dependent. We could change this in future if there are plugins which - // need to run in certain order or depend on others for data. - // This would also translate theme config and content upfront, given the - // translation files that the plugin declares. const loadedPlugins = await executePluginsLoadContent({plugins, context}); const allContent = aggregateAllContent(loadedPlugins); @@ -262,10 +263,6 @@ export async function loadPlugins(context: LoadContext): Promise<{ allContent, }); - // Sort the route config. This ensures that route with nested - // routes are always placed last. - sortRoutes(routes, context.siteConfig.baseUrl); - return {plugins: loadedPlugins, routes, globalData}; }); } From 593f65aa82918cff7fe8aba92a5288fff757e2a3 Mon Sep 17 00:00:00 2001 From: sebastien Date: Fri, 1 Mar 2024 18:37:32 +0100 Subject: [PATCH 15/18] rename index.ts to site.ts --- .../docusaurus-plugin-content-docs/src/__tests__/docs.test.ts | 2 +- .../src/__tests__/index.test.ts | 2 +- packages/docusaurus/src/commands/build.ts | 2 +- packages/docusaurus/src/commands/deploy.ts | 2 +- packages/docusaurus/src/commands/external.ts | 2 +- packages/docusaurus/src/commands/serve.ts | 2 +- packages/docusaurus/src/commands/start/start.ts | 4 ++-- packages/docusaurus/src/commands/swizzle/context.ts | 2 +- packages/docusaurus/src/commands/writeHeadingIds.ts | 2 +- packages/docusaurus/src/commands/writeTranslations.ts | 2 +- .../__snapshots__/{index.test.ts.snap => site.test.ts.snap} | 0 .../src/server/__tests__/{index.test.ts => site.test.ts} | 0 packages/docusaurus/src/server/__tests__/testUtils.ts | 2 +- packages/docusaurus/src/server/i18n.ts | 2 +- packages/docusaurus/src/server/plugins/__tests__/init.test.ts | 2 +- packages/docusaurus/src/server/{index.ts => site.ts} | 0 16 files changed, 14 insertions(+), 14 deletions(-) rename packages/docusaurus/src/server/__tests__/__snapshots__/{index.test.ts.snap => site.test.ts.snap} (100%) rename packages/docusaurus/src/server/__tests__/{index.test.ts => site.test.ts} (100%) rename packages/docusaurus/src/server/{index.ts => site.ts} (100%) diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/docs.test.ts b/packages/docusaurus-plugin-content-docs/src/__tests__/docs.test.ts index a2c86b4d5935..8d64cd27f3d5 100644 --- a/packages/docusaurus-plugin-content-docs/src/__tests__/docs.test.ts +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/docs.test.ts @@ -7,7 +7,7 @@ import {jest} from '@jest/globals'; import path from 'path'; -import {loadContext} from '@docusaurus/core/src/server/index'; +import {loadContext} from '@docusaurus/core/src/server/site'; import {createSlugger, posixPath, DEFAULT_PLUGIN_ID} from '@docusaurus/utils'; import {createSidebarsUtils} from '../sidebars/utils'; import { diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/index.test.ts b/packages/docusaurus-plugin-content-docs/src/__tests__/index.test.ts index dd1bce5e05f6..1aca97041d2a 100644 --- a/packages/docusaurus-plugin-content-docs/src/__tests__/index.test.ts +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/index.test.ts @@ -12,7 +12,7 @@ import _ from 'lodash'; import {isMatch} from 'picomatch'; import commander from 'commander'; import webpack from 'webpack'; -import {loadContext} from '@docusaurus/core/src/server/index'; +import {loadContext} from '@docusaurus/core/src/server/site'; import {applyConfigureWebpack} from '@docusaurus/core/src/webpack/utils'; import {sortRoutes} from '@docusaurus/core/src/server/plugins/routeConfig'; import {posixPath} from '@docusaurus/utils'; diff --git a/packages/docusaurus/src/commands/build.ts b/packages/docusaurus/src/commands/build.ts index 156e669a7e5e..cfd11c0c90a7 100644 --- a/packages/docusaurus/src/commands/build.ts +++ b/packages/docusaurus/src/commands/build.ts @@ -10,7 +10,7 @@ import path from 'path'; import _ from 'lodash'; import logger from '@docusaurus/logger'; import {DOCUSAURUS_VERSION, mapAsyncSequential} from '@docusaurus/utils'; -import {loadSite, loadContext, type LoadContextParams} from '../server'; +import {loadSite, loadContext, type LoadContextParams} from '../server/site'; import {handleBrokenLinks} from '../server/brokenLinks'; import {createBuildClientConfig} from '../webpack/client'; diff --git a/packages/docusaurus/src/commands/deploy.ts b/packages/docusaurus/src/commands/deploy.ts index 3d1b46132dcb..e16f4881552b 100644 --- a/packages/docusaurus/src/commands/deploy.ts +++ b/packages/docusaurus/src/commands/deploy.ts @@ -11,7 +11,7 @@ import os from 'os'; import logger from '@docusaurus/logger'; import shell from 'shelljs'; import {hasSSHProtocol, buildSshUrl, buildHttpsUrl} from '@docusaurus/utils'; -import {loadContext, type LoadContextParams} from '../server'; +import {loadContext, type LoadContextParams} from '../server/site'; import {build} from './build'; export type DeployCLIOptions = Pick< diff --git a/packages/docusaurus/src/commands/external.ts b/packages/docusaurus/src/commands/external.ts index 45ae8d55d11c..44e161a1f11c 100644 --- a/packages/docusaurus/src/commands/external.ts +++ b/packages/docusaurus/src/commands/external.ts @@ -6,7 +6,7 @@ */ import fs from 'fs-extra'; -import {loadContext} from '../server'; +import {loadContext} from '../server/site'; import {initPlugins} from '../server/plugins/init'; import type {CommanderStatic} from 'commander'; diff --git a/packages/docusaurus/src/commands/serve.ts b/packages/docusaurus/src/commands/serve.ts index 1253528961a3..aea422d16c20 100644 --- a/packages/docusaurus/src/commands/serve.ts +++ b/packages/docusaurus/src/commands/serve.ts @@ -15,7 +15,7 @@ import openBrowser from 'react-dev-utils/openBrowser'; import {loadSiteConfig} from '../server/config'; import {build} from './build'; import {getHostPort, type HostPortOptions} from '../server/getHostPort'; -import type {LoadContextParams} from '../server'; +import type {LoadContextParams} from '../server/site'; export type ServeCLIOptions = HostPortOptions & Pick & { diff --git a/packages/docusaurus/src/commands/start/start.ts b/packages/docusaurus/src/commands/start/start.ts index e4ef23578683..d78bea383ee4 100644 --- a/packages/docusaurus/src/commands/start/start.ts +++ b/packages/docusaurus/src/commands/start/start.ts @@ -9,12 +9,12 @@ import fs from 'fs-extra'; import _ from 'lodash'; import logger from '@docusaurus/logger'; import openBrowser from 'react-dev-utils/openBrowser'; -import {loadSite, reloadSite, reloadSitePlugin} from '../../server'; +import {loadSite, reloadSite, reloadSitePlugin} from '../../server/site'; import {PerfLogger} from '../../utils'; import {setupSiteFileWatchers} from './watcher'; import {createWebpackDevServer} from './webpack'; import {createOpenUrlContext} from './utils'; -import type {LoadContextParams, LoadSiteParams} from '../../server'; +import type {LoadContextParams, LoadSiteParams} from '../../server/site'; import type {HostPortOptions} from '../../server/getHostPort'; import type {LoadedPlugin} from '@docusaurus/types'; diff --git a/packages/docusaurus/src/commands/swizzle/context.ts b/packages/docusaurus/src/commands/swizzle/context.ts index 903d447da1f1..45c0e1a87488 100644 --- a/packages/docusaurus/src/commands/swizzle/context.ts +++ b/packages/docusaurus/src/commands/swizzle/context.ts @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -import {loadContext} from '../../server'; +import {loadContext} from '../../server/site'; import {initPlugins} from '../../server/plugins/init'; import {loadPluginConfigs} from '../../server/plugins/configs'; import type {SwizzleCLIOptions, SwizzleContext} from './common'; diff --git a/packages/docusaurus/src/commands/writeHeadingIds.ts b/packages/docusaurus/src/commands/writeHeadingIds.ts index 864708620780..909e3136b55f 100644 --- a/packages/docusaurus/src/commands/writeHeadingIds.ts +++ b/packages/docusaurus/src/commands/writeHeadingIds.ts @@ -11,7 +11,7 @@ import { writeMarkdownHeadingId, type WriteHeadingIDOptions, } from '@docusaurus/utils'; -import {loadContext} from '../server'; +import {loadContext} from '../server/site'; import {initPlugins} from '../server/plugins/init'; import {safeGlobby} from '../server/utils'; diff --git a/packages/docusaurus/src/commands/writeTranslations.ts b/packages/docusaurus/src/commands/writeTranslations.ts index c58465c41df0..9b94cedae28c 100644 --- a/packages/docusaurus/src/commands/writeTranslations.ts +++ b/packages/docusaurus/src/commands/writeTranslations.ts @@ -7,7 +7,7 @@ import fs from 'fs-extra'; import path from 'path'; -import {loadContext, type LoadContextParams} from '../server'; +import {loadContext, type LoadContextParams} from '../server/site'; import {initPlugins} from '../server/plugins/init'; import { writePluginTranslations, diff --git a/packages/docusaurus/src/server/__tests__/__snapshots__/index.test.ts.snap b/packages/docusaurus/src/server/__tests__/__snapshots__/site.test.ts.snap similarity index 100% rename from packages/docusaurus/src/server/__tests__/__snapshots__/index.test.ts.snap rename to packages/docusaurus/src/server/__tests__/__snapshots__/site.test.ts.snap diff --git a/packages/docusaurus/src/server/__tests__/index.test.ts b/packages/docusaurus/src/server/__tests__/site.test.ts similarity index 100% rename from packages/docusaurus/src/server/__tests__/index.test.ts rename to packages/docusaurus/src/server/__tests__/site.test.ts diff --git a/packages/docusaurus/src/server/__tests__/testUtils.ts b/packages/docusaurus/src/server/__tests__/testUtils.ts index 4491e4b5d097..178c3c1c2452 100644 --- a/packages/docusaurus/src/server/__tests__/testUtils.ts +++ b/packages/docusaurus/src/server/__tests__/testUtils.ts @@ -6,7 +6,7 @@ */ import path from 'path'; -import {loadSite, type LoadContextParams} from '../index'; +import {loadSite, type LoadContextParams} from '../site'; import type {Site} from '@docusaurus/types'; // Helper methods to setup dummy/fake projects. diff --git a/packages/docusaurus/src/server/i18n.ts b/packages/docusaurus/src/server/i18n.ts index dc54c6d54e06..f87cc0e93cb8 100644 --- a/packages/docusaurus/src/server/i18n.ts +++ b/packages/docusaurus/src/server/i18n.ts @@ -8,7 +8,7 @@ import logger from '@docusaurus/logger'; import {getLangDir} from 'rtl-detect'; import type {I18n, DocusaurusConfig, I18nLocaleConfig} from '@docusaurus/types'; -import type {LoadContextParams} from './index'; +import type {LoadContextParams} from './site'; function getDefaultLocaleLabel(locale: string) { const languageName = new Intl.DisplayNames(locale, {type: 'language'}).of( diff --git a/packages/docusaurus/src/server/plugins/__tests__/init.test.ts b/packages/docusaurus/src/server/plugins/__tests__/init.test.ts index 7ef1f5e3e41e..6cbb8af06c3d 100644 --- a/packages/docusaurus/src/server/plugins/__tests__/init.test.ts +++ b/packages/docusaurus/src/server/plugins/__tests__/init.test.ts @@ -7,7 +7,7 @@ import path from 'path'; -import {loadContext, type LoadContextParams} from '../../index'; +import {loadContext, type LoadContextParams} from '../../site'; import {initPlugins} from '../init'; describe('initPlugins', () => { diff --git a/packages/docusaurus/src/server/index.ts b/packages/docusaurus/src/server/site.ts similarity index 100% rename from packages/docusaurus/src/server/index.ts rename to packages/docusaurus/src/server/site.ts From f0fdb8c9b902414ad70093b8b0eb984fc99839f3 Mon Sep 17 00:00:00 2001 From: sebastien Date: Fri, 1 Mar 2024 19:51:44 +0100 Subject: [PATCH 16/18] make fine-grained plugin reloading work --- .../docusaurus/src/server/codegen/codegen.ts | 2 +- .../src/server/codegen/codegenRoutes.ts | 7 +- .../docusaurus/src/server/plugins/plugins.ts | 66 +++++++-- packages/docusaurus/src/server/site.ts | 134 +++++++++++++----- 4 files changed, 150 insertions(+), 59 deletions(-) diff --git a/packages/docusaurus/src/server/codegen/codegen.ts b/packages/docusaurus/src/server/codegen/codegen.ts index 2cc8ad3adafd..69450426826f 100644 --- a/packages/docusaurus/src/server/codegen/codegen.ts +++ b/packages/docusaurus/src/server/codegen/codegen.ts @@ -140,7 +140,7 @@ type CodegenParams = { i18n: I18n; codeTranslations: CodeTranslations; siteMetadata: SiteMetadata; - routeConfigs: RouteConfig[]; + routes: RouteConfig[]; }; export async function generateSiteFiles(params: CodegenParams): Promise { diff --git a/packages/docusaurus/src/server/codegen/codegenRoutes.ts b/packages/docusaurus/src/server/codegen/codegenRoutes.ts index c0fa0c415af0..8306e0f78d99 100644 --- a/packages/docusaurus/src/server/codegen/codegenRoutes.ts +++ b/packages/docusaurus/src/server/codegen/codegenRoutes.ts @@ -310,16 +310,15 @@ const genRoutes = ({ type GenerateRouteFilesParams = { generatedFilesDir: string; - routeConfigs: RouteConfig[]; + routes: RouteConfig[]; baseUrl: string; }; export async function generateRouteFiles({ generatedFilesDir, - routeConfigs, + routes, }: GenerateRouteFilesParams): Promise { - const {registry, routesChunkNames, routesConfig} = - generateRoutesCode(routeConfigs); + const {registry, routesChunkNames, routesConfig} = generateRoutesCode(routes); await Promise.all([ genRegistry({generatedFilesDir, registry}), genRoutesChunkNames({generatedFilesDir, routesChunkNames}), diff --git a/packages/docusaurus/src/server/plugins/plugins.ts b/packages/docusaurus/src/server/plugins/plugins.ts index ea810c427301..13b4f59d1b00 100644 --- a/packages/docusaurus/src/server/plugins/plugins.ts +++ b/packages/docusaurus/src/server/plugins/plugins.ts @@ -54,6 +54,7 @@ async function translatePlugin({ translationFiles, }); + // TODO dangerous legacy, need to be refactored! // Side-effect to merge theme config translations. A plugin should only // translate its own slice of theme config and should make no assumptions // about other plugins' keys, so this is safe to run in parallel. @@ -115,6 +116,12 @@ async function executePluginContentLoaded({ }: { plugin: LoadedPlugin; context: LoadContext; + // TODO AllContent was injected to this lifecycle for the debug plugin + // This is what permits to create the debug routes for all other plugins + // This was likely a bad idea and prevents to start executing contentLoaded() + // until all plugins have finished loading all the data + // we'd rather remove this and find another way to implement the debug plugin + // A possible solution: make it a core feature instead of a plugin? allContent: AllContent; }): Promise<{routes: RouteConfig[]; globalData: unknown}> { return PerfLogger.async( @@ -187,18 +194,13 @@ async function executePluginContentLoaded({ async function executePluginsContentLoaded({ plugins, context, - allContent, }: { plugins: LoadedPlugin[]; context: LoadContext; - // TODO AllContent was injected to this lifecycle for the debug plugin - // this was likely a bad idea and prevent to start executing contentLoaded() - // until all plugins have finished loading all the data - // we'd rather remove this and find another way to implement the debug plugin - // A possible solution: make it a core feature instead of a plugin? - allContent: AllContent; }): Promise<{routes: RouteConfig[]; globalData: GlobalData}> { return PerfLogger.async(`Plugins - contentLoaded`, async () => { + const allContent = aggregateAllContent(plugins); + const routes: RouteConfig[] = []; const globalData: GlobalData = {}; @@ -228,17 +230,21 @@ async function executePluginsContentLoaded({ }); } +export type LoadPluginsResult = { + plugins: LoadedPlugin[]; + routes: RouteConfig[]; + globalData: GlobalData; +}; + /** * Initializes the plugins, runs `loadContent`, `translateContent`, * `contentLoaded`, and `translateThemeConfig`. Because `contentLoaded` is * side-effect-ful (it generates temp files), so is this function. This function * would also mutate `context.siteConfig.themeConfig` to translate it. */ -export async function loadPlugins(context: LoadContext): Promise<{ - plugins: LoadedPlugin[]; - routes: RouteConfig[]; - globalData: GlobalData; -}> { +export async function loadPlugins( + context: LoadContext, +): Promise { return PerfLogger.async('Plugins - loadPlugins', async () => { // 1. Plugin Lifecycle - Initialization/Constructor. const plugins: InitializedPlugin[] = await PerfLogger.async( @@ -254,15 +260,45 @@ export async function loadPlugins(context: LoadContext): Promise<{ // 2. Plugin Lifecycle - loadContent. const loadedPlugins = await executePluginsLoadContent({plugins, context}); - const allContent = aggregateAllContent(loadedPlugins); - // 3. Plugin Lifecycle - contentLoaded. const {routes, globalData} = await executePluginsContentLoaded({ plugins: loadedPlugins, context, - allContent, }); return {plugins: loadedPlugins, routes, globalData}; }); } + +export async function reloadPlugin({ + plugin, + plugins, + context, +}: { + plugin: LoadedPlugin; + plugins: LoadedPlugin[]; + context: LoadContext; +}): Promise { + return PerfLogger.async('Plugins - reloadPlugin', async () => { + const pluginIndex = plugins.findIndex( + (p) => p.name === plugin.name && p.options.id === plugin.options.id, + ); + if (pluginIndex === -1) { + throw new Error( + 'Unexpected: this code assumes the plugin to reload is in the list of provided plugins', + ); + } + + const reloadedPlugin = await executePluginLoadContent({plugin, context}); + const newPlugins = plugins.with(pluginIndex, reloadedPlugin); + + // Unfortunately, due to the "AllContent" data we have to re-execute this + // for all plugins, not just the one to reload... + const {routes, globalData} = await executePluginsContentLoaded({ + plugins: newPlugins, + context, + }); + + return {plugins: newPlugins, routes, globalData}; + }); +} diff --git a/packages/docusaurus/src/server/site.ts b/packages/docusaurus/src/server/site.ts index 82311edad36f..f228e816fc94 100644 --- a/packages/docusaurus/src/server/site.ts +++ b/packages/docusaurus/src/server/site.ts @@ -14,7 +14,7 @@ import { import combinePromises from 'combine-promises'; import {loadSiteConfig} from './config'; import {loadClientModules} from './clientModules'; -import {loadPlugins} from './plugins/plugins'; +import {loadPlugins, reloadPlugin} from './plugins/plugins'; import {loadHtmlTags} from './htmlTags'; import {loadSiteMetadata} from './siteMetadata'; import {loadI18n} from './i18n'; @@ -25,8 +25,10 @@ import { import {PerfLogger} from '../utils'; import {generateSiteFiles} from './codegen/codegen'; import {getRoutesPaths, handleDuplicateRoutes} from './routes'; +import type {LoadPluginsResult} from './plugins/plugins'; import type { DocusaurusConfig, + GlobalData, LoadContext, LoadedPlugin, Props, @@ -116,21 +118,13 @@ export async function loadContext( }; } -/** - * This is the crux of the Docusaurus server-side. It reads everything it needs— - * code translations, config file, plugin modules... Plugins then use their - * lifecycles to generate content and other data. It is side-effect-ful because - * it generates temp files in the `.docusaurus` folder for the bundler. - */ -export async function loadSite(params: LoadContextParams): Promise { - const {siteDir} = params; - - PerfLogger.start('Load - loadContext'); - const context = await loadContext(params); - PerfLogger.end('Load - loadContext'); - +async function createSiteProps( + params: LoadPluginsResult & {context: LoadContext}, +): Promise { + const {plugins, routes, context} = params; const { generatedFilesDir, + siteDir, siteConfig, siteConfigPath, outDir, @@ -140,14 +134,10 @@ export async function loadSite(params: LoadContextParams): Promise { codeTranslations: siteCodeTranslations, } = context; - PerfLogger.start('Load - loadPlugins'); - const {plugins, routes, globalData} = await loadPlugins(context); - PerfLogger.end('Load - loadPlugins'); - const {headTags, preBodyTags, postBodyTags} = loadHtmlTags(plugins); - const clientModules = loadClientModules(plugins); const {codeTranslations, siteMetadata} = await combinePromises({ + // TODO code translations should be loaded as part of LoadedPlugin? codeTranslations: PerfLogger.async( 'Load - loadCodeTranslations', async () => ({ @@ -160,24 +150,10 @@ export async function loadSite(params: LoadContextParams): Promise { ), }); - PerfLogger.start('Load - generateSiteCode'); - await generateSiteFiles({ - generatedFilesDir, - clientModules, - siteConfig, - siteMetadata, - i18n, - codeTranslations, - globalData, - routeConfigs: routes, - baseUrl, - }); - PerfLogger.end('Load - generateSiteCode'); - handleDuplicateRoutes(routes, siteConfig.onDuplicateRoutes); const routesPaths = getRoutesPaths(routes, baseUrl); - const props: Props = { + return { siteConfig, siteConfigPath, siteMetadata, @@ -195,11 +171,69 @@ export async function loadSite(params: LoadContextParams): Promise { postBodyTags, codeTranslations, }; +} - return { - props, - params, - }; +// TODO global data should be part of site props? +async function createSiteFiles({ + site, + globalData, +}: { + site: Site; + globalData: GlobalData; +}) { + return PerfLogger.async('Load - createSiteFiles', async () => { + const { + props: { + plugins, + generatedFilesDir, + siteConfig, + siteMetadata, + i18n, + codeTranslations, + routes, + baseUrl, + }, + } = site; + const clientModules = loadClientModules(plugins); + await generateSiteFiles({ + generatedFilesDir, + clientModules, + siteConfig, + siteMetadata, + i18n, + codeTranslations, + globalData, + routes, + baseUrl, + }); + }); +} + +/** + * This is the crux of the Docusaurus server-side. It reads everything it needs— + * code translations, config file, plugin modules... Plugins then use their + * lifecycles to generate content and other data. It is side-effect-ful because + * it generates temp files in the `.docusaurus` folder for the bundler. + */ +export async function loadSite(params: LoadContextParams): Promise { + PerfLogger.start('Load - loadContext'); + const context = await loadContext(params); + PerfLogger.end('Load - loadContext'); + + PerfLogger.start('Load - loadPlugins'); + const {plugins, routes, globalData} = await loadPlugins(context); + PerfLogger.end('Load - loadPlugins'); + + const props = await createSiteProps({plugins, routes, globalData, context}); + + const site: Site = {props, params}; + + await createSiteFiles({ + site, + globalData, + }); + + return site; } export async function reloadSite(site: Site): Promise { @@ -214,5 +248,27 @@ export async function reloadSitePlugin( plugin: LoadedPlugin, ): Promise { console.log(`reloadSitePlugin ${plugin.name}`); - return loadSite(site.params); + + const {plugins, routes, globalData} = await reloadPlugin({ + plugin, + plugins: site.props.plugins, + context: site.props, + }); + + const newProps = await createSiteProps({ + plugins, + routes, + globalData, + context: site.props, // Props extends Context + }); + + const newSite: Site = { + props: newProps, + params: site.params, + }; + + // TODO optimize, bypass codegen if new site is similar to old site + await createSiteFiles({site: newSite, globalData}); + + return newSite; } From ba4b935aa7025a29a02643a9cac4297a8eb211d3 Mon Sep 17 00:00:00 2001 From: sebastien Date: Fri, 8 Mar 2024 12:08:41 +0100 Subject: [PATCH 17/18] Introduce PluginIdentifier type to ensure we don't lookup plugins by object identities --- packages/docusaurus-types/src/plugin.d.ts | 9 ++ .../docusaurus/src/commands/start/start.ts | 81 +---------------- .../docusaurus/src/commands/start/utils.ts | 89 +++++++++++++++++++ .../docusaurus/src/server/plugins/plugins.ts | 36 +++++--- packages/docusaurus/src/server/site.ts | 12 +-- 5 files changed, 133 insertions(+), 94 deletions(-) diff --git a/packages/docusaurus-types/src/plugin.d.ts b/packages/docusaurus-types/src/plugin.d.ts index 646562df8d9d..858fba9b7c69 100644 --- a/packages/docusaurus-types/src/plugin.d.ts +++ b/packages/docusaurus-types/src/plugin.d.ts @@ -163,6 +163,15 @@ export type Plugin = { }) => ThemeConfig; }; +/** + * Data required to uniquely identify a plugin + * The name or instance id alone is not enough + */ +export type PluginIdentifier = { + readonly name: string; + readonly id: string; +}; + export type InitializedPlugin = Plugin & { readonly options: Required; readonly version: PluginVersionInformation; diff --git a/packages/docusaurus/src/commands/start/start.ts b/packages/docusaurus/src/commands/start/start.ts index d78bea383ee4..e0e5595e52c5 100644 --- a/packages/docusaurus/src/commands/start/start.ts +++ b/packages/docusaurus/src/commands/start/start.ts @@ -5,18 +5,13 @@ * LICENSE file in the root directory of this source tree. */ -import fs from 'fs-extra'; -import _ from 'lodash'; import logger from '@docusaurus/logger'; import openBrowser from 'react-dev-utils/openBrowser'; -import {loadSite, reloadSite, reloadSitePlugin} from '../../server/site'; -import {PerfLogger} from '../../utils'; import {setupSiteFileWatchers} from './watcher'; import {createWebpackDevServer} from './webpack'; -import {createOpenUrlContext} from './utils'; -import type {LoadContextParams, LoadSiteParams} from '../../server/site'; +import {createReloadableSite} from './utils'; +import type {LoadContextParams} from '../../server/site'; import type {HostPortOptions} from '../../server/getHostPort'; -import type {LoadedPlugin} from '@docusaurus/types'; export type StartCLIOptions = HostPortOptions & Pick & { @@ -26,77 +21,6 @@ export type StartCLIOptions = HostPortOptions & minify?: boolean; }; -async function createLoadSiteParams({ - siteDirParam, - cliOptions, -}: StartParams): Promise { - const siteDir = await fs.realpath(siteDirParam); - return { - siteDir, - config: cliOptions.config, - locale: cliOptions.locale, - localizePath: undefined, // Should this be configurable? - }; -} - -async function createReloadableSite(startParams: StartParams) { - const openUrlContext = await createOpenUrlContext(startParams); - - let site = await PerfLogger.async('Loading site', async () => { - const params = await createLoadSiteParams(startParams); - return loadSite(params); - }); - - const get = () => site; - - const getOpenUrl = () => - openUrlContext.getOpenUrl({ - baseUrl: site.props.baseUrl, - }); - - const printOpenUrlMessage = () => { - logger.success`Docusaurus website is running at: url=${getOpenUrl()}`; - }; - printOpenUrlMessage(); - - const reloadBase = async () => { - try { - const oldSite = site; - site = await PerfLogger.async('Reloading site', () => reloadSite(site)); - if (oldSite.props.baseUrl !== site.props.baseUrl) { - printOpenUrlMessage(); - } - } catch (e) { - logger.error('Site reload failure'); - console.error(e); - } - }; - - // TODO instead of debouncing we should rather add AbortController support - const reload = _.debounce(reloadBase, 500); - - const reloadPlugin = async (plugin: LoadedPlugin) => { - try { - site = await PerfLogger.async( - `Reloading site plugin ${plugin.name}@${plugin.options.id}`, - () => reloadSitePlugin(site, plugin), - ); - } catch (e) { - logger.error( - `Site plugin reload failure - Plugin ${plugin.name}@${plugin.options.id}`, - ); - console.error(e); - } - }; - - return {get, getOpenUrl, reload, reloadPlugin, openUrlContext}; -} - -type StartParams = { - siteDirParam: string; - cliOptions: Partial; -}; - export async function start( siteDirParam: string = '.', cliOptions: Partial = {}, @@ -108,6 +32,7 @@ export async function start( process.env.DOCUSAURUS_CURRENT_LOCALE = cliOptions.locale; const reloadableSite = await createReloadableSite({siteDirParam, cliOptions}); + setupSiteFileWatchers( {props: reloadableSite.get().props, cliOptions}, ({plugin}) => { diff --git a/packages/docusaurus/src/commands/start/utils.ts b/packages/docusaurus/src/commands/start/utils.ts index bb3398072fc3..f23ac13ecfc6 100644 --- a/packages/docusaurus/src/commands/start/utils.ts +++ b/packages/docusaurus/src/commands/start/utils.ts @@ -5,10 +5,21 @@ * LICENSE file in the root directory of this source tree. */ +import fs from 'fs-extra'; +import _ from 'lodash'; import {prepareUrls} from 'react-dev-utils/WebpackDevServerUtils'; import {normalizeUrl} from '@docusaurus/utils'; +import logger from '@docusaurus/logger'; import {getHostPort} from '../../server/getHostPort'; +import {PerfLogger} from '../../utils'; +import { + loadSite, + type LoadSiteParams, + reloadSite, + reloadSitePlugin, +} from '../../server/site'; import type {StartCLIOptions} from './start'; +import type {LoadedPlugin} from '@docusaurus/types'; export type OpenUrlContext = { host: string; @@ -35,3 +46,81 @@ export async function createOpenUrlContext({ return {host, port, getOpenUrl}; } + +type StartParams = { + siteDirParam: string; + cliOptions: Partial; +}; + +async function createLoadSiteParams({ + siteDirParam, + cliOptions, +}: StartParams): Promise { + const siteDir = await fs.realpath(siteDirParam); + return { + siteDir, + config: cliOptions.config, + locale: cliOptions.locale, + localizePath: undefined, // Should this be configurable? + }; +} + +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types +export async function createReloadableSite(startParams: StartParams) { + const openUrlContext = await createOpenUrlContext(startParams); + + let site = await PerfLogger.async('Loading site', async () => { + const params = await createLoadSiteParams(startParams); + return loadSite(params); + }); + + const get = () => site; + + const getOpenUrl = () => + openUrlContext.getOpenUrl({ + baseUrl: site.props.baseUrl, + }); + + const printOpenUrlMessage = () => { + logger.success`Docusaurus website is running at: url=${getOpenUrl()}`; + }; + printOpenUrlMessage(); + + const reloadBase = async () => { + try { + const oldSite = site; + site = await PerfLogger.async('Reloading site', () => reloadSite(site)); + if (oldSite.props.baseUrl !== site.props.baseUrl) { + printOpenUrlMessage(); + } + } catch (e) { + logger.error('Site reload failure'); + console.error(e); + } + }; + + // TODO instead of debouncing we should rather add AbortController support? + const reload = _.debounce(reloadBase, 500); + + // TODO this could be subject to plugin reloads race conditions + // In practice, it is not likely the user will hot reload 2 plugins at once + // but we should still support it and probably use a task queuing system + const reloadPlugin = async (plugin: LoadedPlugin) => { + try { + site = await PerfLogger.async( + `Reloading site plugin ${plugin.name}@${plugin.options.id}`, + () => { + const pluginIdentifier = {name: plugin.name, id: plugin.options.id}; + return reloadSitePlugin(site, pluginIdentifier); + }, + ); + } catch (e) { + logger.error( + `Site plugin reload failure - Plugin ${plugin.name}@${plugin.options.id}`, + ); + console.error(e); + } + }; + + return {get, getOpenUrl, reload, reloadPlugin, openUrlContext}; +} diff --git a/packages/docusaurus/src/server/plugins/plugins.ts b/packages/docusaurus/src/server/plugins/plugins.ts index 13b4f59d1b00..b7cd7729c6c2 100644 --- a/packages/docusaurus/src/server/plugins/plugins.ts +++ b/packages/docusaurus/src/server/plugins/plugins.ts @@ -8,6 +8,7 @@ import path from 'path'; import _ from 'lodash'; import {docuHash, generate} from '@docusaurus/utils'; +import logger from '@docusaurus/logger'; import {initPlugins} from './init'; import {createBootstrapPlugin, createMDXFallbackPlugin} from './synthetic'; import {localizePluginTranslationFile} from '../translations/translations'; @@ -23,6 +24,7 @@ import type { InitializedPlugin, PluginRouteContext, } from '@docusaurus/types'; +import type {PluginIdentifier} from '@docusaurus/types/src/plugin'; async function translatePlugin({ plugin, @@ -270,27 +272,39 @@ export async function loadPlugins( }); } +export function getPluginByIdentifier({ + plugins, + pluginIdentifier, +}: { + pluginIdentifier: PluginIdentifier; + plugins: LoadedPlugin[]; +}): LoadedPlugin { + const plugin = plugins.find( + (p) => + p.name === pluginIdentifier.name && p.options.id === pluginIdentifier.id, + ); + if (!plugin) { + throw new Error( + logger.interpolate`Plugin not found for identifier ${pluginIdentifier.name}@${pluginIdentifier.id}`, + ); + } + return plugin; +} + export async function reloadPlugin({ - plugin, + pluginIdentifier, plugins, context, }: { - plugin: LoadedPlugin; + pluginIdentifier: PluginIdentifier; plugins: LoadedPlugin[]; context: LoadContext; }): Promise { return PerfLogger.async('Plugins - reloadPlugin', async () => { - const pluginIndex = plugins.findIndex( - (p) => p.name === plugin.name && p.options.id === plugin.options.id, - ); - if (pluginIndex === -1) { - throw new Error( - 'Unexpected: this code assumes the plugin to reload is in the list of provided plugins', - ); - } + const plugin = getPluginByIdentifier({plugins, pluginIdentifier}); const reloadedPlugin = await executePluginLoadContent({plugin, context}); - const newPlugins = plugins.with(pluginIndex, reloadedPlugin); + const newPlugins = plugins.with(plugins.indexOf(plugin), reloadedPlugin); // Unfortunately, due to the "AllContent" data we have to re-execute this // for all plugins, not just the one to reload... diff --git a/packages/docusaurus/src/server/site.ts b/packages/docusaurus/src/server/site.ts index f228e816fc94..a5148781f92f 100644 --- a/packages/docusaurus/src/server/site.ts +++ b/packages/docusaurus/src/server/site.ts @@ -30,9 +30,9 @@ import type { DocusaurusConfig, GlobalData, LoadContext, - LoadedPlugin, Props, } from '@docusaurus/types'; +import type {PluginIdentifier} from '@docusaurus/types/src/plugin'; export type LoadContextParams = { /** Usually the CWD; can be overridden with command argument. */ @@ -245,12 +245,14 @@ export async function reloadSite(site: Site): Promise { export async function reloadSitePlugin( site: Site, - plugin: LoadedPlugin, + pluginIdentifier: PluginIdentifier, ): Promise { - console.log(`reloadSitePlugin ${plugin.name}`); + console.log( + `reloadSitePlugin ${pluginIdentifier.name}@${pluginIdentifier.id}`, + ); const {plugins, routes, globalData} = await reloadPlugin({ - plugin, + pluginIdentifier, plugins: site.props.plugins, context: site.props, }); @@ -267,7 +269,7 @@ export async function reloadSitePlugin( params: site.params, }; - // TODO optimize, bypass codegen if new site is similar to old site + // TODO optimize, bypass useless codegen if new site is similar to old site await createSiteFiles({site: newSite, globalData}); return newSite; From 92d68f236c038cf956488dd007e53f0da707af39 Mon Sep 17 00:00:00 2001 From: sebastien Date: Fri, 8 Mar 2024 12:17:04 +0100 Subject: [PATCH 18/18] fix test import --- .../docusaurus-plugin-content-pages/src/__tests__/index.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/docusaurus-plugin-content-pages/src/__tests__/index.test.ts b/packages/docusaurus-plugin-content-pages/src/__tests__/index.test.ts index dd570260a417..5527f11e5ad0 100644 --- a/packages/docusaurus-plugin-content-pages/src/__tests__/index.test.ts +++ b/packages/docusaurus-plugin-content-pages/src/__tests__/index.test.ts @@ -6,7 +6,7 @@ */ import path from 'path'; -import {loadContext} from '@docusaurus/core/lib/server'; +import {loadContext} from '@docusaurus/core/src/server/site'; import {normalizePluginOptions} from '@docusaurus/utils-validation'; import pluginContentPages from '../index';