mirror of
https://github.com/facebook/docusaurus.git
synced 2025-05-15 18:17:35 +02:00
feat(v2): implement ComponentCreator (#1366)
* v2(feat): convert blog to view-driven content queries * feat(v2): port blog to use ContentRenderer * misc(v2): fix test and change ContentRenderer url * avoid chunkName collision * avoid chunkname collision more * fix(v2): fix content-renderer ssr problem (#1367) * wip * avoid chunk names collision * ContentRenderer is a wrapper for Loadable * convert docs and pages * nits and rename * rename routeModules -> modules * remove lodash from component creator * resolve chunk not being picked up correctly * add comment for explanation * small refactoring
This commit is contained in:
parent
0ac2441d23
commit
96cb4672d5
14 changed files with 220 additions and 80 deletions
|
@ -45,6 +45,7 @@ module.exports = {
|
|||
'jsx-a11y/click-events-have-key-events': OFF, // Revisit in future™
|
||||
'jsx-a11y/no-noninteractive-element-interactions': OFF, // Revisit in future™
|
||||
'no-console': OFF,
|
||||
'no-underscore-dangle': OFF,
|
||||
'react/jsx-closing-bracket-location': OFF, // Conflicts with Prettier.
|
||||
'react/jsx-filename-extension': OFF,
|
||||
'react/jsx-one-expression-per-line': OFF,
|
||||
|
|
|
@ -129,13 +129,18 @@ class DocusaurusPluginContentBlog {
|
|||
path: permalink,
|
||||
component: blogPageComponent,
|
||||
metadata: metadataItem,
|
||||
modules: metadataItem.posts.map(post => ({
|
||||
path: post.source,
|
||||
query: {
|
||||
truncated: true,
|
||||
},
|
||||
})),
|
||||
modules: {
|
||||
entries: metadataItem.posts.map(post => ({
|
||||
// To tell routes.js this is an import and not a nested object to recurse.
|
||||
__import: true,
|
||||
path: post.source,
|
||||
query: {
|
||||
truncated: true,
|
||||
},
|
||||
})),
|
||||
},
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -143,7 +148,9 @@ class DocusaurusPluginContentBlog {
|
|||
path: permalink,
|
||||
component: blogPostComponent,
|
||||
metadata: metadataItem,
|
||||
modules: [metadataItem.source],
|
||||
modules: {
|
||||
content: metadataItem.source,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -215,7 +215,9 @@ class DocusaurusPluginContentDocs {
|
|||
path: metadataItem.permalink,
|
||||
component: docItemComponent,
|
||||
metadata: metadataItem,
|
||||
modules: [metadataItem.source],
|
||||
modules: {
|
||||
content: metadataItem.source,
|
||||
},
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
|
|
@ -104,7 +104,9 @@ class DocusaurusPluginContentPages {
|
|||
path: permalink,
|
||||
component,
|
||||
metadata: metadataItem,
|
||||
modules: [source],
|
||||
modules: {
|
||||
content: source,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -33,7 +33,7 @@ describe('load utils', () => {
|
|||
|
||||
test('genComponentName', () => {
|
||||
const asserts = {
|
||||
'/': 'Index',
|
||||
'/': 'index',
|
||||
'/foo-bar': 'FooBar096',
|
||||
'/foo/bar': 'FooBar1Df',
|
||||
'/blog/2017/12/14/introducing-docusaurus':
|
||||
|
@ -51,7 +51,7 @@ describe('load utils', () => {
|
|||
test('docuHash', () => {
|
||||
const asserts = {
|
||||
'': '-d41',
|
||||
'/': 'Index',
|
||||
'/': 'index',
|
||||
'/foo-bar': 'foo-bar-096',
|
||||
'/foo/bar': 'foo-bar-1df',
|
||||
'/endi/lie': 'endi-lie-9fa',
|
||||
|
@ -81,7 +81,7 @@ describe('load utils', () => {
|
|||
});
|
||||
|
||||
test('genChunkName', () => {
|
||||
const asserts = {
|
||||
let asserts = {
|
||||
'/docs/adding-blog': 'docs-adding-blog-062',
|
||||
'/docs/versioning': 'docs-versioning-8a8',
|
||||
'/': 'index',
|
||||
|
@ -94,6 +94,20 @@ describe('load utils', () => {
|
|||
Object.keys(asserts).forEach(str => {
|
||||
expect(genChunkName(str)).toBe(asserts[str]);
|
||||
});
|
||||
|
||||
// Don't allow different chunk name for same path.
|
||||
expect(genChunkName('path/is/similar', 'oldPrefix')).toEqual(
|
||||
genChunkName('path/is/similar', 'newPrefix'),
|
||||
);
|
||||
|
||||
// Even with same preferred name, still different chunk name for different path
|
||||
asserts = {
|
||||
'/blog/1': 'blog-85-f-089',
|
||||
'/blog/2': 'blog-353-489',
|
||||
};
|
||||
Object.keys(asserts).forEach(str => {
|
||||
expect(genChunkName(str, undefined, 'blog')).toBe(asserts[str]);
|
||||
});
|
||||
});
|
||||
|
||||
test('idx', () => {
|
||||
|
|
|
@ -47,7 +47,7 @@ function encodePath(userpath) {
|
|||
*/
|
||||
function docuHash(str) {
|
||||
if (str === '/') {
|
||||
return 'Index';
|
||||
return 'index';
|
||||
}
|
||||
const shortHash = createHash('md5')
|
||||
.update(str)
|
||||
|
@ -63,7 +63,7 @@ function docuHash(str) {
|
|||
*/
|
||||
function genComponentName(pagePath) {
|
||||
if (pagePath === '/') {
|
||||
return 'Index';
|
||||
return 'index';
|
||||
}
|
||||
const pageHash = docuHash(pagePath);
|
||||
const pascalCase = _.flow(
|
||||
|
@ -88,9 +88,23 @@ function posixPath(str) {
|
|||
return str.replace(/\\/g, '/');
|
||||
}
|
||||
|
||||
function genChunkName(str, prefix) {
|
||||
const name = str === '/' ? 'index' : docuHash(str);
|
||||
return prefix ? `${prefix}---${name}` : name;
|
||||
const chunkNameCache = new Map();
|
||||
function genChunkName(modulePath, prefix, preferredName) {
|
||||
let chunkName = chunkNameCache.get(modulePath);
|
||||
if (!chunkName) {
|
||||
let str = modulePath;
|
||||
if (preferredName) {
|
||||
const shortHash = createHash('md5')
|
||||
.update(modulePath)
|
||||
.digest('hex')
|
||||
.substr(0, 3);
|
||||
str = `${preferredName}${shortHash}`;
|
||||
}
|
||||
const name = str === '/' ? 'index' : docuHash(str);
|
||||
chunkName = prefix ? `${prefix}---${name}` : name;
|
||||
chunkNameCache.set(modulePath, chunkName);
|
||||
}
|
||||
return chunkName;
|
||||
}
|
||||
|
||||
function idx(target, keyPaths) {
|
||||
|
|
84
packages/docusaurus/lib/client/exports/ComponentCreator.js
Normal file
84
packages/docusaurus/lib/client/exports/ComponentCreator.js
Normal file
|
@ -0,0 +1,84 @@
|
|||
/**
|
||||
* Copyright (c) 2017-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import Loadable from 'react-loadable';
|
||||
import Loading from '@theme/Loading';
|
||||
import routesAsyncModules from '@generated/routesAsyncModules';
|
||||
import registry from '@generated/registry';
|
||||
|
||||
function ComponentCreator(path) {
|
||||
const modules = routesAsyncModules[path];
|
||||
const originalModules = modules;
|
||||
const optsModules = [];
|
||||
const optsWebpack = [];
|
||||
const mappedModules = {};
|
||||
|
||||
// Transform an object of
|
||||
// {
|
||||
// a: 'foo',
|
||||
// b: { c: 'bar' },
|
||||
// d: ['baz', 'qux']
|
||||
// }
|
||||
// into
|
||||
// {
|
||||
// a: () => import('foo'),
|
||||
// b.c: () => import('bar'),
|
||||
// d.0: () => import('baz'),
|
||||
// d.1: () => import('qux'),
|
||||
// }
|
||||
// for React Loadable to process.
|
||||
function traverseModules(module, keys) {
|
||||
if (Array.isArray(module)) {
|
||||
module.forEach((value, index) => {
|
||||
traverseModules(value, [...keys, index]);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof module === 'object') {
|
||||
Object.keys(module).forEach(key => {
|
||||
traverseModules(module[key], [...keys, key]);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
mappedModules[keys.join('.')] = registry[module].importStatement;
|
||||
optsModules.push(registry[module].module);
|
||||
optsWebpack.push(registry[module].webpack);
|
||||
}
|
||||
|
||||
traverseModules(modules, []);
|
||||
|
||||
return Loadable.Map({
|
||||
loading: Loading,
|
||||
loader: mappedModules,
|
||||
// We need to provide opts.modules and opts.webpack to React Loadable
|
||||
// Our loader is now dynamical, the react-loadable/babel won't do the heavy lifting for us.
|
||||
// https://github.com/jamiebuilds/react-loadable#declaring-which-modules-are-being-loaded
|
||||
modules: optsModules,
|
||||
webpack: () => optsWebpack,
|
||||
render: (loaded, props) => {
|
||||
// Transform back loaded modules back into the original structure.
|
||||
const loadedModules = originalModules;
|
||||
Object.keys(loaded).forEach(key => {
|
||||
let val = loadedModules;
|
||||
const keyPath = key.split('.');
|
||||
for (let i = 0; i < keyPath.length - 1; i += 1) {
|
||||
val = val[keyPath[i]];
|
||||
}
|
||||
val[keyPath[keyPath.length - 1]] = loaded[key].default;
|
||||
});
|
||||
|
||||
const Component = loadedModules.component;
|
||||
delete loadedModules.component;
|
||||
return <Component {...loadedModules} {...props} />;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export default ComponentCreator;
|
|
@ -18,7 +18,7 @@ function BlogPage(props) {
|
|||
const {baseUrl, favicon} = siteConfig;
|
||||
const {
|
||||
metadata: {posts = []},
|
||||
modules: BlogPosts,
|
||||
entries: BlogPosts,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
|
|
|
@ -19,8 +19,8 @@ function BlogPost(props) {
|
|||
);
|
||||
const {baseUrl, favicon} = siteConfig;
|
||||
const {language, title} = contextMetadata;
|
||||
const {modules, metadata} = props;
|
||||
const BlogPostContents = modules[0];
|
||||
const {content, metadata} = props;
|
||||
const BlogPostContents = content;
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
|
|
|
@ -14,14 +14,14 @@ import Head from '@docusaurus/Head';
|
|||
import styles from './styles.module.css';
|
||||
|
||||
function DocBody(props) {
|
||||
const {metadata, modules} = props;
|
||||
const {metadata, content} = props;
|
||||
const {language, version} = metadata;
|
||||
const context = useContext(DocusaurusContext);
|
||||
useEffect(() => {
|
||||
context.setContext({metadata});
|
||||
}, []);
|
||||
|
||||
const DocContents = modules[0];
|
||||
const DocContents = content;
|
||||
return (
|
||||
<div className={styles.docBody}>
|
||||
<Head>
|
||||
|
|
|
@ -12,12 +12,12 @@ import Layout from '@theme/Layout'; // eslint-disable-line
|
|||
|
||||
import DocusaurusContext from '@docusaurus/context';
|
||||
|
||||
function Pages({modules}) {
|
||||
function Pages({content}) {
|
||||
const context = useContext(DocusaurusContext);
|
||||
const {metadata = {}, siteConfig = {}} = context;
|
||||
const {baseUrl, favicon} = siteConfig;
|
||||
const {language} = metadata;
|
||||
const PageContents = modules[0];
|
||||
const PageContents = content;
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
|
|
|
@ -58,6 +58,7 @@ module.exports = async function load(siteDir, cliOptions = {}) {
|
|||
|
||||
// Routing
|
||||
const {
|
||||
registry,
|
||||
routesAsyncModules,
|
||||
routesConfig,
|
||||
routesMetadata,
|
||||
|
@ -65,12 +66,19 @@ module.exports = async function load(siteDir, cliOptions = {}) {
|
|||
routesPaths,
|
||||
} = await loadRoutes(pluginsRouteConfigs);
|
||||
|
||||
// Mapping of routePath -> metadataPath. Example: '/blog' -> '@generated/metadata/blog-c06.json'
|
||||
// Very useful to know which json metadata file is related to certain route
|
||||
await generate(
|
||||
generatedFilesDir,
|
||||
'routesMetadataPath.json',
|
||||
JSON.stringify(routesMetadataPath, null, 2),
|
||||
'registry.js',
|
||||
`export default {
|
||||
${Object.keys(registry)
|
||||
.map(
|
||||
key => ` '${key}': {
|
||||
'importStatement': ${registry[key].importStatement},
|
||||
'module': '${registry[key].modulePath}',
|
||||
'webpack': require.resolveWeak('${registry[key].modulePath}'),
|
||||
},`,
|
||||
)
|
||||
.join('\n')}};\n`,
|
||||
);
|
||||
|
||||
// Mapping of routePath -> async imported modules. Example: '/blog' -> ['@theme/BlogPage']
|
||||
|
|
|
@ -63,6 +63,7 @@ module.exports = async function loadPlugins({pluginConfigs = [], context}) {
|
|||
|
||||
// 3. Plugin lifecycle - contentLoaded
|
||||
const pluginsRouteConfigs = [];
|
||||
|
||||
const actions = {
|
||||
addRoute: config => pluginsRouteConfigs.push(config),
|
||||
};
|
||||
|
|
|
@ -7,25 +7,27 @@
|
|||
|
||||
const {genChunkName, docuHash} = require('@docusaurus/utils');
|
||||
const {stringify} = require('querystring');
|
||||
const _ = require('lodash');
|
||||
|
||||
async function loadRoutes(pluginsRouteConfigs) {
|
||||
const routesImports = [
|
||||
`import React from 'react';`,
|
||||
`import Loadable from 'react-loadable';`,
|
||||
`import Loading from '@theme/Loading';`,
|
||||
`import NotFound from '@theme/NotFound';`,
|
||||
`import ComponentCreator from '@docusaurus/ComponentCreator';`,
|
||||
];
|
||||
// Routes paths. Example: ['/', '/docs', '/blog/2017/09/03/test']
|
||||
const routesPaths = [];
|
||||
const addRoutesPath = routePath => {
|
||||
routesPaths.push(routePath);
|
||||
};
|
||||
|
||||
// Mapping of routePath -> metadataPath. Example: '/blog' -> '@generated/metadata/blog-c06.json'
|
||||
const routesMetadataPath = {};
|
||||
const addRoutesMetadataPath = routePath => {
|
||||
const fileName = `${docuHash(routePath)}.json`;
|
||||
routesMetadataPath[routePath] = `@generated/metadata/${fileName}`;
|
||||
};
|
||||
|
||||
// Mapping of routePath -> metadata. Example: '/blog' -> { isBlogPage: true, permalink: '/blog' }
|
||||
const routesMetadata = {};
|
||||
const addRoutesMetadata = (routePath, metadata) => {
|
||||
|
@ -33,13 +35,21 @@ async function loadRoutes(pluginsRouteConfigs) {
|
|||
routesMetadata[routePath] = metadata;
|
||||
}
|
||||
};
|
||||
|
||||
const registry = {};
|
||||
|
||||
// Mapping of routePath -> async imported modules. Example: '/blog' -> ['@theme/BlogPage']
|
||||
const routesAsyncModules = {};
|
||||
const addRoutesAsyncModule = (routePath, module) => {
|
||||
const addRoutesAsyncModule = (routePath, key, importChunk) => {
|
||||
// TODO: Port other plugins to use modules and not rely on this.
|
||||
if (!routesAsyncModules[routePath]) {
|
||||
routesAsyncModules[routePath] = [];
|
||||
routesAsyncModules[routePath] = {};
|
||||
}
|
||||
routesAsyncModules[routePath].push(module);
|
||||
routesAsyncModules[routePath][key] = importChunk.chunkName;
|
||||
registry[importChunk.chunkName] = {
|
||||
importStatement: importChunk.importStatement,
|
||||
modulePath: importChunk.modulePath,
|
||||
};
|
||||
};
|
||||
|
||||
// This is the higher level overview of route code generation
|
||||
|
@ -48,7 +58,7 @@ async function loadRoutes(pluginsRouteConfigs) {
|
|||
path: routePath,
|
||||
component,
|
||||
metadata,
|
||||
modules = [],
|
||||
modules = {},
|
||||
routes,
|
||||
} = routeConfig;
|
||||
|
||||
|
@ -58,9 +68,9 @@ async function loadRoutes(pluginsRouteConfigs) {
|
|||
|
||||
// Given an input (object or string), get the import path str
|
||||
const getModulePath = target => {
|
||||
const isObj = typeof target === 'object';
|
||||
const importStr = isObj ? target.path : target;
|
||||
const importStr = _.isObject(target) ? target.path : target;
|
||||
const queryStr = target.query ? `?${stringify(target.query)}` : '';
|
||||
|
||||
return `${importStr}${queryStr}`;
|
||||
};
|
||||
|
||||
|
@ -68,71 +78,67 @@ async function loadRoutes(pluginsRouteConfigs) {
|
|||
throw new Error(`path: ${routePath} need a component`);
|
||||
}
|
||||
const componentPath = getModulePath(component);
|
||||
addRoutesAsyncModule(routePath, componentPath);
|
||||
|
||||
const genImportStr = (modulePath, prefix, name) => {
|
||||
const chunkName = genChunkName(name || modulePath, prefix);
|
||||
const genImportChunk = (modulePath, prefix, name) => {
|
||||
const chunkName = genChunkName(modulePath, prefix, name);
|
||||
const finalStr = JSON.stringify(modulePath);
|
||||
return `() => import(/* webpackChunkName: '${chunkName}' */ ${finalStr})`;
|
||||
return {
|
||||
chunkName,
|
||||
modulePath,
|
||||
importStatement: `() => import(/* webpackChunkName: '${chunkName}' */ ${finalStr})`,
|
||||
};
|
||||
};
|
||||
|
||||
const componentChunk = genImportChunk(componentPath, 'component');
|
||||
addRoutesAsyncModule(routePath, 'component', componentChunk);
|
||||
|
||||
if (routes) {
|
||||
const componentStr = `Loadable({
|
||||
loader: ${genImportStr(componentPath, 'component')},
|
||||
loading: Loading
|
||||
})`;
|
||||
return `
|
||||
{
|
||||
path: '${routePath}',
|
||||
component: ${componentStr},
|
||||
component: ComponentCreator('${routePath}'),
|
||||
routes: [${routes.map(generateRouteCode).join(',')}],
|
||||
}`;
|
||||
}
|
||||
|
||||
const modulesImportStr = modules
|
||||
.map((module, i) => {
|
||||
const modulePath = getModulePath(module);
|
||||
addRoutesAsyncModule(routePath, modulePath);
|
||||
return `Mod${i}: ${genImportStr(modulePath, i, routePath)},`;
|
||||
})
|
||||
.join('\n');
|
||||
const modulesLoadedStr = modules
|
||||
.map((module, i) => `loaded.Mod${i}.default,`)
|
||||
.join('\n');
|
||||
function genRouteAsyncModule(value) {
|
||||
if (Array.isArray(value)) {
|
||||
return value.map(genRouteAsyncModule);
|
||||
}
|
||||
|
||||
let metadataImportStr = '';
|
||||
if (metadata) {
|
||||
const metadataPath = routesMetadataPath[routePath];
|
||||
addRoutesAsyncModule(routePath, metadataPath);
|
||||
metadataImportStr = `metadata: ${genImportStr(
|
||||
metadataPath,
|
||||
'metadata',
|
||||
if (_.isObject(value) && !value.__import) {
|
||||
const newValue = {};
|
||||
Object.keys(value).forEach(key => {
|
||||
newValue[key] = genRouteAsyncModule(value[key]);
|
||||
});
|
||||
return newValue;
|
||||
}
|
||||
|
||||
const importChunk = genImportChunk(
|
||||
getModulePath(value),
|
||||
'module',
|
||||
routePath,
|
||||
)},`;
|
||||
);
|
||||
registry[importChunk.chunkName] = {
|
||||
importStatement: importChunk.importStatement,
|
||||
modulePath: importChunk.modulePath,
|
||||
};
|
||||
return importChunk.chunkName;
|
||||
}
|
||||
|
||||
const componentStr = `Loadable.Map({
|
||||
loader: {
|
||||
${modulesImportStr}
|
||||
${metadataImportStr}
|
||||
Component: ${genImportStr(componentPath, 'component')},
|
||||
},
|
||||
loading: Loading,
|
||||
render(loaded, props) {
|
||||
const Component = loaded.Component.default;
|
||||
const metadata = loaded.metadata || {};
|
||||
const modules = [${modulesLoadedStr}];
|
||||
return (
|
||||
<Component {...props} metadata={metadata} modules={modules}/>
|
||||
);
|
||||
}
|
||||
})\n`;
|
||||
_.assign(routesAsyncModules[routePath], genRouteAsyncModule(modules));
|
||||
|
||||
if (metadata) {
|
||||
const metadataPath = routesMetadataPath[routePath];
|
||||
const metadataChunk = genImportChunk(metadataPath, 'metadata', routePath);
|
||||
addRoutesAsyncModule(routePath, 'metadata', metadataChunk);
|
||||
}
|
||||
|
||||
return `
|
||||
{
|
||||
path: '${routePath}',
|
||||
exact: true,
|
||||
component: ${componentStr}
|
||||
component: ComponentCreator('${routePath}')
|
||||
}`;
|
||||
}
|
||||
|
||||
|
@ -152,6 +158,7 @@ export default [
|
|||
];\n`;
|
||||
|
||||
return {
|
||||
registry,
|
||||
routesAsyncModules,
|
||||
routesConfig,
|
||||
routesMetadata,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue