Skip to content

Commit 29b7a4d

Browse files
authored
fix(core): codegen should generate unique route prop filenames (#10131)
1 parent 394ce84 commit 29b7a4d

File tree

4 files changed

+153
-26
lines changed

4 files changed

+153
-26
lines changed

packages/docusaurus-utils/src/__tests__/hashUtils.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,4 +48,30 @@ describe('docuHash', () => {
4848
expect(docuHash(file)).toBe(asserts[file]);
4949
});
5050
});
51+
52+
it('docuHash works with hashLength option', () => {
53+
const asserts: {[key: string]: string} = {
54+
'': '-d41d8',
55+
'/': 'index',
56+
'/foo-bar': 'foo-bar-09652',
57+
'/foo/bar': 'foo-bar-1df48',
58+
};
59+
Object.keys(asserts).forEach((file) => {
60+
expect(docuHash(file, {hashLength: 5})).toBe(asserts[file]);
61+
});
62+
});
63+
64+
it('docuHash works with hashExtra option', () => {
65+
expect(docuHash('')).toBe('-d41');
66+
expect(docuHash('', {hashExtra: ''})).toBe('-d41');
67+
expect(docuHash('', {hashExtra: 'some-extra'})).toBe('-928');
68+
69+
expect(docuHash('/')).toBe('index');
70+
expect(docuHash('/', {hashExtra: ''})).toBe('index-6a9');
71+
expect(docuHash('/', {hashExtra: 'some-extra'})).toBe('index-68e');
72+
73+
expect(docuHash('/foo/bar')).toBe('foo-bar-1df');
74+
expect(docuHash('/foo/bar', {hashExtra: ''})).toBe('foo-bar-1df');
75+
expect(docuHash('/foo/bar', {hashExtra: 'some-extra'})).toBe('foo-bar-7d4');
76+
});
5177
});

