mirror of
https://github.com/facebook/docusaurus.git
synced 2025-05-02 19:57:25 +02:00
fix(v2): refactor routes.ts + add route hash for chunkNames key (#3001)
* add simpleHash util * refactor/split the routes generation logic + add route hash to avoid chunk conflicts * minor fixes + fix tests * fix comment typo
This commit is contained in:
parent
984e2d4598
commit
cf97662eef
6 changed files with 161 additions and 149 deletions
|
@ -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
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import path from 'path';
|
||||
import {
|
||||
fileToPath,
|
||||
simpleHash,
|
||||
docuHash,
|
||||
genComponentName,
|
||||
genChunkName,
|
||||
|
@ -71,6 +72,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',
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -12,7 +12,10 @@ import routesChunkNames from '@generated/routesChunkNames';
|
|||
import registry from '@generated/registry';
|
||||
import flat from '../flat';
|
||||
|
||||
function ComponentCreator(path: string): ReturnType<typeof Loadable> {
|
||||
function ComponentCreator(
|
||||
path: string,
|
||||
hash: string,
|
||||
): ReturnType<typeof Loadable> {
|
||||
// 404 page
|
||||
if (path === '*') {
|
||||
return Loadable({
|
||||
|
@ -21,7 +24,8 @@ function ComponentCreator(path: string): ReturnType<typeof Loadable> {
|
|||
});
|
||||
}
|
||||
|
||||
const chunkNames = routesChunkNames[path];
|
||||
const chunkNamesKey = `${path}-${hash}`;
|
||||
const chunkNames = routesChunkNames[chunkNamesKey];
|
||||
const optsModules: string[] = [];
|
||||
const optsWebpack: string[] = [];
|
||||
const optsLoader = {};
|
||||
|
|
|
@ -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 [
|
||||
|
|
|
@ -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<LoadedRoutes> {
|
||||
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;
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue