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...) // (/docs, /docs/next, /docs/1.0 etc...)
// The component applies the layout and renders the appropriate doc // The component applies the layout and renders the appropriate doc
const addBaseRoute = async ( const addBaseRoute = async (
docsBaseRoute: string, docsBasePath: string,
docsBaseMetadata: DocsBaseMetadata, docsBaseMetadata: DocsBaseMetadata,
routes: RouteConfig[], routes: RouteConfig[],
priority?: number, priority?: number,
) => { ) => {
const docsBaseMetadataPath = await createData( const docsBaseMetadataPath = await createData(
`${docuHash(normalizeUrl([docsBaseRoute, ':route']))}.json`, `${docuHash(normalizeUrl([docsBasePath, ':route']))}.json`,
JSON.stringify(docsBaseMetadata, null, 2), 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({ addRoute({
path: docsPath, path: docsBasePath,
exact: false, // allow matching /docs/* as well exact: false, // allow matching /docs/* as well
component: docLayoutComponent, // main docs component (DocPage) component: docLayoutComponent, // main docs component (DocPage)
routes, // subroute for each doc routes, // subroute for each doc

View file

@ -8,6 +8,7 @@
import path from 'path'; import path from 'path';
import { import {
fileToPath, fileToPath,
simpleHash,
docuHash, docuHash,
genComponentName, genComponentName,
genChunkName, 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', () => { test('docuHash', () => {
const asserts = { const asserts = {
'': '-d41', '': '-d41',

View file

@ -80,6 +80,10 @@ export function encodePath(userpath: string): string {
.join('/'); .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. * Given an input string, convert to kebab-case and append a hash.
* Avoid str collision. * Avoid str collision.
@ -88,7 +92,7 @@ export function docuHash(str: string): string {
if (str === '/') { if (str === '/') {
return 'index'; return 'index';
} }
const shortHash = createHash('md5').update(str).digest('hex').substr(0, 3); const shortHash = simpleHash(str, 3);
return `${kebabCase(str)}-${shortHash}`; return `${kebabCase(str)}-${shortHash}`;
} }
@ -139,17 +143,11 @@ export function genChunkName(
let chunkName: string | undefined = chunkNameCache.get(modulePath); let chunkName: string | undefined = chunkNameCache.get(modulePath);
if (!chunkName) { if (!chunkName) {
if (shortId) { if (shortId) {
chunkName = createHash('md5') chunkName = simpleHash(modulePath, 8);
.update(modulePath)
.digest('hex')
.substr(0, 8);
} else { } else {
let str = modulePath; let str = modulePath;
if (preferredName) { if (preferredName) {
const shortHash = createHash('md5') const shortHash = simpleHash(modulePath, 3);
.update(modulePath)
.digest('hex')
.substr(0, 3);
str = `${preferredName}${shortHash}`; str = `${preferredName}${shortHash}`;
} }
const name = str === '/' ? 'index' : docuHash(str); const name = str === '/' ? 'index' : docuHash(str);

View file

@ -12,7 +12,10 @@ import routesChunkNames from '@generated/routesChunkNames';
import registry from '@generated/registry'; import registry from '@generated/registry';
import flat from '../flat'; import flat from '../flat';
function ComponentCreator(path: string): ReturnType<typeof Loadable> { function ComponentCreator(
path: string,
hash: string,
): ReturnType<typeof Loadable> {
// 404 page // 404 page
if (path === '*') { if (path === '*') {
return Loadable({ 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 optsModules: string[] = [];
const optsWebpack: string[] = []; const optsWebpack: string[] = [];
const optsLoader = {}; const optsLoader = {};

View file

@ -21,7 +21,7 @@ Object {
}, },
}, },
"routesChunkNames": Object { "routesChunkNames": Object {
"/blog": Object { "/blog-94e": Object {
"component": "component---theme-blog-list-pagea-6-a-7ba", "component": "component---theme-blog-list-pagea-6-a-7ba",
"items": Array [ "items": Array [
Object { Object {
@ -38,20 +38,16 @@ Object {
"routesConfig": " "routesConfig": "
import React from 'react'; import React from 'react';
import ComponentCreator from '@docusaurus/ComponentCreator'; import ComponentCreator from '@docusaurus/ComponentCreator';
export default [ export default [
{ {
path: '/blog', path: '/blog',
component: ComponentCreator('/blog'), component: ComponentCreator('/blog','94e'),
exact: true, exact: true,
}, },
{
{ path: '*',
path: '*', component: ComponentCreator('*')
component: ComponentCreator('*') }
}
]; ];
", ",
"routesPaths": Array [ "routesPaths": Array [
@ -94,16 +90,16 @@ Object {
}, },
}, },
"routesChunkNames": Object { "routesChunkNames": Object {
"/docs/hello": Object { "/docs/hello-f94": Object {
"component": "component---theme-doc-item-178-a40", "component": "component---theme-doc-item-178-a40",
"content": "content---docs-helloaff-811", "content": "content---docs-helloaff-811",
"metadata": "metadata---docs-hello-956-741", "metadata": "metadata---docs-hello-956-741",
}, },
"/docs:route": Object { "/docs:route-838": Object {
"component": "component---theme-doc-page-1-be-9be", "component": "component---theme-doc-page-1-be-9be",
"docsMetadata": "docsMetadata---docs-routef-34-881", "docsMetadata": "docsMetadata---docs-routef-34-881",
}, },
"docs/foo/baz": Object { "docs/foo/baz-f88": Object {
"component": "component---theme-doc-item-178-a40", "component": "component---theme-doc-item-178-a40",
"content": "content---docs-foo-baz-8-ce-61e", "content": "content---docs-foo-baz-8-ce-61e",
"metadata": "metadata---docs-foo-baz-2-cf-fa7", "metadata": "metadata---docs-foo-baz-2-cf-fa7",
@ -112,32 +108,28 @@ Object {
"routesConfig": " "routesConfig": "
import React from 'react'; import React from 'react';
import ComponentCreator from '@docusaurus/ComponentCreator'; import ComponentCreator from '@docusaurus/ComponentCreator';
export default [ export default [
{ {
path: '/docs:route', path: '/docs:route',
component: ComponentCreator('/docs:route'), component: ComponentCreator('/docs:route','838'),
routes: [ routes: [
{ {
path: '/docs/hello', path: '/docs/hello',
component: ComponentCreator('/docs/hello'), component: ComponentCreator('/docs/hello','f94'),
exact: true, exact: true,
}, },
{ {
path: 'docs/foo/baz', path: 'docs/foo/baz',
component: ComponentCreator('docs/foo/baz'), component: ComponentCreator('docs/foo/baz','f88'),
}],
}, },
]
{ },
path: '*', {
component: ComponentCreator('*') path: '*',
} component: ComponentCreator('*')
}
]; ];
", ",
"routesPaths": Array [ "routesPaths": Array [
@ -157,27 +149,23 @@ Object {
}, },
}, },
"routesChunkNames": Object { "routesChunkNames": Object {
"": Object { "-b2a": Object {
"component": "component---hello-world-jse-0-f-b6c", "component": "component---hello-world-jse-0-f-b6c",
}, },
}, },
"routesConfig": " "routesConfig": "
import React from 'react'; import React from 'react';
import ComponentCreator from '@docusaurus/ComponentCreator'; import ComponentCreator from '@docusaurus/ComponentCreator';
export default [ export default [
{ {
path: '', path: '',
component: ComponentCreator(''), component: ComponentCreator('','b2a'),
}, },
{
{ path: '*',
path: '*', component: ComponentCreator('*')
component: ComponentCreator('*') }
}
]; ];
", ",
"routesPaths": Array [ "routesPaths": Array [

View file

@ -5,7 +5,12 @@
* LICENSE file in the root directory of this source tree. * 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 has from 'lodash.has';
import isPlainObject from 'lodash.isplainobject'; import isPlainObject from 'lodash.isplainobject';
import isString from 'lodash.isstring'; import isString from 'lodash.isstring';
@ -17,7 +22,42 @@ import {
RouteModule, RouteModule,
ChunkNames, ChunkNames,
} from '@docusaurus/types'; } 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 { function isModule(value: unknown): value is Module {
if (isString(value)) { if (isString(value)) {
@ -52,10 +92,6 @@ export default async function loadRoutes(
pluginsRouteConfigs: RouteConfig[], pluginsRouteConfigs: RouteConfig[],
baseUrl: string, baseUrl: string,
): Promise<LoadedRoutes> { ): Promise<LoadedRoutes> {
const routesImports = [
`import React from 'react';`,
`import ComponentCreator from '@docusaurus/ComponentCreator';`,
];
const registry: { const registry: {
[chunkName: string]: ChunkRegistry; [chunkName: string]: ChunkRegistry;
} = {}; } = {};
@ -70,7 +106,7 @@ export default async function loadRoutes(
path: routePath, path: routePath,
component, component,
modules = {}, modules = {},
routes, routes: subroutes,
exact, exact,
} = routeConfig; } = 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); routesPaths.push(routePath);
} }
function genRouteChunkNames( // We hash the route to generate the key, because 2 routes can conflict with
value: RouteModule | RouteModule[] | Module | null | undefined, // each others if they have the same path, ex: parent=/docs, child=/docs
prefix?: string, // see https://github.com/facebook/docusaurus/issues/2917
name?: string, const routeHash = simpleHash(JSON.stringify(routeConfig), 3);
) { const chunkNamesKey = `${routePath}-${routeHash}`;
if (!value) { routesChunkNames[chunkNamesKey] = {
return null; ...genRouteChunkNames(registry, {component}, 'component', component),
} ...genRouteChunkNames(registry, modules, 'module', routePath),
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),
}; };
// 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 return createRouteCodeString({
? `routes: [${routes.map(generateRouteCode).join(',')}],` routePath: routeConfig.path,
: ''; routeHash,
const exactStr = exact ? `exact: true,` : ''; exact,
subroutesCodeStrings: subroutes?.map(generateRouteCode),
return ` });
{
path: '${routePath}',
component: ComponentCreator('${routePath}'),
${exactStr}
${routesStr}
}`;
} }
const routes = pluginsRouteConfigs.map(generateRouteCode);
const notFoundRoute = `
{
path: '*',
component: ComponentCreator('*')
}`;
const routesConfig = ` const routesConfig = `
${routesImports.join('\n')} ${RoutesImportsCode}
export default [ export default [
${routes.join(',')}, ${pluginsRouteConfigs.map(generateRouteCode).join(',\n')},
${notFoundRoute} ${NotFoundRouteCode}
];\n`; ];\n`;
return { return {
@ -187,3 +157,44 @@ export default [
routesPaths, 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;
}