diff --git a/.eslintrc.js b/.eslintrc.js index 214c41daf3..9b5f970108 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -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, diff --git a/packages/docusaurus-plugin-content-blog/src/index.js b/packages/docusaurus-plugin-content-blog/src/index.js index 3227379059..d299fd721c 100644 --- a/packages/docusaurus-plugin-content-blog/src/index.js +++ b/packages/docusaurus-plugin-content-blog/src/index.js @@ -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, + }, }); }); } diff --git a/packages/docusaurus-plugin-content-docs/src/index.js b/packages/docusaurus-plugin-content-docs/src/index.js index ce4d2894ec..5965771fcd 100644 --- a/packages/docusaurus-plugin-content-docs/src/index.js +++ b/packages/docusaurus-plugin-content-docs/src/index.js @@ -215,7 +215,9 @@ class DocusaurusPluginContentDocs { path: metadataItem.permalink, component: docItemComponent, metadata: metadataItem, - modules: [metadataItem.source], + modules: { + content: metadataItem.source, + }, })), }); } diff --git a/packages/docusaurus-plugin-content-pages/src/index.js b/packages/docusaurus-plugin-content-pages/src/index.js index 0b74e5ff78..d865711566 100644 --- a/packages/docusaurus-plugin-content-pages/src/index.js +++ b/packages/docusaurus-plugin-content-pages/src/index.js @@ -104,7 +104,9 @@ class DocusaurusPluginContentPages { path: permalink, component, metadata: metadataItem, - modules: [source], + modules: { + content: source, + }, }); }); } diff --git a/packages/docusaurus-utils/src/__tests__/index.test.js b/packages/docusaurus-utils/src/__tests__/index.test.js index 8d8e154d44..ad48fae6c0 100644 --- a/packages/docusaurus-utils/src/__tests__/index.test.js +++ b/packages/docusaurus-utils/src/__tests__/index.test.js @@ -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', () => { diff --git a/packages/docusaurus-utils/src/index.js b/packages/docusaurus-utils/src/index.js index df9025de26..b682eea4ae 100644 --- a/packages/docusaurus-utils/src/index.js +++ b/packages/docusaurus-utils/src/index.js @@ -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) { diff --git a/packages/docusaurus/lib/client/exports/ComponentCreator.js b/packages/docusaurus/lib/client/exports/ComponentCreator.js new file mode 100644 index 0000000000..5efc1dfe8f --- /dev/null +++ b/packages/docusaurus/lib/client/exports/ComponentCreator.js @@ -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 ; + }, + }); +} + +export default ComponentCreator; diff --git a/packages/docusaurus/lib/default-theme/BlogPage/index.js b/packages/docusaurus/lib/default-theme/BlogPage/index.js index d96d4bdedd..e42899521c 100644 --- a/packages/docusaurus/lib/default-theme/BlogPage/index.js +++ b/packages/docusaurus/lib/default-theme/BlogPage/index.js @@ -18,7 +18,7 @@ function BlogPage(props) { const {baseUrl, favicon} = siteConfig; const { metadata: {posts = []}, - modules: BlogPosts, + entries: BlogPosts, } = props; return ( diff --git a/packages/docusaurus/lib/default-theme/BlogPost/index.js b/packages/docusaurus/lib/default-theme/BlogPost/index.js index 7079073d63..072fa84192 100644 --- a/packages/docusaurus/lib/default-theme/BlogPost/index.js +++ b/packages/docusaurus/lib/default-theme/BlogPost/index.js @@ -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 ( diff --git a/packages/docusaurus/lib/default-theme/DocBody/index.js b/packages/docusaurus/lib/default-theme/DocBody/index.js index ccae3f62b9..6835f2beb7 100644 --- a/packages/docusaurus/lib/default-theme/DocBody/index.js +++ b/packages/docusaurus/lib/default-theme/DocBody/index.js @@ -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 (
diff --git a/packages/docusaurus/lib/default-theme/Pages/index.js b/packages/docusaurus/lib/default-theme/Pages/index.js index f7328cd2d7..ba26a08e7a 100644 --- a/packages/docusaurus/lib/default-theme/Pages/index.js +++ b/packages/docusaurus/lib/default-theme/Pages/index.js @@ -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 ( diff --git a/packages/docusaurus/lib/server/load/index.js b/packages/docusaurus/lib/server/load/index.js index 254dcb890b..7ffb853c0d 100644 --- a/packages/docusaurus/lib/server/load/index.js +++ b/packages/docusaurus/lib/server/load/index.js @@ -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'] diff --git a/packages/docusaurus/lib/server/load/plugins.js b/packages/docusaurus/lib/server/load/plugins.js index 28a8efe7e2..f74189bb39 100644 --- a/packages/docusaurus/lib/server/load/plugins.js +++ b/packages/docusaurus/lib/server/load/plugins.js @@ -63,6 +63,7 @@ module.exports = async function loadPlugins({pluginConfigs = [], context}) { // 3. Plugin lifecycle - contentLoaded const pluginsRouteConfigs = []; + const actions = { addRoute: config => pluginsRouteConfigs.push(config), }; diff --git a/packages/docusaurus/lib/server/load/routes.js b/packages/docusaurus/lib/server/load/routes.js index 6240893ed0..429862df75 100644 --- a/packages/docusaurus/lib/server/load/routes.js +++ b/packages/docusaurus/lib/server/load/routes.js @@ -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 ( - - ); - } -})\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,