diff --git a/packages/docusaurus-plugin-content-docs/src/index.ts b/packages/docusaurus-plugin-content-docs/src/index.ts index e35f3c186613..4937c3853757 100644 --- a/packages/docusaurus-plugin-content-docs/src/index.ts +++ b/packages/docusaurus-plugin-content-docs/src/index.ts @@ -381,23 +381,18 @@ Available document ids= // (/docs, /docs/next, /docs/1.0 etc...) // The component applies the layout and renders the appropriate doc const addBaseRoute = async ( - docsBaseRoute: string, + docsBasePath: string, docsBaseMetadata: DocsBaseMetadata, routes: RouteConfig[], priority?: number, ) => { const docsBaseMetadataPath = await createData( - `${docuHash(normalizeUrl([docsBaseRoute, ':route']))}.json`, + `${docuHash(normalizeUrl([docsBasePath, ':route']))}.json`, JSON.stringify(docsBaseMetadata, null, 2), ); - // Important: the layout component should not end with /, - // as it conflicts with the home doc - // Workaround fix for https://github.com/facebook/docusaurus/issues/2917 - const docsPath = docsBaseRoute === '/' ? '' : docsBaseRoute; - addRoute({ - path: docsPath, + path: docsBasePath, exact: false, // allow matching /docs/* as well component: docLayoutComponent, // main docs component (DocPage) routes, // subroute for each doc diff --git a/packages/docusaurus-utils/src/__tests__/index.test.ts b/packages/docusaurus-utils/src/__tests__/index.test.ts index 29cde6684ae4..9d454c98e633 100644 --- a/packages/docusaurus-utils/src/__tests__/index.test.ts +++ b/packages/docusaurus-utils/src/__tests__/index.test.ts @@ -8,6 +8,7 @@ import path from 'path'; import { fileToPath, + simpleHash, docuHash, genComponentName, genChunkName, @@ -70,6 +71,21 @@ describe('load utils', () => { }); }); + test('simpleHash', () => { + const asserts = { + '': 'd41', + '/foo-bar': '096', + '/foo/bar': '1df', + '/endi/lie': '9fa', + '/endi-lie': 'fd3', + '/yangshun/tay': '48d', + '/yangshun-tay': 'f3b', + }; + Object.keys(asserts).forEach((file) => { + expect(simpleHash(file, 3)).toBe(asserts[file]); + }); + }); + test('docuHash', () => { const asserts = { '': '-d41', diff --git a/packages/docusaurus-utils/src/index.ts b/packages/docusaurus-utils/src/index.ts index 542b05ba2450..45e32d43f8db 100644 --- a/packages/docusaurus-utils/src/index.ts +++ b/packages/docusaurus-utils/src/index.ts @@ -80,6 +80,10 @@ export function encodePath(userpath: string): string { .join('/'); } +export function simpleHash(str: string, length: number): string { + return createHash('md5').update(str).digest('hex').substr(0, length); +} + /** * Given an input string, convert to kebab-case and append a hash. * Avoid str collision. @@ -88,7 +92,7 @@ export function docuHash(str: string): string { if (str === '/') { return 'index'; } - const shortHash = createHash('md5').update(str).digest('hex').substr(0, 3); + const shortHash = simpleHash(str, 3); return `${kebabCase(str)}-${shortHash}`; } @@ -139,17 +143,11 @@ export function genChunkName( let chunkName: string | undefined = chunkNameCache.get(modulePath); if (!chunkName) { if (shortId) { - chunkName = createHash('md5') - .update(modulePath) - .digest('hex') - .substr(0, 8); + chunkName = simpleHash(modulePath, 8); } else { let str = modulePath; if (preferredName) { - const shortHash = createHash('md5') - .update(modulePath) - .digest('hex') - .substr(0, 3); + const shortHash = simpleHash(modulePath, 3); str = `${preferredName}${shortHash}`; } const name = str === '/' ? 'index' : docuHash(str); diff --git a/packages/docusaurus/src/client/exports/ComponentCreator.tsx b/packages/docusaurus/src/client/exports/ComponentCreator.tsx index 1d3a8706a018..2cc048abca30 100644 --- a/packages/docusaurus/src/client/exports/ComponentCreator.tsx +++ b/packages/docusaurus/src/client/exports/ComponentCreator.tsx @@ -12,7 +12,10 @@ import routesChunkNames from '@generated/routesChunkNames'; import registry from '@generated/registry'; import flat from '../flat'; -function ComponentCreator(path: string): ReturnType { +function ComponentCreator( + path: string, + hash: string, +): ReturnType { // 404 page if (path === '*') { return Loadable({ @@ -21,7 +24,8 @@ function ComponentCreator(path: string): ReturnType { }); } - const chunkNames = routesChunkNames[path]; + const chunkNamesKey = `${path}-${hash}`; + const chunkNames = routesChunkNames[chunkNamesKey]; const optsModules: string[] = []; const optsWebpack: string[] = []; const optsLoader = {}; 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 b91f71b62f9d..bf1e81a3116b 100644 --- a/packages/docusaurus/src/server/__tests__/__snapshots__/routes.test.ts.snap +++ b/packages/docusaurus/src/server/__tests__/__snapshots__/routes.test.ts.snap @@ -21,7 +21,7 @@ Object { }, }, "routesChunkNames": Object { - "/blog": Object { + "/blog-94e": Object { "component": "component---theme-blog-list-pagea-6-a-7ba", "items": Array [ Object { @@ -38,20 +38,16 @@ Object { "routesConfig": " import React from 'react'; import ComponentCreator from '@docusaurus/ComponentCreator'; - export default [ - { path: '/blog', - component: ComponentCreator('/blog'), + component: ComponentCreator('/blog','94e'), exact: true, - }, - - { - path: '*', - component: ComponentCreator('*') - } +{ + path: '*', + component: ComponentCreator('*') +} ]; ", "routesPaths": Array [ @@ -94,16 +90,16 @@ Object { }, }, "routesChunkNames": Object { - "/docs/hello": Object { + "/docs/hello-f94": Object { "component": "component---theme-doc-item-178-a40", "content": "content---docs-helloaff-811", "metadata": "metadata---docs-hello-956-741", }, - "/docs:route": Object { + "/docs:route-838": Object { "component": "component---theme-doc-page-1-be-9be", "docsMetadata": "docsMetadata---docs-routef-34-881", }, - "docs/foo/baz": Object { + "docs/foo/baz-f88": Object { "component": "component---theme-doc-item-178-a40", "content": "content---docs-foo-baz-8-ce-61e", "metadata": "metadata---docs-foo-baz-2-cf-fa7", @@ -112,32 +108,28 @@ Object { "routesConfig": " import React from 'react'; import ComponentCreator from '@docusaurus/ComponentCreator'; - export default [ - { path: '/docs:route', - component: ComponentCreator('/docs:route'), + component: ComponentCreator('/docs:route','838'), routes: [ { path: '/docs/hello', - component: ComponentCreator('/docs/hello'), + component: ComponentCreator('/docs/hello','f94'), exact: true, - }, { path: 'docs/foo/baz', - component: ComponentCreator('docs/foo/baz'), - + component: ComponentCreator('docs/foo/baz','f88'), -}], }, - - { - path: '*', - component: ComponentCreator('*') - } +] +}, +{ + path: '*', + component: ComponentCreator('*') +} ]; ", "routesPaths": Array [ @@ -157,27 +149,23 @@ Object { }, }, "routesChunkNames": Object { - "": Object { + "-b2a": Object { "component": "component---hello-world-jse-0-f-b6c", }, }, "routesConfig": " import React from 'react'; import ComponentCreator from '@docusaurus/ComponentCreator'; - export default [ - { path: '', - component: ComponentCreator(''), - + component: ComponentCreator('','b2a'), }, - - { - path: '*', - component: ComponentCreator('*') - } +{ + path: '*', + component: ComponentCreator('*') +} ]; ", "routesPaths": Array [ diff --git a/packages/docusaurus/src/server/routes.ts b/packages/docusaurus/src/server/routes.ts index ff5dd868b15f..455c5fa0591a 100644 --- a/packages/docusaurus/src/server/routes.ts +++ b/packages/docusaurus/src/server/routes.ts @@ -5,7 +5,12 @@ * LICENSE file in the root directory of this source tree. */ -import {genChunkName, normalizeUrl} from '@docusaurus/utils'; +import { + genChunkName, + normalizeUrl, + removeSuffix, + simpleHash, +} from '@docusaurus/utils'; import has from 'lodash.has'; import isPlainObject from 'lodash.isplainobject'; import isString from 'lodash.isstring'; @@ -17,7 +22,42 @@ import { RouteModule, ChunkNames, } from '@docusaurus/types'; -import chalk from 'chalk'; + +const createRouteCodeString = ({ + routePath, + routeHash, + exact, + subroutesCodeStrings, +}: { + routePath: string; + routeHash: string; + exact?: boolean; + subroutesCodeStrings?: string[]; +}) => { + const str = `{ + path: '${routePath}', + component: ComponentCreator('${routePath}','${routeHash}'), + ${exact ? `exact: true,` : ''} +${ + subroutesCodeStrings + ? ` routes: [ +${removeSuffix(subroutesCodeStrings.join(',\n'), ',\n')}, +] +` + : '' +}}`; + return str; +}; + +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 (isString(value)) { @@ -52,10 +92,6 @@ export default async function loadRoutes( pluginsRouteConfigs: RouteConfig[], baseUrl: string, ): Promise { - const routesImports = [ - `import React from 'react';`, - `import ComponentCreator from '@docusaurus/ComponentCreator';`, - ]; const registry: { [chunkName: string]: ChunkRegistry; } = {}; @@ -70,7 +106,7 @@ export default async function loadRoutes( path: routePath, component, modules = {}, - routes, + routes: subroutes, exact, } = routeConfig; @@ -82,102 +118,36 @@ export default async function loadRoutes( ); } - if (!routes) { + // 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); } - function genRouteChunkNames( - value: RouteModule | RouteModule[] | Module | null | undefined, - prefix?: string, - name?: string, - ) { - if (!value) { - return null; - } - - if (Array.isArray(value)) { - return value.map((val, index) => - genRouteChunkNames(val, `${index}`, name), - ); - } - - if (isModule(value)) { - const modulePath = getModulePath(value); - const chunkName = genChunkName(modulePath, prefix, name); - // We need to JSON.stringify so that if its on windows, backslashes are escaped. - const loader = `() => import(/* webpackChunkName: '${chunkName}' */ ${JSON.stringify( - modulePath, - )})`; - - registry[chunkName] = { - loader, - modulePath, - }; - return chunkName; - } - - const newValue: ChunkNames = {}; - Object.keys(value).forEach((key) => { - newValue[key] = genRouteChunkNames(value[key], key, name); - }); - return newValue; - } - - const alreadyExistingRouteChunkNames = routesChunkNames[routePath]; - const chunkNames = { - ...genRouteChunkNames({component}, 'component', component), - ...genRouteChunkNames(modules, 'module', 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), }; - // TODO is it safe to merge? that could lead to unwanted overrides - // See https://github.com/facebook/docusaurus/issues/2917 - routesChunkNames[routePath] = { - ...alreadyExistingRouteChunkNames, - ...chunkNames, - }; - if (alreadyExistingRouteChunkNames) { - console.warn( - chalk.red( - `It seems multiple routes have been created for routePath=[${routePath}], this can lead to unexpected behaviors. -Components used for this route: -- ${alreadyExistingRouteChunkNames.component} -- ${chunkNames.component} -${ - routePath === '/' - ? "If you are using the docs-only/blog-only mode, don't forget to delete the homepage at ./src/pages/index.js" - : '' -} -`, - ), - ); - } - const routesStr = routes - ? `routes: [${routes.map(generateRouteCode).join(',')}],` - : ''; - const exactStr = exact ? `exact: true,` : ''; - - return ` -{ - path: '${routePath}', - component: ComponentCreator('${routePath}'), - ${exactStr} - ${routesStr} -}`; + return createRouteCodeString({ + routePath: routeConfig.path, + routeHash, + exact, + subroutesCodeStrings: subroutes?.map(generateRouteCode), + }); } - const routes = pluginsRouteConfigs.map(generateRouteCode); - const notFoundRoute = ` - { - path: '*', - component: ComponentCreator('*') - }`; - const routesConfig = ` -${routesImports.join('\n')} - +${RoutesImportsCode} export default [ - ${routes.join(',')}, - ${notFoundRoute} +${pluginsRouteConfigs.map(generateRouteCode).join(',\n')}, +${NotFoundRouteCode} ];\n`; return { @@ -187,3 +157,44 @@ export default [ routesPaths, }; } + +function genRouteChunkNames( + // TODO instead of passing a mutating the registry, return a registry slice? + registry: { + [chunkName: string]: ChunkRegistry; + }, + value: RouteModule | RouteModule[] | Module | null | undefined, + prefix?: string, + name?: string, +) { + 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); + const chunkName = genChunkName(modulePath, prefix, name); + // We need to JSON.stringify so that if its on windows, backslashes are escaped. + const loader = `() => import(/* webpackChunkName: '${chunkName}' */ ${JSON.stringify( + modulePath, + )})`; + + registry[chunkName] = { + loader, + modulePath, + }; + return chunkName; + } + + const newValue: ChunkNames = {}; + Object.keys(value).forEach((key) => { + newValue[key] = genRouteChunkNames(registry, value[key], key, name); + }); + return newValue; +}