diff --git a/packages/docusaurus/src/client/exports/ComponentCreator.tsx b/packages/docusaurus/src/client/exports/ComponentCreator.tsx
index 86c52a213e..7c79b33575 100644
--- a/packages/docusaurus/src/client/exports/ComponentCreator.tsx
+++ b/packages/docusaurus/src/client/exports/ComponentCreator.tsx
@@ -21,77 +21,97 @@ export default function ComponentCreator(
if (path === '*') {
return Loadable({
loading: Loading,
- loader: async () => {
- const NotFound = (await import('@theme/NotFound')).default;
- return (props) => (
- // Is there a better API for this?
+ loader: () =>
+ import('@theme/NotFound').then(({default: NotFound}) => (props) => (
- );
- },
+ )),
});
}
const chunkNames = routesChunkNames[`${path}-${hash}`]!;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
- const optsLoader: {[key: string]: () => Promise} = {};
- const optsModules: string[] = [];
+ const loader: {[key: string]: () => Promise} = {};
+ const modules: string[] = [];
const optsWebpack: string[] = [];
+ // A map from prop names to chunk names.
+ // e.g. Suppose the plugin added this as route:
+ // { __comp: "...", prop: { foo: "..." }, items: ["...", "..."] }
+ // It will become:
+ // { __comp: "...", "prop.foo": "...", "items.0": "...", "items.1": ... }
+ // Loadable.Map will _map_ over `loader` and load each key.
const flatChunkNames = flat(chunkNames);
- Object.entries(flatChunkNames).forEach(([key, chunkName]) => {
+ Object.entries(flatChunkNames).forEach(([keyPath, chunkName]) => {
const chunkRegistry = registry[chunkName];
if (chunkRegistry) {
// eslint-disable-next-line prefer-destructuring
- optsLoader[key] = chunkRegistry[0];
- optsModules.push(chunkRegistry[1]);
+ loader[keyPath] = chunkRegistry[0];
+ modules.push(chunkRegistry[1]);
optsWebpack.push(chunkRegistry[2]);
}
});
return Loadable.Map({
loading: Loading,
- loader: optsLoader,
- modules: optsModules,
+ loader,
+ modules,
webpack: () => optsWebpack,
render: (loaded, props) => {
- // Clone the original object since we don't want to alter the original.
+ // `loaded` will be a map from key path (as returned from the flattened
+ // chunk names) to the modules loaded from the loaders. We now have to
+ // restore the chunk names' previous shape from this flat record.
+ // We do so by taking advantage of the existing `chunkNames` and replacing
+ // each chunk name with its loaded module, so we don't create another
+ // object from scratch.
const loadedModules = JSON.parse(JSON.stringify(chunkNames));
- Object.keys(loaded).forEach((key) => {
- const newComp = loaded[key].default;
- if (!newComp) {
+ Object.entries(loaded).forEach(([keyPath, loadedModule]) => {
+ // JSON modules are also loaded as `{ default: ... }` (`import()`
+ // semantics) but we just want to pass the actual value to props.
+ const chunk = loadedModule.default;
+ // One loaded chunk can only be one of two things: a module (props) or a
+ // component. Modules are always JSON, so `default` always exists. This
+ // could only happen with a user-defined component.
+ if (!chunk) {
throw new Error(
`The page component at ${path} doesn't have a default export. This makes it impossible to render anything. Consider default-exporting a React component.`,
);
}
- if (typeof newComp === 'object' || typeof newComp === 'function') {
- Object.keys(loaded[key])
+ // A module can be a primitive, for example, if the user stored a string
+ // as a prop. However, there seems to be a bug with swc-loader's CJS
+ // logic, in that it would load a JSON module with content "foo" as
+ // `{ default: "foo", 0: "f", 1: "o", 2: "o" }`. Just to be safe, we
+ // first make sure that the chunk is non-primitive.
+ if (typeof chunk === 'object' || typeof chunk === 'function') {
+ Object.keys(loadedModule)
.filter((k) => k !== 'default')
.forEach((nonDefaultKey) => {
- newComp[nonDefaultKey] = loaded[key][nonDefaultKey];
+ chunk[nonDefaultKey] = loadedModule[nonDefaultKey];
});
}
+ // We now have this chunk prepared. Go down the key path and replace the
+ // chunk name with the actual chunk.
let val = loadedModules;
- const keyPath = key.split('.');
- keyPath.slice(0, -1).forEach((k) => {
+ const keyPaths = keyPath.split('.');
+ keyPaths.slice(0, -1).forEach((k) => {
val = val[k];
});
- val[keyPath[keyPath.length - 1]!] = newComp;
+ val[keyPaths[keyPaths.length - 1]!] = chunk;
});
- const Component = loadedModules.component;
- delete loadedModules.component;
-
/* eslint-disable no-underscore-dangle */
- const routeContextModule = loadedModules.__routeContextModule;
- delete loadedModules.__routeContextModule;
+ const Component = loadedModules.__comp;
+ delete loadedModules.__comp;
+ const routeContext = loadedModules.__context;
+ delete loadedModules.__context;
/* eslint-enable no-underscore-dangle */
// Is there any way to put this RouteContextProvider upper in the tree?
return (
-
+
);
diff --git a/packages/docusaurus/src/client/prefetch.ts b/packages/docusaurus/src/client/prefetch.ts
index 8223c7d89d..5c28025eef 100644
--- a/packages/docusaurus/src/client/prefetch.ts
+++ b/packages/docusaurus/src/client/prefetch.ts
@@ -5,21 +5,13 @@
* LICENSE file in the root directory of this source tree.
*/
-function support(feature: string) {
- if (typeof document === 'undefined') {
- return false;
- }
-
- const fakeLink = document.createElement('link');
+function supports(feature: string) {
try {
- if (fakeLink.relList && typeof fakeLink.relList.supports === 'function') {
- return fakeLink.relList.supports(feature);
- }
+ const fakeLink = document.createElement('link');
+ return fakeLink.relList?.supports?.(feature);
} catch (err) {
return false;
}
-
- return false;
}
function linkPrefetchStrategy(url: string) {
@@ -61,7 +53,7 @@ function xhrPrefetchStrategy(url: string): Promise {
});
}
-const supportedPrefetchStrategy = support('prefetch')
+const supportedPrefetchStrategy = supports('prefetch')
? linkPrefetchStrategy
: xhrPrefetchStrategy;
diff --git a/packages/docusaurus/src/client/serverEntry.tsx b/packages/docusaurus/src/client/serverEntry.tsx
index 32af3e4e15..d8cbfee47d 100644
--- a/packages/docusaurus/src/client/serverEntry.tsx
+++ b/packages/docusaurus/src/client/serverEntry.tsx
@@ -78,14 +78,14 @@ async function doRender(locals: Locals & {path: string}) {
const location = routesLocation[locals.path]!;
await preload(routes, location);
const modules = new Set();
- const context = {};
+ const routerContext = {};
const helmetContext = {};
const linksCollector = createStatefulLinksCollector();
const appHtml = ReactDOMServer.renderToString(
modules.add(moduleName)}>
-
+
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 a69dd7b18c..b895467529 100644
--- a/packages/docusaurus/src/server/__tests__/__snapshots__/routes.test.ts.snap
+++ b/packages/docusaurus/src/server/__tests__/__snapshots__/routes.test.ts.snap
@@ -12,8 +12,8 @@ This could lead to non-deterministic routing behavior."
exports[`loadRoutes loads flat route config 1`] = `
{
"registry": {
- "component---theme-blog-list-pagea-6-a-7ba": {
- "loader": "() => import(/* webpackChunkName: 'component---theme-blog-list-pagea-6-a-7ba' */ '@theme/BlogListPage')",
+ "__comp---theme-blog-list-pagea-6-a-7ba": {
+ "loader": "() => import(/* webpackChunkName: '__comp---theme-blog-list-pagea-6-a-7ba' */ '@theme/BlogListPage')",
"modulePath": "@theme/BlogListPage",
},
"content---blog-0-b-4-09e": {
@@ -31,7 +31,7 @@ exports[`loadRoutes loads flat route config 1`] = `
},
"routesChunkNames": {
"/blog-599": {
- "component": "component---theme-blog-list-pagea-6-a-7ba",
+ "__comp": "__comp---theme-blog-list-pagea-6-a-7ba",
"items": [
{
"content": "content---blog-0-b-4-09e",
@@ -71,12 +71,12 @@ export default [
exports[`loadRoutes loads nested route config 1`] = `
{
"registry": {
- "component---theme-doc-item-178-a40": {
- "loader": "() => import(/* webpackChunkName: 'component---theme-doc-item-178-a40' */ '@theme/DocItem')",
+ "__comp---theme-doc-item-178-a40": {
+ "loader": "() => import(/* webpackChunkName: '__comp---theme-doc-item-178-a40' */ '@theme/DocItem')",
"modulePath": "@theme/DocItem",
},
- "component---theme-doc-page-1-be-9be": {
- "loader": "() => import(/* webpackChunkName: 'component---theme-doc-page-1-be-9be' */ '@theme/DocPage')",
+ "__comp---theme-doc-page-1-be-9be": {
+ "loader": "() => import(/* webpackChunkName: '__comp---theme-doc-page-1-be-9be' */ '@theme/DocPage')",
"modulePath": "@theme/DocPage",
},
"content---docs-foo-baz-8-ce-61e": {
@@ -102,16 +102,16 @@ exports[`loadRoutes loads nested route config 1`] = `
},
"routesChunkNames": {
"/docs/hello-44b": {
- "component": "component---theme-doc-item-178-a40",
+ "__comp": "__comp---theme-doc-item-178-a40",
"content": "content---docs-helloaff-811",
"metadata": "metadata---docs-hello-956-741",
},
"/docs:route-52d": {
- "component": "component---theme-doc-page-1-be-9be",
+ "__comp": "__comp---theme-doc-page-1-be-9be",
"docsMetadata": "docsMetadata---docs-routef-34-881",
},
"docs/foo/baz-070": {
- "component": "component---theme-doc-item-178-a40",
+ "__comp": "__comp---theme-doc-item-178-a40",
"content": "content---docs-foo-baz-8-ce-61e",
"metadata": "metadata---docs-foo-baz-2-cf-fa7",
},
@@ -159,14 +159,14 @@ export default [
exports[`loadRoutes loads route config with empty (but valid) path string 1`] = `
{
"registry": {
- "component---hello-world-jse-0-f-b6c": {
- "loader": "() => import(/* webpackChunkName: 'component---hello-world-jse-0-f-b6c' */ 'hello/world.js')",
+ "__comp---hello-world-jse-0-f-b6c": {
+ "loader": "() => import(/* webpackChunkName: '__comp---hello-world-jse-0-f-b6c' */ 'hello/world.js')",
"modulePath": "hello/world.js",
},
},
"routesChunkNames": {
"-b2a": {
- "component": "component---hello-world-jse-0-f-b6c",
+ "__comp": "__comp---hello-world-jse-0-f-b6c",
},
},
"routesConfig": "import React from 'react';
diff --git a/packages/docusaurus/src/server/plugins/index.ts b/packages/docusaurus/src/server/plugins/index.ts
index 5fe5990cc5..d63b95589b 100644
--- a/packages/docusaurus/src/server/plugins/index.ts
+++ b/packages/docusaurus/src/server/plugins/index.ts
@@ -131,7 +131,7 @@ export async function loadPlugins(context: LoadContext): Promise<{
...finalRouteConfig,
modules: {
...finalRouteConfig.modules,
- __routeContextModule: pluginRouteContextModulePath,
+ __context: pluginRouteContextModulePath,
},
});
},
diff --git a/packages/docusaurus/src/server/routes.ts b/packages/docusaurus/src/server/routes.ts
index 48fe2722d4..22fb44bbd7 100644
--- a/packages/docusaurus/src/server/routes.ts
+++ b/packages/docusaurus/src/server/routes.ts
@@ -151,7 +151,10 @@ const isModule = (value: unknown): value is Module =>
// eslint-disable-next-line no-underscore-dangle
!!(value as {[key: string]: unknown})?.__import);
-/** Takes a {@link Module} and returns the string path it represents. */
+/**
+ * Takes a {@link Module} (which is nothing more than a path plus some metadata
+ * like query) and returns the string path it represents.
+ */
function getModulePath(target: Module): string {
if (typeof target === 'string') {
return target;
@@ -241,7 +244,7 @@ This could lead to non-deterministic routing behavior.`;
/**
* This is the higher level overview of route code generation. For each route
- * config node, it return the node's serialized form, and mutate `registry`,
+ * config node, it returns the node's serialized form, and mutates `registry`,
* `routesPaths`, and `routesChunkNames` accordingly.
*/
function genRouteCode(routeConfig: RouteConfig, res: LoadedRoutes): string {
@@ -268,7 +271,8 @@ ${JSON.stringify(routeConfig)}`,
const routeHash = simpleHash(JSON.stringify(routeConfig), 3);
res.routesChunkNames[`${routePath}-${routeHash}`] = {
- ...genChunkNames({component}, 'component', component, res),
+ // Avoid clash with a prop called "component"
+ ...genChunkNames({__comp: component}, 'component', component, res),
...genChunkNames(modules, 'module', routePath, res),
};
@@ -297,7 +301,7 @@ export async function loadRoutes(
): Promise {
handleDuplicateRoutes(routeConfigs, onDuplicateRoutes);
const res: LoadedRoutes = {
- // To be written
+ // To be written by `genRouteCode`
routesConfig: '',
routesChunkNames: {},
registry: {},