packages/docusaurus-utils/src/hashUtils.ts

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,28 @@ export function simpleHash(str: string, length: number): string {
2525
* collision. Also removes part of the string if its larger than the allowed
2626
* filename per OS, avoiding `ERRNAMETOOLONG` error.
2727
*/
28-
export function docuHash(str: string): string {
29-
if (str === '/') {
28+
export function docuHash(
29+
strInput: string,
30+
options?: {
31+
// String that contributes to the hash value
32+
// but does not contribute to the returned string
33+
hashExtra?: string;
34+
// Length of the hash to append
35+
hashLength?: number;
36+
},
37+
): string {
38+
// TODO check this historical behavior
39+
// I'm not sure it makes sense to keep it...
40+
if (strInput === '/' && typeof options?.hashExtra === 'undefined') {
3041
return 'index';
3142
}
32-
const shortHash = simpleHash(str, 3);
43+
const str = strInput === '/' ? 'index' : strInput;
44+
45+
const hashExtra = options?.hashExtra ?? '';
46+
const hashLength = options?.hashLength ?? 3;
47+
48+
const stringToHash = str + hashExtra;
49+
const shortHash = simpleHash(stringToHash, hashLength);
3350
const parsedPath = `${_.kebabCase(str)}-${shortHash}`;
3451
if (isNameTooLong(parsedPath)) {
3552
return `${shortName(_.kebabCase(str))}-${shortHash}`;

packages/docusaurus/src/server/codegen/__tests__/codegenRoutes.test.ts

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,75 @@
55
* LICENSE file in the root directory of this source tree.
66
*/
77

8-
import {generateRoutesCode, genChunkName} from '../codegenRoutes';
8+
import {
9+
generateRoutesCode,
10+
genChunkName,
11+
generateRoutePropFilename,
12+
} from '../codegenRoutes';
913
import type {RouteConfig} from '@docusaurus/types';
1014

15+
describe('generateRoutePropFilename', () => {
16+
it('generate filename based on route path', () => {
17+
expect(
18+
generateRoutePropFilename({
19+
path: '/some/route-path/',
20+
component: '@theme/Home',
21+
}),
22+
).toEqual(expect.stringMatching(/^some-route-path-[a-z\d]{3}.json$/));
23+
});
24+
25+
it('generate filename for /', () => {
26+
expect(
27+
generateRoutePropFilename({
28+
path: '/',
29+
component: '@theme/Home',
30+
}),
31+
).toEqual(expect.stringMatching(/^index-[a-z\d]{3}.json$/));
32+
});
33+
34+
it('generate filename for /category/', () => {
35+
expect(
36+
generateRoutePropFilename({
37+
path: '/category/',
38+
component: '@theme/Home',
39+
}),
40+
).toEqual(expect.stringMatching(/^category-[a-z\d]{3}.json$/));
41+
});
42+
43+
it('generate unique filenames for /', () => {
44+
expect(
45+
generateRoutePropFilename({path: '/', component: '@theme/Home'}),
46+
).toEqual(generateRoutePropFilename({path: '/', component: '@theme/Home'}));
47+
expect(
48+
generateRoutePropFilename({path: '/', component: '@theme/Home'}),
49+
).not.toEqual(
50+
generateRoutePropFilename({
51+
path: '/',
52+
component: '@theme/AnotherComponent',
53+
}),
54+
);
55+
});
56+
57+
it('generate unique filenames for /some/path', () => {
58+
expect(
59+
generateRoutePropFilename({path: '/some/path', component: '@theme/Home'}),
60+
).toEqual(
61+
generateRoutePropFilename({path: '/some/path', component: '@theme/Home'}),
62+
);
63+
expect(
64+
generateRoutePropFilename({
65+
path: '/some/path',
66+
component: '@theme/Home',
67+
}),
68+
).not.toEqual(
69+
generateRoutePropFilename({
70+
path: '/some/path',
71+
component: '@theme/AnotherComponent',
72+
}),
73+
);
74+
});
75+
});
76+
1177
describe('genChunkName', () => {
1278
it('works', () => {
1379
const firstAssert: {[key: string]: string} = {

packages/docusaurus/src/server/codegen/codegenRoutes.ts

Lines changed: 40 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,22 @@ type GenerateRouteFilesParams = {
320320
baseUrl: string;
321321
};
322322

323+
// The generated filename per route must be unique to avoid conflicts
324+
// See also https://github.com/facebook/docusaurus/issues/10125
325+
export function generateRoutePropFilename(route: RouteConfig): string {
326+
// TODO if possible, we could try to shorten the filename by removing
327+
// the plugin routeBasePath prefix from the name
328+
return `${docuHash(
329+
route.path,
330+
// Note: using hash(route.path + route.component) is not technically
331+
// as robust as hashing the entire prop content object.
332+
// But it's faster and should be good enough considering it's very unlikely
333+
// anyone would have 2 routes on the same path also rendering the exact
334+
// same component.
335+
{hashExtra: route.component},
336+
)}.json`;
337+
}
338+
323339
async function generateRoutePropModule({
324340
generatedFilesDir,
325341
route,
@@ -339,7 +355,7 @@ async function generateRoutePropModule({
339355
plugin.name,
340356
plugin.id,
341357
'p',
342-
`${docuHash(route.path)}.json`,
358+
generateRoutePropFilename(route),
343359
);
344360
const modulePath = path.posix.join(generatedFilesDir, relativePath);
345361
const aliasedPath = path.posix.join('@generated', relativePath);
@@ -376,29 +392,31 @@ async function preprocessRouteProps({
376392
route: RouteConfig;
377393
plugin: PluginIdentifier;
378394
}): Promise<RouteConfig> {
379-
const propsModulePathPromise = route.props
380-
? generateRoutePropModule({
381-
generatedFilesDir,
382-
route,
383-
plugin,
384-
})
385-
: undefined;
386-
387-
const subRoutesPromise = route.routes
388-
? Promise.all(
389-
route.routes.map((subRoute: RouteConfig) => {
390-
return preprocessRouteProps({
391-
generatedFilesDir,
392-
route: subRoute,
393-
plugin,
394-
});
395-
}),
396-
)
397-
: undefined;
395+
const getPropsModulePathPromise = () =>
396+
route.props
397+
? generateRoutePropModule({
398+
generatedFilesDir,
399+
route,
400+
plugin,
401+
})
402+
: undefined;
403+
404+
const getSubRoutesPromise = () =>
405+
route.routes
406+
? Promise.all(
407+
route.routes.map((subRoute: RouteConfig) => {
408+
return preprocessRouteProps({
409+
generatedFilesDir,
410+
route: subRoute,
411+
plugin,
412+
});
413+
}),
414+
)
415+
: undefined;
398416

399417
const [propsModulePath, subRoutes] = await Promise.all([
400-
propsModulePathPromise,
401-
subRoutesPromise,
418+
getPropsModulePathPromise(),
419+
getSubRoutesPromise(),
402420
]);
403421

404422
const newRoute: RouteConfig = {

0 commit comments

Comments
 (0)