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:
Sébastien Lorber 2020-06-30 11:52:39 +02:00 committed by GitHub
parent 984e2d4598
commit cf97662eef
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 161 additions and 149 deletions

View file

@ -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

View file

@ -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',

View file

@ -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);

View file

@ -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 = {};

View file

@ -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 [

View file

@ -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;
}