From 77662260f82a0d550f88846f2b70ad69a24841f1 Mon Sep 17 00:00:00 2001 From: Joshua Chen Date: Tue, 29 Mar 2022 16:37:29 +0800 Subject: [PATCH] refactor(core): refactor routes generation logic (#7054) * refactor(core): refactor routes generation logic * fixes --- .../src/index.d.ts | 18 +- .../src/index.ts | 20 +- .../src/utils/__tests__/routesUtils.test.ts | 4 +- .../src/utils/routesUtils.ts | 14 +- packages/docusaurus-types/src/index.d.ts | 309 ++++++++++------ .../src/__tests__/emitUtils.test.ts | 51 +-- packages/docusaurus-utils/src/emitUtils.ts | 35 +- packages/docusaurus-utils/src/index.ts | 2 +- packages/docusaurus/src/client/docusaurus.ts | 16 +- .../src/client/exports/ComponentCreator.tsx | 21 +- packages/docusaurus/src/client/flat.ts | 21 +- .../__snapshots__/routes.test.ts.snap | 35 +- .../src/server/__tests__/routes.test.ts | 53 ++- .../docusaurus/src/server/plugins/index.ts | 84 ++--- .../src/server/plugins/routeConfig.ts | 1 + packages/docusaurus/src/server/routes.ts | 333 ++++++++++-------- .../src/webpack/__tests__/utils.test.ts | 15 +- packages/docusaurus/src/webpack/utils.ts | 7 +- .../docs/api/plugin-methods/lifecycle-apis.md | 6 +- 19 files changed, 545 insertions(+), 500 deletions(-) diff --git a/packages/docusaurus-module-type-aliases/src/index.d.ts b/packages/docusaurus-module-type-aliases/src/index.d.ts index c069c7cb7adb..e8a8655a1e79 100644 --- a/packages/docusaurus-module-type-aliases/src/index.d.ts +++ b/packages/docusaurus-module-type-aliases/src/index.d.ts @@ -27,24 +27,26 @@ declare module '@generated/site-metadata' { } declare module '@generated/registry' { - const registry: { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - readonly [key: string]: [() => Promise, string, string]; - }; + import type {Registry} from '@docusaurus/types'; + + const registry: Registry; export default registry; } declare module '@generated/routes' { - import type {Route} from '@docusaurus/types'; + import type {RouteConfig as RRRouteConfig} from 'react-router-config'; - const routes: Route[]; + type RouteConfig = RRRouteConfig & { + path: string; + }; + const routes: RouteConfig[]; export default routes; } declare module '@generated/routesChunkNames' { - import type {RouteChunksTree} from '@docusaurus/types'; + import type {RouteChunkNames} from '@docusaurus/types'; - const routesChunkNames: {[route: string]: RouteChunksTree}; + const routesChunkNames: RouteChunkNames; export = routesChunkNames; } diff --git a/packages/docusaurus-plugin-content-blog/src/index.ts b/packages/docusaurus-plugin-content-blog/src/index.ts index 93cf975f8be3..c1b8d82089ef 100644 --- a/packages/docusaurus-plugin-content-blog/src/index.ts +++ b/packages/docusaurus-plugin-content-blog/src/index.ts @@ -292,19 +292,15 @@ export default async function pluginContentBlog( exact: true, modules: { sidebar: aliasedSource(sidebarProp), - items: items.map((postID) => - // To tell routes.js this is an import and not a nested object - // to recurse. - ({ - content: { - __import: true, - path: blogItemsToMetadata[postID]!.source, - query: { - truncated: true, - }, + items: items.map((postID) => ({ + content: { + __import: true, + path: blogItemsToMetadata[postID]!.source, + query: { + truncated: true, }, - }), - ), + }, + })), metadata: aliasedSource(pageMetadataPath), }, }); diff --git a/packages/docusaurus-theme-common/src/utils/__tests__/routesUtils.test.ts b/packages/docusaurus-theme-common/src/utils/__tests__/routesUtils.test.ts index a36e447d72a5..a226c13f1156 100644 --- a/packages/docusaurus-theme-common/src/utils/__tests__/routesUtils.test.ts +++ b/packages/docusaurus-theme-common/src/utils/__tests__/routesUtils.test.ts @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -import type {Route} from '@docusaurus/types'; +import type {RouteConfig} from 'react-router-config'; import {findHomePageRoute, isSamePath} from '../routesUtils'; describe('isSamePath', () => { @@ -41,7 +41,7 @@ describe('isSamePath', () => { }); describe('findHomePageRoute', () => { - const homePage: Route = { + const homePage: RouteConfig = { path: '/', exact: true, }; diff --git a/packages/docusaurus-theme-common/src/utils/routesUtils.ts b/packages/docusaurus-theme-common/src/utils/routesUtils.ts index 2b48b8bccde2..4faa24b675a0 100644 --- a/packages/docusaurus-theme-common/src/utils/routesUtils.ts +++ b/packages/docusaurus-theme-common/src/utils/routesUtils.ts @@ -8,7 +8,7 @@ import {useMemo} from 'react'; import generatedRoutes from '@generated/routes'; import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; -import type {Route} from '@docusaurus/types'; +import type {RouteConfig} from 'react-router-config'; /** * Compare the 2 paths, case insensitive and ignoring trailing slash @@ -34,18 +34,18 @@ export function findHomePageRoute({ baseUrl, routes: initialRoutes, }: { - routes: Route[]; + routes: RouteConfig[]; baseUrl: string; -}): Route | undefined { - function isHomePageRoute(route: Route): boolean { +}): RouteConfig | undefined { + function isHomePageRoute(route: RouteConfig): boolean { return route.path === baseUrl && route.exact === true; } - function isHomeParentRoute(route: Route): boolean { + function isHomeParentRoute(route: RouteConfig): boolean { return route.path === baseUrl && !route.exact; } - function doFindHomePageRoute(routes: Route[]): Route | undefined { + function doFindHomePageRoute(routes: RouteConfig[]): RouteConfig | undefined { if (routes.length === 0) { return undefined; } @@ -66,7 +66,7 @@ export function findHomePageRoute({ * Fetches the route that points to "/". Use this instead of the naive "/", * because the homepage may not exist. */ -export function useHomePageRoute(): Route | undefined { +export function useHomePageRoute(): RouteConfig | undefined { const {baseUrl} = useDocusaurusContext().siteConfig; return useMemo( () => findHomePageRoute({routes: generatedRoutes, baseUrl}), diff --git a/packages/docusaurus-types/src/index.d.ts b/packages/docusaurus-types/src/index.d.ts index 0d0db0682136..2c33f1599a3e 100644 --- a/packages/docusaurus-types/src/index.d.ts +++ b/packages/docusaurus-types/src/index.d.ts @@ -11,19 +11,42 @@ import type {CommanderStatic} from 'commander'; import type {ParsedUrlQueryInput} from 'querystring'; import type Joi from 'joi'; import type { + DeepRequired, Required as RequireKeys, DeepPartial, - DeepRequired, } from 'utility-types'; import type {Location} from 'history'; -import type Loadable from 'react-loadable'; + +// === Configuration === export type ReportingSeverity = 'ignore' | 'log' | 'warn' | 'error' | 'throw'; +export type PluginOptions = {id?: string} & {[key: string]: unknown}; + +export type PluginConfig = + | string + | [string, PluginOptions] + | [PluginModule, PluginOptions] + | PluginModule; + +export type PresetConfig = string | [string, {[key: string]: unknown}]; + export type ThemeConfig = { [key: string]: unknown; }; +export type I18nLocaleConfig = { + label: string; + htmlLang: string; + direction: string; +}; + +export type I18nConfig = { + defaultLocale: string; + locales: [string, ...string[]]; + localeConfigs: {[locale: string]: Partial}; +}; + /** * Docusaurus config, after validation/normalization. */ @@ -92,6 +115,8 @@ export type Config = RequireKeys< 'title' | 'url' | 'baseUrl' >; +// === Data loading === + /** * - `type: 'package'`, plugin is in a different package. * - `type: 'project'`, plugin is in the same docusaurus project. @@ -115,26 +140,29 @@ export type SiteMetadata = { readonly pluginVersions: {[pluginName: string]: PluginVersionInformation}; }; -// Inspired by Chrome JSON, because it's a widely supported i18n format -// https://developer.chrome.com/apps/i18n-messages -// https://support.crowdin.com/file-formats/chrome-json/ -// https://www.applanga.com/docs/formats/chrome_i18n_json -// https://docs.transifex.com/formats/chrome-json -// https://help.phrase.com/help/chrome-json-messages +/** + * Inspired by Chrome JSON, because it's a widely supported i18n format + * @see https://developer.chrome.com/apps/i18n-messages + * @see https://support.crowdin.com/file-formats/chrome-json/ + * @see https://www.applanga.com/docs/formats/chrome_i18n_json + * @see https://docs.transifex.com/formats/chrome-json + * @see https://help.phrase.com/help/chrome-json-messages + */ export type TranslationMessage = {message: string; description?: string}; export type TranslationFileContent = {[key: string]: TranslationMessage}; -export type TranslationFile = {path: string; content: TranslationFileContent}; - -export type I18nLocaleConfig = { - label: string; - htmlLang: string; - direction: string; -}; - -export type I18nConfig = { - defaultLocale: string; - locales: [string, ...string[]]; - localeConfigs: {[locale: string]: Partial}; +/** + * An abstract representation of how a translation file exists on disk. The core + * would handle the file reading/writing; plugins just need to deal with + * translations in-memory. + */ +export type TranslationFile = { + /** + * Relative to the directory where it's expected to be found. For plugin + * files, it's relative to `i18n///`. Should NOT + * have any extension. + */ + path: string; + content: TranslationFileContent; }; export type I18n = DeepRequired & {currentLocale: string}; @@ -153,21 +181,6 @@ export type DocusaurusContext = { // isBrowser: boolean; // Not here on purpose! }; -export type Preset = { - plugins?: PluginConfig[]; - themes?: PluginConfig[]; -}; - -export type PresetModule = { - (context: LoadContext, presetOptions: T): Preset; -}; - -export type ImportedPresetModule = PresetModule & { - default?: PresetModule; -}; - -export type PresetConfig = string | [string, {[key: string]: unknown}]; - export type HostPortCLIOptions = { host?: string; port?: string; @@ -191,14 +204,11 @@ export type ServeCLIOptions = HostPortCLIOptions & build: boolean; }; -export type BuildOptions = ConfigOptions & { +export type BuildCLIOptions = ConfigOptions & { bundleAnalyzer: boolean; outDir: string; minify: boolean; skipBuild: boolean; -}; - -export type BuildCLIOptions = BuildOptions & { locale?: string; }; @@ -219,8 +229,6 @@ export type LoadContext = { codeTranslations: {[msgId: string]: string}; }; -export type HtmlTags = string | HtmlTagObject | (string | HtmlTagObject)[]; - export type Props = LoadContext & { readonly headTags: string; readonly preBodyTags: string; @@ -231,12 +239,27 @@ export type Props = LoadContext & { readonly plugins: LoadedPlugin[]; }; +// === Plugin === + export type PluginContentLoadedActions = { addRoute: (config: RouteConfig) => void; createData: (name: string, data: string) => Promise; setGlobalData: (data: unknown) => void; }; +export type ConfigureWebpackUtils = { + getStyleLoaders: ( + isServer: boolean, + cssOptions: { + [key: string]: unknown; + }, + ) => RuleSetRule[]; + getJSLoader: (options: { + isServer: boolean; + babelOptions?: {[key: string]: unknown}; + }) => RuleSetRule; +}; + export type AllContent = { [pluginName: string]: { [pluginID: string]: unknown; @@ -246,6 +269,37 @@ export type AllContent = { // TODO improve type (not exposed by postcss-loader) export type PostCssOptions = {[key: string]: unknown} & {plugins: unknown[]}; +type HtmlTagObject = { + /** + * Attributes of the html tag. + * E.g. `{ disabled: true, value: "demo", rel: "preconnect" }` + */ + attributes?: Partial<{[key: string]: string | boolean}>; + /** The tag name, e.g. `div`, `script`, `link`, `meta` */ + tagName: string; + /** The inner HTML */ + innerHTML?: string; +}; + +export type HtmlTags = string | HtmlTagObject | (string | HtmlTagObject)[]; + +export type ValidationSchema = Joi.ObjectSchema; + +export type Validate = ( + validationSchema: ValidationSchema, + options: T, +) => U; + +export type OptionValidationContext = { + validate: Validate; + options: T; +}; + +export type ThemeConfigValidationContext = { + validate: Validate; + themeConfig: Partial; +}; + export type Plugin = { name: string; loadContent?: () => Promise; @@ -266,7 +320,9 @@ export type Plugin = { utils: ConfigureWebpackUtils, content: Content, ) => WebpackConfiguration & { - mergeStrategy?: ConfigureWebpackFnMergeStrategy; + mergeStrategy?: { + [key: string]: CustomizeRuleString; + }; }; configurePostCss?: (options: PostCssOptions) => PostCssOptions; getThemePath?: () => string; @@ -334,9 +390,7 @@ export type NormalizedPluginConfig = { export type InitializedPlugin = Plugin & { readonly options: Required; readonly version: PluginVersionInformation; - /** - * The absolute path to the folder containing the entry point file. - */ + /** The absolute path to the folder containing the entry point file. */ readonly path: string; }; @@ -372,48 +426,71 @@ export type ImportedPluginModule = PluginModule & { default?: PluginModule; }; -export type ConfigureWebpackFn = Plugin['configureWebpack']; -export type ConfigureWebpackFnMergeStrategy = { - [key: string]: CustomizeRuleString; +export type Preset = { + plugins?: PluginConfig[]; + themes?: PluginConfig[]; }; -export type ConfigurePostCssFn = Plugin['configurePostCss']; - -export type PluginOptions = {id?: string} & {[key: string]: unknown}; -export type PluginConfig = - | string - | [string, PluginOptions] - | [PluginModule, PluginOptions] - | PluginModule; +export type PresetModule = { + (context: LoadContext, presetOptions: T): Preset; +}; -export type ChunkRegistry = { - loader: string; - modulePath: string; +export type ImportedPresetModule = PresetModule & { + default?: PresetModule; }; +// === Route registry === + +/** + * A "module" represents a unit of serialized data emitted from the plugin. It + * will be imported on client-side and passed as props, context, etc. + * + * If it's a string, it's a file path that Webpack can `require`; if it's + * an object, it can also contain `query` or other metadata. + */ export type Module = | { - path: string; + /** + * A marker that tells the route generator this is an import and not a + * nested object to recurse. + */ __import?: boolean; + path: string; query?: ParsedUrlQueryInput; } | string; -export type RouteModule = { - [module: string]: Module | RouteModule | RouteModule[]; -}; - -export type ChunkNames = { - [name: string]: string | null | ChunkNames | ChunkNames[]; +/** + * Represents the data attached to each route. Since the routes.js is a + * monolithic data file, any data (like props) should be serialized separately + * and registered here as file paths (a {@link Module}), so that we could + * code-split. + */ +export type RouteModules = { + [propName: string]: Module | RouteModules | RouteModules[]; }; +/** + * Represents a "slice" of the final route structure returned from the plugin + * `addRoute` action. + */ export type RouteConfig = { + /** With leading slash. Trailing slash will be normalized by config. */ path: string; + /** Component used to render this route, a path that Webpack can `require`. */ component: string; - modules?: RouteModule; + /** + * Props. Each entry should be `[propName]: pathToPropModule` (created with + * `createData`) + */ + modules?: RouteModules; + /** Nested routes config. */ routes?: RouteConfig[]; + /** React router config option: `exact` routes would not match subroutes. */ exact?: boolean; + /** Used to sort routes. Higher-priority routes will be placed first. */ priority?: number; + /** Extra props; will be copied to routes.js. */ [propName: string]: unknown; }; @@ -435,60 +512,64 @@ export type PluginRouteContext = RouteContext & { }; }; -export type Route = { - readonly path: string; - readonly component: ReturnType; - readonly exact?: boolean; - readonly routes?: Route[]; -}; - /** - * Aliases used for Webpack resolution (useful for implementing swizzling) + * The shape would be isomorphic to {@link RouteModules}: + * {@link Module} -> `string`, `RouteModules[]` -> `ChunkNames[]`. + * + * Each `string` chunk name will correlate with one key in the {@link Registry}. */ -export type ThemeAliases = { - [alias: string]: string; -}; - -export type ConfigureWebpackUtils = { - getStyleLoaders: ( - isServer: boolean, - cssOptions: { - [key: string]: unknown; - }, - ) => RuleSetRule[]; - getJSLoader: (options: { - isServer: boolean; - babelOptions?: {[key: string]: unknown}; - }) => RuleSetRule; +export type ChunkNames = { + [propName: string]: string | ChunkNames | ChunkNames[]; }; -type HtmlTagObject = { - /** - * Attributes of the html tag. - * E.g. `{ disabled: true, value: "demo", rel: "preconnect" }` - */ - attributes?: Partial<{[key: string]: string | boolean}>; - /** The tag name, e.g. `div`, `script`, `link`, `meta` */ - tagName: string; - /** The inner HTML */ - innerHTML?: string; +/** + * A map from route paths (with a hash) to the chunk names of each module, which + * the bundler will collect. + * + * Chunk keys are routes with a hash, because 2 routes can conflict with each + * other if they have the same path, e.g.: parent=/docs, child=/docs + * + * @see https://github.com/facebook/docusaurus/issues/2917 + */ +export type RouteChunkNames = { + [routePathHashed: string]: ChunkNames; }; -export type ValidationSchema = Joi.ObjectSchema; - -export type Validate = ( - validationSchema: ValidationSchema, - options: T, -) => U; - -export type OptionValidationContext = { - validate: Validate; - options: T; +/** + * Each key is the chunk name, which you can get from `routeChunkNames` (see + * {@link RouteChunkNames}). The values are the opts data that react-loadable + * needs. For example: + * + * ```js + * const options = { + * optsLoader: { + * component: () => import('./Pages.js'), + * content.foo: () => import('./doc1.md'), + * }, + * optsModules: ['./Pages.js', './doc1.md'], + * optsWebpack: [ + * require.resolveWeak('./Pages.js'), + * require.resolveWeak('./doc1.md'), + * ], + * } + * ``` + * + * @see https://github.com/jamiebuilds/react-loadable#declaring-which-modules-are-being-loaded + */ +export type Registry = { + readonly [chunkName: string]: [ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + Loader: () => Promise, + ModuleName: string, + ResolvedModuleName: string, + ]; }; -export type ThemeConfigValidationContext = { - validate: Validate; - themeConfig: Partial; +/** + * Aliases used for Webpack resolution (useful for implementing swizzling) + */ +export type ThemeAliases = { + [alias: string]: string; }; export type TOCItem = { @@ -497,8 +578,6 @@ export type TOCItem = { readonly level: number; }; -export type RouteChunksTree = {[x: string | number]: string | RouteChunksTree}; - export type ClientModule = { onRouteUpdate?: (args: { previousLocation: Location | null; diff --git a/packages/docusaurus-utils/src/__tests__/emitUtils.test.ts b/packages/docusaurus-utils/src/__tests__/emitUtils.test.ts index 2fe8cb7d34e3..e5f975abb9f0 100644 --- a/packages/docusaurus-utils/src/__tests__/emitUtils.test.ts +++ b/packages/docusaurus-utils/src/__tests__/emitUtils.test.ts @@ -6,59 +6,10 @@ */ import {jest} from '@jest/globals'; -import {genChunkName, readOutputHTMLFile, generate} from '../emitUtils'; +import {readOutputHTMLFile, generate} from '../emitUtils'; import path from 'path'; import fs from 'fs-extra'; -describe('genChunkName', () => { - it('works', () => { - const firstAssert: {[key: string]: string} = { - '/docs/adding-blog': 'docs-adding-blog-062', - '/docs/versioning': 'docs-versioning-8a8', - '/': 'index', - '/blog/2018/04/30/How-I-Converted-Profilo-To-Docusaurus': - 'blog-2018-04-30-how-i-converted-profilo-to-docusaurus-4f2', - '/youtube': 'youtube-429', - '/users/en/': 'users-en-f7a', - '/blog': 'blog-c06', - }; - Object.keys(firstAssert).forEach((str) => { - expect(genChunkName(str)).toBe(firstAssert[str]); - }); - }); - - it("doesn't allow different chunk name for same path", () => { - expect(genChunkName('path/is/similar', 'oldPrefix')).toEqual( - genChunkName('path/is/similar', 'newPrefix'), - ); - }); - - it('emits different chunk names for different paths even with same preferred name', () => { - const secondAssert: {[key: string]: string} = { - '/blog/1': 'blog-85-f-089', - '/blog/2': 'blog-353-489', - }; - Object.keys(secondAssert).forEach((str) => { - expect(genChunkName(str, undefined, 'blog')).toBe(secondAssert[str]); - }); - }); - - it('only generates short unique IDs', () => { - const thirdAssert: {[key: string]: string} = { - a: '0cc175b9', - b: '92eb5ffe', - c: '4a8a08f0', - d: '8277e091', - }; - Object.keys(thirdAssert).forEach((str) => { - expect(genChunkName(str, undefined, undefined, true)).toBe( - thirdAssert[str], - ); - }); - expect(genChunkName('d', undefined, undefined, true)).toBe('8277e091'); - }); -}); - describe('readOutputHTMLFile', () => { it('trailing slash undefined', async () => { await expect( diff --git a/packages/docusaurus-utils/src/emitUtils.ts b/packages/docusaurus-utils/src/emitUtils.ts index ba4e29de0832..89680f92ad08 100644 --- a/packages/docusaurus-utils/src/emitUtils.ts +++ b/packages/docusaurus-utils/src/emitUtils.ts @@ -8,7 +8,6 @@ import path from 'path'; import fs from 'fs-extra'; import {createHash} from 'crypto'; -import {simpleHash, docuHash} from './hashUtils'; import {findAsyncSequential} from './jsUtils'; const fileHash = new Map(); @@ -18,7 +17,8 @@ const fileHash = new Map(); * differs from cache (for hot reload performance). * * @param generatedFilesDir Absolute path. - * @param file Path relative to `generatedFilesDir`. + * @param file Path relative to `generatedFilesDir`. File will always be + * outputted; no need to ensure directory exists. * @param content String content to write. * @param skipCache If `true` (defaults as `true` for production), file is * force-rewritten, skipping cache. @@ -29,7 +29,7 @@ export async function generate( content: string, skipCache: boolean = process.env.NODE_ENV === 'production', ): Promise { - const filepath = path.join(generatedFilesDir, file); + const filepath = path.resolve(generatedFilesDir, file); if (skipCache) { await fs.outputFile(filepath, content); @@ -62,35 +62,6 @@ export async function generate( } } -const chunkNameCache = new Map(); - -/** - * Generate unique chunk name given a module path. - */ -export function genChunkName( - modulePath: string, - prefix?: string, - preferredName?: string, - shortId: boolean = process.env.NODE_ENV === 'production', -): string { - let chunkName = chunkNameCache.get(modulePath); - if (!chunkName) { - if (shortId) { - chunkName = simpleHash(modulePath, 8); - } else { - let str = modulePath; - if (preferredName) { - const shortHash = simpleHash(modulePath, 3); - str = `${preferredName}${shortHash}`; - } - const name = str === '/' ? 'index' : docuHash(str); - chunkName = prefix ? `${prefix}---${name}` : name; - } - chunkNameCache.set(modulePath, chunkName); - } - return chunkName; -} - /** * @param permalink The URL that the HTML file corresponds to, without base URL * @param outDir Full path to the output directory diff --git a/packages/docusaurus-utils/src/index.ts b/packages/docusaurus-utils/src/index.ts index 999cc1928f08..7cd871a631e8 100644 --- a/packages/docusaurus-utils/src/index.ts +++ b/packages/docusaurus-utils/src/index.ts @@ -22,7 +22,7 @@ export { DEFAULT_PLUGIN_ID, WEBPACK_URL_LOADER_LIMIT, } from './constants'; -export {generate, genChunkName, readOutputHTMLFile} from './emitUtils'; +export {generate, readOutputHTMLFile} from './emitUtils'; export { getFileCommitDate, FileNotTrackedError, diff --git a/packages/docusaurus/src/client/docusaurus.ts b/packages/docusaurus/src/client/docusaurus.ts index 20923a84d48e..1ee1605b1a35 100644 --- a/packages/docusaurus/src/client/docusaurus.ts +++ b/packages/docusaurus/src/client/docusaurus.ts @@ -34,20 +34,16 @@ const canPrefetch = (routePath: string) => const canPreload = (routePath: string) => !isSlowConnection() && !loaded[routePath]; -// Remove the last part containing the route hash -// input: /blog/2018/12/14/Happy-First-Birthday-Slash-fe9 -// output: /blog/2018/12/14/Happy-First-Birthday-Slash -const removeRouteNameHash = (str: string) => str.replace(/-[^-]+$/, ''); - const getChunkNamesToLoad = (path: string): string[] => Object.entries(routesChunkNames) .filter( - ([routeNameWithHash]) => removeRouteNameHash(routeNameWithHash) === path, + // Remove the last part containing the route hash + // input: /blog/2018/12/14/Happy-First-Birthday-Slash-fe9 + // output: /blog/2018/12/14/Happy-First-Birthday-Slash + ([routeNameWithHash]) => + routeNameWithHash.replace(/-[^-]+$/, '') === path, ) - .flatMap(([, routeChunks]) => - // flat() is useful for nested chunk names, it's not like array.flat() - Object.values(flat(routeChunks)), - ); + .flatMap(([, routeChunks]) => Object.values(flat(routeChunks))); const docusaurus = { prefetch: (routePath: string): boolean => { diff --git a/packages/docusaurus/src/client/exports/ComponentCreator.tsx b/packages/docusaurus/src/client/exports/ComponentCreator.tsx index 9d59a19492be..86c52a213ed3 100644 --- a/packages/docusaurus/src/client/exports/ComponentCreator.tsx +++ b/packages/docusaurus/src/client/exports/ComponentCreator.tsx @@ -34,27 +34,12 @@ export default function ComponentCreator( }); } - const chunkNamesKey = `${path}-${hash}`; - const chunkNames = routesChunkNames[chunkNamesKey]!; - const optsModules: string[] = []; - const optsWebpack: string[] = []; + const chunkNames = routesChunkNames[`${path}-${hash}`]!; // eslint-disable-next-line @typescript-eslint/no-explicit-any const optsLoader: {[key: string]: () => Promise} = {}; + const optsModules: string[] = []; + const optsWebpack: string[] = []; - /* Prepare opts data that react-loadable needs - https://github.com/jamiebuilds/react-loadable#declaring-which-modules-are-being-loaded - Example: - - optsLoader: - { - component: () => import('./Pages.js'), - content.foo: () => import('./doc1.md'), - } - - optsModules: ['./Pages.js', './doc1.md'] - - optsWebpack: [ - require.resolveWeak('./Pages.js'), - require.resolveWeak('./doc1.md'), - ] - */ const flatChunkNames = flat(chunkNames); Object.entries(flatChunkNames).forEach(([key, chunkName]) => { const chunkRegistry = registry[chunkName]; diff --git a/packages/docusaurus/src/client/flat.ts b/packages/docusaurus/src/client/flat.ts index 76b2f57a378c..c015d106955d 100644 --- a/packages/docusaurus/src/client/flat.ts +++ b/packages/docusaurus/src/client/flat.ts @@ -5,18 +5,27 @@ * LICENSE file in the root directory of this source tree. */ -import type {RouteChunksTree} from '@docusaurus/types'; +import type {ChunkNames} from '@docusaurus/types'; -const isTree = (x: string | RouteChunksTree): x is RouteChunksTree => +type Chunk = ChunkNames[string]; +type Tree = Exclude; + +const isTree = (x: Chunk): x is Tree => typeof x === 'object' && !!x && Object.keys(x).length > 0; -export default function flat(target: RouteChunksTree): { - [keyPath: string]: string; -} { +/** + * Takes a tree, and flattens it into a map of keyPath -> value. + * + * ```js + * flat({ a: { b: 1 } }) === { "a.b": 1 }; + * flat({ a: [1, 2] }) === { "a.0": 1, "a.1": 2 }; + * ``` + */ +export default function flat(target: ChunkNames): {[keyPath: string]: string} { const delimiter = '.'; const output: {[keyPath: string]: string} = {}; - function step(object: RouteChunksTree, prefix?: string | number) { + function step(object: Tree, prefix?: string | number) { Object.entries(object).forEach(([key, value]) => { const newKey = prefix ? `${prefix}${delimiter}${key}` : key; diff --git a/packages/docusaurus/src/server/__tests__/__snapshots__/routes.test.ts.snap b/packages/docusaurus/src/server/__tests__/__snapshots__/routes.test.ts.snap index 7870bab42380..a69dd7b18c1a 100644 --- a/packages/docusaurus/src/server/__tests__/__snapshots__/routes.test.ts.snap +++ b/packages/docusaurus/src/server/__tests__/__snapshots__/routes.test.ts.snap @@ -30,7 +30,7 @@ exports[`loadRoutes loads flat route config 1`] = ` }, }, "routesChunkNames": { - "/blog-1e7": { + "/blog-599": { "component": "component---theme-blog-list-pagea-6-a-7ba", "items": [ { @@ -39,29 +39,26 @@ exports[`loadRoutes loads flat route config 1`] = ` }, { "content": "content---blog-7-b-8-fd9", - "metadata": null, }, { "content": "content---blog-7-b-8-fd9", - "metadata": null, }, ], }, }, - "routesConfig": " -import React from 'react'; + "routesConfig": "import React from 'react'; import ComponentCreator from '@docusaurus/ComponentCreator'; export default [ { path: '/blog', - component: ComponentCreator('/blog','1e7'), + component: ComponentCreator('/blog', '599'), exact: true }, { path: '*', - component: ComponentCreator('*') - } + component: ComponentCreator('*'), + }, ]; ", "routesPaths": [ @@ -119,24 +116,23 @@ exports[`loadRoutes loads nested route config 1`] = ` "metadata": "metadata---docs-foo-baz-2-cf-fa7", }, }, - "routesConfig": " -import React from 'react'; + "routesConfig": "import React from 'react'; import ComponentCreator from '@docusaurus/ComponentCreator'; export default [ { path: '/docs:route', - component: ComponentCreator('/docs:route','52d'), + component: ComponentCreator('/docs:route', '52d'), routes: [ { path: '/docs/hello', - component: ComponentCreator('/docs/hello','44b'), + component: ComponentCreator('/docs/hello', '44b'), exact: true, sidebar: \\"main\\" }, { path: 'docs/foo/baz', - component: ComponentCreator('docs/foo/baz','070'), + component: ComponentCreator('docs/foo/baz', '070'), sidebar: \\"secondary\\", \\"key:a\\": \\"containing colon\\", \\"key'b\\": \\"containing quote\\", @@ -148,8 +144,8 @@ export default [ }, { path: '*', - component: ComponentCreator('*') - } + component: ComponentCreator('*'), + }, ]; ", "routesPaths": [ @@ -173,19 +169,18 @@ exports[`loadRoutes loads route config with empty (but valid) path string 1`] = "component": "component---hello-world-jse-0-f-b6c", }, }, - "routesConfig": " -import React from 'react'; + "routesConfig": "import React from 'react'; import ComponentCreator from '@docusaurus/ComponentCreator'; export default [ { path: '', - component: ComponentCreator('','b2a') + component: ComponentCreator('', 'b2a') }, { path: '*', - component: ComponentCreator('*') - } + component: ComponentCreator('*'), + }, ]; ", "routesPaths": [ diff --git a/packages/docusaurus/src/server/__tests__/routes.test.ts b/packages/docusaurus/src/server/__tests__/routes.test.ts index acac972d0f19..faf37563fd94 100644 --- a/packages/docusaurus/src/server/__tests__/routes.test.ts +++ b/packages/docusaurus/src/server/__tests__/routes.test.ts @@ -6,9 +6,58 @@ */ import {jest} from '@jest/globals'; -import {loadRoutes, handleDuplicateRoutes} from '../routes'; +import {loadRoutes, handleDuplicateRoutes, genChunkName} from '../routes'; import type {RouteConfig} from '@docusaurus/types'; +describe('genChunkName', () => { + it('works', () => { + const firstAssert: {[key: string]: string} = { + '/docs/adding-blog': 'docs-adding-blog-062', + '/docs/versioning': 'docs-versioning-8a8', + '/': 'index', + '/blog/2018/04/30/How-I-Converted-Profilo-To-Docusaurus': + 'blog-2018-04-30-how-i-converted-profilo-to-docusaurus-4f2', + '/youtube': 'youtube-429', + '/users/en/': 'users-en-f7a', + '/blog': 'blog-c06', + }; + Object.keys(firstAssert).forEach((str) => { + expect(genChunkName(str)).toBe(firstAssert[str]); + }); + }); + + it("doesn't allow different chunk name for same path", () => { + expect(genChunkName('path/is/similar', 'oldPrefix')).toEqual( + genChunkName('path/is/similar', 'newPrefix'), + ); + }); + + it('emits different chunk names for different paths even with same preferred name', () => { + const secondAssert: {[key: string]: string} = { + '/blog/1': 'blog-85-f-089', + '/blog/2': 'blog-353-489', + }; + Object.keys(secondAssert).forEach((str) => { + expect(genChunkName(str, undefined, 'blog')).toBe(secondAssert[str]); + }); + }); + + it('only generates short unique IDs', () => { + const thirdAssert: {[key: string]: string} = { + a: '0cc175b9', + b: '92eb5ffe', + c: '4a8a08f0', + d: '8277e091', + }; + Object.keys(thirdAssert).forEach((str) => { + expect(genChunkName(str, undefined, undefined, true)).toBe( + thirdAssert[str], + ); + }); + expect(genChunkName('d', undefined, undefined, true)).toBe('8277e091'); + }); +}); + describe('handleDuplicateRoutes', () => { const routes: RouteConfig[] = [ { @@ -110,14 +159,12 @@ describe('loadRoutes', () => { }, { content: 'blog/2018-12-14-Happy-First-Birthday-Slash.md', - metadata: null, }, { content: { __import: true, path: 'blog/2018-12-14-Happy-First-Birthday-Slash.md', }, - metadata: null, }, ], }, diff --git a/packages/docusaurus/src/server/plugins/index.ts b/packages/docusaurus/src/server/plugins/index.ts index 7484ae966669..9a253211ffbe 100644 --- a/packages/docusaurus/src/server/plugins/index.ts +++ b/packages/docusaurus/src/server/plugins/index.ts @@ -6,7 +6,6 @@ */ import {docuHash, generate} from '@docusaurus/utils'; -import fs from 'fs-extra'; import path from 'path'; import type { LoadContext, @@ -28,7 +27,8 @@ import {applyRouteTrailingSlash, sortConfig} from './routeConfig'; /** * Initializes the plugins, runs `loadContent`, `translateContent`, - * `contentLoaded`, and `translateThemeConfig`. + * `contentLoaded`, and `translateThemeConfig`. Because `contentLoaded` is + * side-effect-ful (it generates temp files), so is this function. */ export async function loadPlugins(context: LoadContext): Promise<{ plugins: LoadedPlugin[]; @@ -99,65 +99,53 @@ export async function loadPlugins(context: LoadContext): Promise<{ if (!plugin.contentLoaded) { return; } - const pluginId = plugin.options.id; - // plugins data files are namespaced by pluginName/pluginId - const dataDirRoot = path.join(context.generatedFilesDir, plugin.name); - const dataDir = path.join(dataDirRoot, pluginId); - - const createData: PluginContentLoadedActions['createData'] = async ( - name, - data, - ) => { - const modulePath = path.join(dataDir, name); - await fs.ensureDir(path.dirname(modulePath)); - await generate(dataDir, name, data); - return modulePath; - }; - + const dataDir = path.join( + context.generatedFilesDir, + plugin.name, + pluginId, + ); // TODO this would be better to do all that in the codegen phase // TODO handle context for nested routes const pluginRouteContext: PluginRouteContext = { plugin: {name: plugin.name, id: pluginId}, data: undefined, // TODO allow plugins to provide context data }; - const pluginRouteContextModulePath = await createData( + const pluginRouteContextModulePath = path.join( + dataDir, `${docuHash('pluginRouteContextModule')}.json`, + ); + await generate( + '/', + pluginRouteContextModulePath, JSON.stringify(pluginRouteContext, null, 2), ); - const addRoute: PluginContentLoadedActions['addRoute'] = ( - initialRouteConfig, - ) => { - // Trailing slash behavior is handled in a generic way for all plugins - const finalRouteConfig = applyRouteTrailingSlash(initialRouteConfig, { - trailingSlash: context.siteConfig.trailingSlash, - baseUrl: context.siteConfig.baseUrl, - }); - pluginsRouteConfigs.push({ - ...finalRouteConfig, - modules: { - ...finalRouteConfig.modules, - __routeContextModule: pluginRouteContextModulePath, - }, - }); - }; - - // the plugins global data are namespaced to avoid data conflicts: - // - by plugin name - // - by plugin id (allow using multiple instances of the same plugin) - const setGlobalData: PluginContentLoadedActions['setGlobalData'] = ( - data, - ) => { - globalData[plugin.name] = globalData[plugin.name] ?? {}; - globalData[plugin.name]![pluginId] = data; - }; - const actions: PluginContentLoadedActions = { - addRoute, - createData, - setGlobalData, + addRoute(initialRouteConfig) { + // Trailing slash behavior is handled generically for all plugins + const finalRouteConfig = applyRouteTrailingSlash( + initialRouteConfig, + context.siteConfig, + ); + pluginsRouteConfigs.push({ + ...finalRouteConfig, + modules: { + ...finalRouteConfig.modules, + __routeContextModule: 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; + }, }; const translatedContent = diff --git a/packages/docusaurus/src/server/plugins/routeConfig.ts b/packages/docusaurus/src/server/plugins/routeConfig.ts index 63fad7cd52f6..e5664dd1693f 100644 --- a/packages/docusaurus/src/server/plugins/routeConfig.ts +++ b/packages/docusaurus/src/server/plugins/routeConfig.ts @@ -11,6 +11,7 @@ import { type ApplyTrailingSlashParams, } from '@docusaurus/utils-common'; +/** Recursively applies trailing slash config to all nested routes. */ export function applyRouteTrailingSlash( route: RouteConfig, params: ApplyTrailingSlashParams, diff --git a/packages/docusaurus/src/server/routes.ts b/packages/docusaurus/src/server/routes.ts index 39affe45a31d..48fe2722d4ac 100644 --- a/packages/docusaurus/src/server/routes.ts +++ b/packages/docusaurus/src/server/routes.ts @@ -6,34 +6,90 @@ */ import { - genChunkName, + docuHash, normalizeUrl, - removeSuffix, simpleHash, escapePath, reportMessage, } from '@docusaurus/utils'; -import {stringify} from 'querystring'; +import _ from 'lodash'; +import query from 'querystring'; import {getAllFinalRoutes} from './utils'; import type { - ChunkRegistry, Module, RouteConfig, - RouteModule, + RouteModules, ChunkNames, + RouteChunkNames, ReportingSeverity, } from '@docusaurus/types'; -type RegistryMap = { - [chunkName: string]: ChunkRegistry; +type LoadedRoutes = { + /** Serialized routes config that can be directly emitted into temp file. */ + routesConfig: string; + /** @see {ChunkNames} */ + routesChunkNames: RouteChunkNames; + /** A map from chunk name to module loaders. */ + registry: { + [chunkName: string]: {loader: string; modulePath: 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. */ function indent(str: string) { - const spaces = ' '; - return `${spaces}${str.replace(/\n/g, `\n${spaces}`)}`; + return ` ${str.replace(/\n/g, `\n `)}`; } -function createRouteCodeString({ +const chunkNameCache = new Map(); + +/** + * Generates a unique chunk name that can be used in the chunk registry. + * + * @param modulePath A path to generate chunk name from. The actual value has no + * semantic significance. + * @param prefix A prefix to append to the chunk name, to avoid name clash. + * @param preferredName Chunk names default to `modulePath`, and this can supply + * a more human-readable name. + * @param shortId When `true`, the chunk name would only be a hash without any + * other characters. Useful for bundle size. Defaults to `true` in production. + */ +export function genChunkName( + modulePath: string, + prefix?: string, + preferredName?: string, + shortId: boolean = process.env.NODE_ENV === 'production', +): string { + let chunkName = chunkNameCache.get(modulePath); + if (!chunkName) { + if (shortId) { + chunkName = simpleHash(modulePath, 8); + } else { + let str = modulePath; + if (preferredName) { + const shortHash = simpleHash(modulePath, 3); + str = `${preferredName}${shortHash}`; + } + const name = str === '/' ? 'index' : docuHash(str); + chunkName = prefix ? `${prefix}---${name}` : name; + } + chunkNameCache.set(modulePath, chunkName); + } + return chunkName; +} + +/** + * Takes a piece of route config, and serializes it into raw JS code. The shape + * is the same as react-router's `RouteConfig`. Formatting is similar to + * `JSON.stringify` but without all the quotes. + */ +function serializeRouteConfig({ routePath, routeHash, exact, @@ -48,7 +104,7 @@ function createRouteCodeString({ }) { const parts = [ `path: '${routePath}'`, - `component: ComponentCreator('${routePath}','${routeHash}')`, + `component: ComponentCreator('${routePath}', '${routeHash}')`, ]; if (exact) { @@ -58,7 +114,7 @@ function createRouteCodeString({ if (subroutesCodeStrings) { parts.push( `routes: [ -${indent(removeSuffix(subroutesCodeStrings.join(',\n'), ',\n'))} +${indent(subroutesCodeStrings.join(',\n'))} ]`, ); } @@ -89,96 +145,67 @@ ${indent(parts.join(',\n'))} }`; } -const NotFoundRouteCode = `{ - path: '*', - component: ComponentCreator('*') -}`; - -const RoutesImportsCode = [ - `import React from 'react';`, - `import ComponentCreator from '@docusaurus/ComponentCreator';`, -].join('\n'); - -function isModule(value: unknown): value is Module { - if (typeof value === 'string') { - return true; - } - if ( - typeof value === 'object' && +const isModule = (value: unknown): value is Module => + typeof value === 'string' || + (typeof value === 'object' && // eslint-disable-next-line no-underscore-dangle - (value as {[key: string]: unknown})?.__import && - (value as {[key: string]: unknown})?.path - ) { - return true; - } - return false; -} + !!(value as {[key: string]: unknown})?.__import); +/** Takes a {@link Module} and returns the string path it represents. */ function getModulePath(target: Module): string { if (typeof target === 'string') { return target; } - const queryStr = target.query ? `?${stringify(target.query)}` : ''; + const queryStr = target.query ? `?${query.stringify(target.query)}` : ''; return `${target.path}${queryStr}`; } -function genRouteChunkNames( - registry: RegistryMap, - value: Module, - prefix?: string, - name?: string, -): string; -function genRouteChunkNames( - registry: RegistryMap, - value: RouteModule, - prefix?: string, - name?: string, +/** + * Takes a route module (which is a tree of modules), and transforms each module + * into a chunk name. It also mutates `res.registry` and registers the loaders + * for each chunk. + * + * @param routeModule One route module to be transformed. + * @param prefix Prefix passed to {@link genChunkName}. + * @param name Preferred name passed to {@link genChunkName}. + * @param res The route structures being loaded. + */ +function genChunkNames( + routeModule: RouteModules, + prefix: string, + name: string, + res: LoadedRoutes, ): ChunkNames; -function genRouteChunkNames( - registry: RegistryMap, - value: RouteModule[], - prefix?: string, - name?: string, -): ChunkNames[]; -function genRouteChunkNames( - registry: RegistryMap, - value: RouteModule | RouteModule[] | Module, - prefix?: string, - name?: string, +function genChunkNames( + routeModule: RouteModules | RouteModules[] | Module, + prefix: string, + name: string, + res: LoadedRoutes, ): ChunkNames | ChunkNames[] | string; -function genRouteChunkNames( - // TODO instead of passing a mutating the registry, return a registry slice? - registry: RegistryMap, - value: RouteModule | RouteModule[] | Module | null | undefined, - prefix?: string, - name?: string, -): null | string | ChunkNames | ChunkNames[] { - if (!value) { - return null; - } - - if (Array.isArray(value)) { - return value.map((val, index) => - genRouteChunkNames(registry, val, `${index}`, name), - ); - } - - if (isModule(value)) { - const modulePath = getModulePath(value); +function genChunkNames( + routeModule: RouteModules | RouteModules[] | Module, + prefix: string, + name: string, + res: LoadedRoutes, +): string | ChunkNames | ChunkNames[] { + if (isModule(routeModule)) { + // This is a leaf node, no need to recurse + const modulePath = getModulePath(routeModule); const chunkName = genChunkName(modulePath, prefix, name); - const loader = `() => import(/* webpackChunkName: '${chunkName}' */ '${escapePath( + res.registry[chunkName] = { + loader: `() => import(/* webpackChunkName: '${chunkName}' */ '${escapePath( + modulePath, + )}')`, modulePath, - )}')`; - - registry[chunkName] = {loader, modulePath}; + }; return chunkName; } - - const newValue: ChunkNames = {}; - Object.entries(value).forEach(([key, v]) => { - newValue[key] = genRouteChunkNames(registry, v, key, name); - }); - return newValue; + if (Array.isArray(routeModule)) { + return routeModule.map((val, index) => + genChunkNames(val, `${index}`, name, res), + ); + } + return _.mapValues(routeModule, (v, key) => genChunkNames(v, key, name, res)); } export function handleDuplicateRoutes( @@ -212,80 +239,82 @@ This could lead to non-deterministic routing behavior.`; } } -export async function loadRoutes( - pluginsRouteConfigs: RouteConfig[], - baseUrl: string, - onDuplicateRoutes: ReportingSeverity, -): Promise<{ - registry: {[chunkName: string]: ChunkRegistry}; - routesConfig: string; - routesChunkNames: {[routePath: string]: ChunkNames}; - routesPaths: string[]; -}> { - handleDuplicateRoutes(pluginsRouteConfigs, onDuplicateRoutes); - const registry: {[chunkName: string]: ChunkRegistry} = {}; - const routesPaths: string[] = [normalizeUrl([baseUrl, '404.html'])]; - const routesChunkNames: {[routePath: string]: ChunkNames} = {}; - - // This is the higher level overview of route code generation. - function generateRouteCode(routeConfig: RouteConfig): string { - const { - path: routePath, - component, - modules = {}, - routes: subroutes, - exact, - priority, - ...props - } = routeConfig; +/** + * This is the higher level overview of route code generation. For each route + * config node, it return the node's serialized form, and mutate `registry`, + * `routesPaths`, and `routesChunkNames` accordingly. + */ +function genRouteCode(routeConfig: RouteConfig, res: LoadedRoutes): string { + const { + path: routePath, + component, + modules = {}, + routes: subroutes, + priority, + exact, + ...props + } = routeConfig; - if (typeof routePath !== 'string' || !component) { - throw new Error( - `Invalid route config: path must be a string and component is required. + if (typeof routePath !== 'string' || !component) { + throw new Error( + `Invalid route config: path must be a string and component is required. ${JSON.stringify(routeConfig)}`, - ); - } + ); + } - // Collect all page paths for injecting it later in the plugin lifecycle - // This is useful for plugins like sitemaps, redirects etc... - // If a route has subroutes, it is not necessarily a valid page path (more - // likely to be a wrapper) - if (!subroutes) { - routesPaths.push(routePath); - } + if (!subroutes) { + res.routesPaths.push(routePath); + } - // We hash the route to generate the key, because 2 routes can conflict with - // each others if they have the same path, ex: parent=/docs, child=/docs - // see https://github.com/facebook/docusaurus/issues/2917 - const routeHash = simpleHash(JSON.stringify(routeConfig), 3); - const chunkNamesKey = `${routePath}-${routeHash}`; - routesChunkNames[chunkNamesKey] = { - ...genRouteChunkNames(registry, {component}, 'component', component), - ...genRouteChunkNames(registry, modules, 'module', routePath), - }; + const routeHash = simpleHash(JSON.stringify(routeConfig), 3); + res.routesChunkNames[`${routePath}-${routeHash}`] = { + ...genChunkNames({component}, 'component', component, res), + ...genChunkNames(modules, 'module', routePath, res), + }; - return createRouteCodeString({ - routePath: routeConfig.path.replace(/'/g, "\\'"), - routeHash, - exact, - subroutesCodeStrings: subroutes?.map(generateRouteCode), - props, - }); - } + return serializeRouteConfig({ + routePath: routePath.replace(/'/g, "\\'"), + routeHash, + subroutesCodeStrings: subroutes?.map((r) => genRouteCode(r, res)), + exact, + props, + }); +} - const routesConfig = ` -${RoutesImportsCode} +/** + * Routes are prepared into three temp files: + * + * - `routesConfig`, the route config passed to react-router. This file is kept + * minimal, because it can't be code-splitted. + * - `routesChunkNames`, a mapping from route paths (hashed) to code-splitted + * chunk names. + * - `registry`, a mapping from chunk names to options for react-loadable. + */ +export async function loadRoutes( + routeConfigs: RouteConfig[], + baseUrl: string, + onDuplicateRoutes: ReportingSeverity, +): Promise { + handleDuplicateRoutes(routeConfigs, onDuplicateRoutes); + const res: LoadedRoutes = { + // To be written + routesConfig: '', + routesChunkNames: {}, + registry: {}, + routesPaths: [normalizeUrl([baseUrl, '404.html'])], + }; + + res.routesConfig = `import React from 'react'; +import ComponentCreator from '@docusaurus/ComponentCreator'; export default [ -${indent(`${pluginsRouteConfigs.map(generateRouteCode).join(',\n')},`)} -${indent(NotFoundRouteCode)} +${indent(`${routeConfigs.map((r) => genRouteCode(r, res)).join(',\n')},`)} + { + path: '*', + component: ComponentCreator('*'), + }, ]; `; - return { - registry, - routesConfig, - routesChunkNames, - routesPaths, - }; + return res; } diff --git a/packages/docusaurus/src/webpack/__tests__/utils.test.ts b/packages/docusaurus/src/webpack/__tests__/utils.test.ts index 85a80f2ca5de..1a388ed4e4f8 100644 --- a/packages/docusaurus/src/webpack/__tests__/utils.test.ts +++ b/packages/docusaurus/src/webpack/__tests__/utils.test.ts @@ -14,10 +14,7 @@ import { applyConfigurePostCss, getHttpsConfig, } from '../utils'; -import type { - ConfigureWebpackFn, - ConfigureWebpackFnMergeStrategy, -} from '@docusaurus/types'; +import type {Plugin} from '@docusaurus/types'; describe('customize JS loader', () => { it('getCustomizableJSLoader defaults to babel loader', () => { @@ -63,7 +60,7 @@ describe('extending generated webpack config', () => { }, }; - const configureWebpack: ConfigureWebpackFn = ( + const configureWebpack: Plugin['configureWebpack'] = ( generatedConfig, isServer, ) => { @@ -99,7 +96,7 @@ describe('extending generated webpack config', () => { }, }; - const configureWebpack: ConfigureWebpackFn = () => ({ + const configureWebpack: Plugin['configureWebpack'] = () => ({ entry: 'entry.js', output: { path: path.join(__dirname, 'dist'), @@ -128,9 +125,9 @@ describe('extending generated webpack config', () => { }, }; - const createConfigureWebpack: ( - mergeStrategy?: ConfigureWebpackFnMergeStrategy, - ) => ConfigureWebpackFn = (mergeStrategy) => () => ({ + const createConfigureWebpack: (mergeStrategy?: { + [key: string]: 'prepend' | 'append'; + }) => Plugin['configureWebpack'] = (mergeStrategy) => () => ({ module: { rules: [{use: 'zzz'}], }, diff --git a/packages/docusaurus/src/webpack/utils.ts b/packages/docusaurus/src/webpack/utils.ts index f9626b819604..85345c201b0a 100644 --- a/packages/docusaurus/src/webpack/utils.ts +++ b/packages/docusaurus/src/webpack/utils.ts @@ -25,8 +25,7 @@ import crypto from 'crypto'; import logger from '@docusaurus/logger'; import type {TransformOptions} from '@babel/core'; import type { - ConfigureWebpackFn, - ConfigurePostCssFn, + Plugin, PostCssOptions, ConfigureWebpackUtils, } from '@docusaurus/types'; @@ -172,7 +171,7 @@ export const getCustomizableJSLoader = * @returns final/ modified webpack config */ export function applyConfigureWebpack( - configureWebpack: ConfigureWebpackFn, + configureWebpack: NonNullable, config: Configuration, isServer: boolean, jsLoader: 'babel' | ((isServer: boolean) => RuleSetRule) | undefined, @@ -198,7 +197,7 @@ export function applyConfigureWebpack( } export function applyConfigurePostCss( - configurePostCss: NonNullable, + configurePostCss: NonNullable, config: Configuration, ): Configuration { type LocalPostCSSLoader = unknown & { diff --git a/website/docs/api/plugin-methods/lifecycle-apis.md b/website/docs/api/plugin-methods/lifecycle-apis.md index 3d30cb3ea5ba..e963f401251e 100644 --- a/website/docs/api/plugin-methods/lifecycle-apis.md +++ b/website/docs/api/plugin-methods/lifecycle-apis.md @@ -46,13 +46,13 @@ Create a route to add to the website. interface RouteConfig { path: string; component: string; - modules?: RouteModule; + modules?: RouteModules; routes?: RouteConfig[]; exact?: boolean; priority?: number; } -interface RouteModule { - [module: string]: Module | RouteModule | RouteModule[]; +interface RouteModules { + [module: string]: Module | RouteModules | RouteModules[]; } type Module = | {