mirror of
https://github.com/facebook/docusaurus.git
synced 2025-07-25 20:48:50 +02:00
refactor(core): add comments for react-loadable logic (#7075)
* refactor(core): add comments for react-loadable logic * fix test
This commit is contained in:
parent
13e7de853e
commit
04affa60b6
6 changed files with 77 additions and 61 deletions
|
@ -21,77 +21,97 @@ export default function ComponentCreator(
|
||||||
if (path === '*') {
|
if (path === '*') {
|
||||||
return Loadable({
|
return Loadable({
|
||||||
loading: Loading,
|
loading: Loading,
|
||||||
loader: async () => {
|
loader: () =>
|
||||||
const NotFound = (await import('@theme/NotFound')).default;
|
import('@theme/NotFound').then(({default: NotFound}) => (props) => (
|
||||||
return (props) => (
|
|
||||||
// Is there a better API for this?
|
|
||||||
<RouteContextProvider
|
<RouteContextProvider
|
||||||
|
// Do we want a better name than native-default?
|
||||||
value={{plugin: {name: 'native', id: 'default'}}}>
|
value={{plugin: {name: 'native', id: 'default'}}}>
|
||||||
<NotFound {...(props as never)} />
|
<NotFound {...(props as never)} />
|
||||||
</RouteContextProvider>
|
</RouteContextProvider>
|
||||||
);
|
)),
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const chunkNames = routesChunkNames[`${path}-${hash}`]!;
|
const chunkNames = routesChunkNames[`${path}-${hash}`]!;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const optsLoader: {[key: string]: () => Promise<any>} = {};
|
const loader: {[key: string]: () => Promise<any>} = {};
|
||||||
const optsModules: string[] = [];
|
const modules: string[] = [];
|
||||||
const optsWebpack: 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);
|
const flatChunkNames = flat(chunkNames);
|
||||||
Object.entries(flatChunkNames).forEach(([key, chunkName]) => {
|
Object.entries(flatChunkNames).forEach(([keyPath, chunkName]) => {
|
||||||
const chunkRegistry = registry[chunkName];
|
const chunkRegistry = registry[chunkName];
|
||||||
if (chunkRegistry) {
|
if (chunkRegistry) {
|
||||||
// eslint-disable-next-line prefer-destructuring
|
// eslint-disable-next-line prefer-destructuring
|
||||||
optsLoader[key] = chunkRegistry[0];
|
loader[keyPath] = chunkRegistry[0];
|
||||||
optsModules.push(chunkRegistry[1]);
|
modules.push(chunkRegistry[1]);
|
||||||
optsWebpack.push(chunkRegistry[2]);
|
optsWebpack.push(chunkRegistry[2]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return Loadable.Map({
|
return Loadable.Map({
|
||||||
loading: Loading,
|
loading: Loading,
|
||||||
loader: optsLoader,
|
loader,
|
||||||
modules: optsModules,
|
modules,
|
||||||
webpack: () => optsWebpack,
|
webpack: () => optsWebpack,
|
||||||
render: (loaded, props) => {
|
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));
|
const loadedModules = JSON.parse(JSON.stringify(chunkNames));
|
||||||
Object.keys(loaded).forEach((key) => {
|
Object.entries(loaded).forEach(([keyPath, loadedModule]) => {
|
||||||
const newComp = loaded[key].default;
|
// JSON modules are also loaded as `{ default: ... }` (`import()`
|
||||||
if (!newComp) {
|
// 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(
|
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.`,
|
`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') {
|
// A module can be a primitive, for example, if the user stored a string
|
||||||
Object.keys(loaded[key])
|
// 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')
|
.filter((k) => k !== 'default')
|
||||||
.forEach((nonDefaultKey) => {
|
.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;
|
let val = loadedModules;
|
||||||
const keyPath = key.split('.');
|
const keyPaths = keyPath.split('.');
|
||||||
keyPath.slice(0, -1).forEach((k) => {
|
keyPaths.slice(0, -1).forEach((k) => {
|
||||||
val = val[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 */
|
/* eslint-disable no-underscore-dangle */
|
||||||
const routeContextModule = loadedModules.__routeContextModule;
|
const Component = loadedModules.__comp;
|
||||||
delete loadedModules.__routeContextModule;
|
delete loadedModules.__comp;
|
||||||
|
const routeContext = loadedModules.__context;
|
||||||
|
delete loadedModules.__context;
|
||||||
/* eslint-enable no-underscore-dangle */
|
/* eslint-enable no-underscore-dangle */
|
||||||
|
|
||||||
// Is there any way to put this RouteContextProvider upper in the tree?
|
// Is there any way to put this RouteContextProvider upper in the tree?
|
||||||
return (
|
return (
|
||||||
<RouteContextProvider value={routeContextModule}>
|
<RouteContextProvider value={routeContext}>
|
||||||
<Component {...loadedModules} {...props} />
|
<Component {...loadedModules} {...props} />
|
||||||
</RouteContextProvider>
|
</RouteContextProvider>
|
||||||
);
|
);
|
||||||
|
|
|
@ -5,21 +5,13 @@
|
||||||
* LICENSE file in the root directory of this source tree.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function support(feature: string) {
|
function supports(feature: string) {
|
||||||
if (typeof document === 'undefined') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fakeLink = document.createElement('link');
|
|
||||||
try {
|
try {
|
||||||
if (fakeLink.relList && typeof fakeLink.relList.supports === 'function') {
|
const fakeLink = document.createElement('link');
|
||||||
return fakeLink.relList.supports(feature);
|
return fakeLink.relList?.supports?.(feature);
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function linkPrefetchStrategy(url: string) {
|
function linkPrefetchStrategy(url: string) {
|
||||||
|
@ -61,7 +53,7 @@ function xhrPrefetchStrategy(url: string): Promise<void> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const supportedPrefetchStrategy = support('prefetch')
|
const supportedPrefetchStrategy = supports('prefetch')
|
||||||
? linkPrefetchStrategy
|
? linkPrefetchStrategy
|
||||||
: xhrPrefetchStrategy;
|
: xhrPrefetchStrategy;
|
||||||
|
|
||||||
|
|
|
@ -78,14 +78,14 @@ async function doRender(locals: Locals & {path: string}) {
|
||||||
const location = routesLocation[locals.path]!;
|
const location = routesLocation[locals.path]!;
|
||||||
await preload(routes, location);
|
await preload(routes, location);
|
||||||
const modules = new Set<string>();
|
const modules = new Set<string>();
|
||||||
const context = {};
|
const routerContext = {};
|
||||||
const helmetContext = {};
|
const helmetContext = {};
|
||||||
|
|
||||||
const linksCollector = createStatefulLinksCollector();
|
const linksCollector = createStatefulLinksCollector();
|
||||||
const appHtml = ReactDOMServer.renderToString(
|
const appHtml = ReactDOMServer.renderToString(
|
||||||
<Loadable.Capture report={(moduleName) => modules.add(moduleName)}>
|
<Loadable.Capture report={(moduleName) => modules.add(moduleName)}>
|
||||||
<HelmetProvider context={helmetContext}>
|
<HelmetProvider context={helmetContext}>
|
||||||
<StaticRouter location={location} context={context}>
|
<StaticRouter location={location} context={routerContext}>
|
||||||
<LinksCollectorProvider linksCollector={linksCollector}>
|
<LinksCollectorProvider linksCollector={linksCollector}>
|
||||||
<App />
|
<App />
|
||||||
</LinksCollectorProvider>
|
</LinksCollectorProvider>
|
||||||
|
|
|
@ -12,8 +12,8 @@ This could lead to non-deterministic routing behavior."
|
||||||
exports[`loadRoutes loads flat route config 1`] = `
|
exports[`loadRoutes loads flat route config 1`] = `
|
||||||
{
|
{
|
||||||
"registry": {
|
"registry": {
|
||||||
"component---theme-blog-list-pagea-6-a-7ba": {
|
"__comp---theme-blog-list-pagea-6-a-7ba": {
|
||||||
"loader": "() => import(/* webpackChunkName: 'component---theme-blog-list-pagea-6-a-7ba' */ '@theme/BlogListPage')",
|
"loader": "() => import(/* webpackChunkName: '__comp---theme-blog-list-pagea-6-a-7ba' */ '@theme/BlogListPage')",
|
||||||
"modulePath": "@theme/BlogListPage",
|
"modulePath": "@theme/BlogListPage",
|
||||||
},
|
},
|
||||||
"content---blog-0-b-4-09e": {
|
"content---blog-0-b-4-09e": {
|
||||||
|
@ -31,7 +31,7 @@ exports[`loadRoutes loads flat route config 1`] = `
|
||||||
},
|
},
|
||||||
"routesChunkNames": {
|
"routesChunkNames": {
|
||||||
"/blog-599": {
|
"/blog-599": {
|
||||||
"component": "component---theme-blog-list-pagea-6-a-7ba",
|
"__comp": "__comp---theme-blog-list-pagea-6-a-7ba",
|
||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
"content": "content---blog-0-b-4-09e",
|
"content": "content---blog-0-b-4-09e",
|
||||||
|
@ -71,12 +71,12 @@ export default [
|
||||||
exports[`loadRoutes loads nested route config 1`] = `
|
exports[`loadRoutes loads nested route config 1`] = `
|
||||||
{
|
{
|
||||||
"registry": {
|
"registry": {
|
||||||
"component---theme-doc-item-178-a40": {
|
"__comp---theme-doc-item-178-a40": {
|
||||||
"loader": "() => import(/* webpackChunkName: 'component---theme-doc-item-178-a40' */ '@theme/DocItem')",
|
"loader": "() => import(/* webpackChunkName: '__comp---theme-doc-item-178-a40' */ '@theme/DocItem')",
|
||||||
"modulePath": "@theme/DocItem",
|
"modulePath": "@theme/DocItem",
|
||||||
},
|
},
|
||||||
"component---theme-doc-page-1-be-9be": {
|
"__comp---theme-doc-page-1-be-9be": {
|
||||||
"loader": "() => import(/* webpackChunkName: 'component---theme-doc-page-1-be-9be' */ '@theme/DocPage')",
|
"loader": "() => import(/* webpackChunkName: '__comp---theme-doc-page-1-be-9be' */ '@theme/DocPage')",
|
||||||
"modulePath": "@theme/DocPage",
|
"modulePath": "@theme/DocPage",
|
||||||
},
|
},
|
||||||
"content---docs-foo-baz-8-ce-61e": {
|
"content---docs-foo-baz-8-ce-61e": {
|
||||||
|
@ -102,16 +102,16 @@ exports[`loadRoutes loads nested route config 1`] = `
|
||||||
},
|
},
|
||||||
"routesChunkNames": {
|
"routesChunkNames": {
|
||||||
"/docs/hello-44b": {
|
"/docs/hello-44b": {
|
||||||
"component": "component---theme-doc-item-178-a40",
|
"__comp": "__comp---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-52d": {
|
"/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",
|
"docsMetadata": "docsMetadata---docs-routef-34-881",
|
||||||
},
|
},
|
||||||
"docs/foo/baz-070": {
|
"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",
|
"content": "content---docs-foo-baz-8-ce-61e",
|
||||||
"metadata": "metadata---docs-foo-baz-2-cf-fa7",
|
"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`] = `
|
exports[`loadRoutes loads route config with empty (but valid) path string 1`] = `
|
||||||
{
|
{
|
||||||
"registry": {
|
"registry": {
|
||||||
"component---hello-world-jse-0-f-b6c": {
|
"__comp---hello-world-jse-0-f-b6c": {
|
||||||
"loader": "() => import(/* webpackChunkName: 'component---hello-world-jse-0-f-b6c' */ 'hello/world.js')",
|
"loader": "() => import(/* webpackChunkName: '__comp---hello-world-jse-0-f-b6c' */ 'hello/world.js')",
|
||||||
"modulePath": "hello/world.js",
|
"modulePath": "hello/world.js",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"routesChunkNames": {
|
"routesChunkNames": {
|
||||||
"-b2a": {
|
"-b2a": {
|
||||||
"component": "component---hello-world-jse-0-f-b6c",
|
"__comp": "__comp---hello-world-jse-0-f-b6c",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"routesConfig": "import React from 'react';
|
"routesConfig": "import React from 'react';
|
||||||
|
|
|
@ -131,7 +131,7 @@ export async function loadPlugins(context: LoadContext): Promise<{
|
||||||
...finalRouteConfig,
|
...finalRouteConfig,
|
||||||
modules: {
|
modules: {
|
||||||
...finalRouteConfig.modules,
|
...finalRouteConfig.modules,
|
||||||
__routeContextModule: pluginRouteContextModulePath,
|
__context: pluginRouteContextModulePath,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
|
@ -151,7 +151,10 @@ const isModule = (value: unknown): value is Module =>
|
||||||
// eslint-disable-next-line no-underscore-dangle
|
// eslint-disable-next-line no-underscore-dangle
|
||||||
!!(value as {[key: string]: unknown})?.__import);
|
!!(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 {
|
function getModulePath(target: Module): string {
|
||||||
if (typeof target === 'string') {
|
if (typeof target === 'string') {
|
||||||
return target;
|
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
|
* 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.
|
* `routesPaths`, and `routesChunkNames` accordingly.
|
||||||
*/
|
*/
|
||||||
function genRouteCode(routeConfig: RouteConfig, res: LoadedRoutes): string {
|
function genRouteCode(routeConfig: RouteConfig, res: LoadedRoutes): string {
|
||||||
|
@ -268,7 +271,8 @@ ${JSON.stringify(routeConfig)}`,
|
||||||
|
|
||||||
const routeHash = simpleHash(JSON.stringify(routeConfig), 3);
|
const routeHash = simpleHash(JSON.stringify(routeConfig), 3);
|
||||||
res.routesChunkNames[`${routePath}-${routeHash}`] = {
|
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),
|
...genChunkNames(modules, 'module', routePath, res),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -297,7 +301,7 @@ export async function loadRoutes(
|
||||||
): Promise<LoadedRoutes> {
|
): Promise<LoadedRoutes> {
|
||||||
handleDuplicateRoutes(routeConfigs, onDuplicateRoutes);
|
handleDuplicateRoutes(routeConfigs, onDuplicateRoutes);
|
||||||
const res: LoadedRoutes = {
|
const res: LoadedRoutes = {
|
||||||
// To be written
|
// To be written by `genRouteCode`
|
||||||
routesConfig: '',
|
routesConfig: '',
|
||||||
routesChunkNames: {},
|
routesChunkNames: {},
|
||||||
registry: {},
|
registry: {},
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue