diff --git a/packages/docusaurus/src/client/docusaurus.js b/packages/docusaurus/src/client/docusaurus.js index 379c5237a6..71d40de97c 100644 --- a/packages/docusaurus/src/client/docusaurus.js +++ b/packages/docusaurus/src/client/docusaurus.js @@ -37,6 +37,8 @@ const docusaurus = { if (!canPrefetch(routePath)) { return false; } + // prevent future duplicate prefetch of routePath + fetched[routePath] = true; // Find all webpack chunk names needed const matches = matchRoutes(routes, routePath); @@ -51,12 +53,14 @@ const docusaurus = { }, []); // Prefetch all webpack chunk assets file needed - const chunkAssetsNeeded = chunkNamesNeeded.reduce((arr, chunkName) => { - const chunkAssets = window.__chunkMapping[chunkName] || []; - return arr.concat(chunkAssets); - }, []); - Promise.all(chunkAssetsNeeded.map(prefetchHelper)).then(() => { - fetched[routePath] = true; + chunkNamesNeeded.forEach(chunkName => { + // "__webpack_require__.gca" is a custom function provided by ChunkAssetPlugin + // Pass it the chunkName or chunkId you want to load and it will return the URL for that chunk + // eslint-disable-next-line no-undef + const chunkAsset = __webpack_require__.gca(chunkName); + if (chunkAsset) { + prefetchHelper(chunkAsset); + } }); return true; }, diff --git a/packages/docusaurus/src/client/serverEntry.js b/packages/docusaurus/src/client/serverEntry.js index e6cbaa3289..c19e9f4776 100644 --- a/packages/docusaurus/src/client/serverEntry.js +++ b/packages/docusaurus/src/client/serverEntry.js @@ -50,18 +50,6 @@ export default async function render(locals) { const manifestPath = path.join(generatedFilesDir, 'client-manifest.json'); const manifest = JSON.parse(await fs.readFile(manifestPath, 'utf8')); - // chunkName -> chunkAssets mapping. - const chunkManifestPath = path.join(generatedFilesDir, 'chunk-map.json'); - const chunkManifest = JSON.parse( - await fs.readFile(chunkManifestPath, 'utf8'), - ); - const chunkManifestScript = - ``; - // Get all required assets for this particular page based on client manifest information const modulesToBeLoaded = [...manifest.entrypoints, ...Array.from(modules)]; const bundles = getBundles(manifest, modulesToBeLoaded); @@ -74,7 +62,6 @@ export default async function render(locals) { { appHtml, baseUrl, - chunkManifestScript, htmlAttributes: htmlAttributes || '', bodyAttributes: bodyAttributes || '', headTags, diff --git a/packages/docusaurus/src/client/templates/ssr.html.template.js b/packages/docusaurus/src/client/templates/ssr.html.template.js index cca4486191..6df16c6848 100644 --- a/packages/docusaurus/src/client/templates/ssr.html.template.js +++ b/packages/docusaurus/src/client/templates/ssr.html.template.js @@ -13,7 +13,6 @@ module.exports = ` <%- headTags %> - <%- chunkManifestScript %> <% metaAttributes.forEach((metaAttribute) => { %> <%- metaAttribute %> <% }); %> diff --git a/packages/docusaurus/src/commands/build.ts b/packages/docusaurus/src/commands/build.ts index 4ed2e9b44b..adaf0fdd08 100644 --- a/packages/docusaurus/src/commands/build.ts +++ b/packages/docusaurus/src/commands/build.ts @@ -108,16 +108,10 @@ export async function build( ); }); - // Make sure generated client-manifest and chunk-map is cleaned first so we don't reuse the one from prevs build - const chunkManifestPath = path.join(generatedFilesDir, 'chunk-map.json'); - await Promise.all( - [clientManifestPath, chunkManifestPath].map(async manifestPath => { - const manifestExist = await fs.pathExists(manifestPath); - if (manifestExist) { - await fs.unlink(manifestPath); - } - }), - ); + // Make sure generated client-manifest is cleaned first so we don't reuse the one from prevs build + if (fs.existsSync(clientManifestPath)) { + fs.unlinkSync(clientManifestPath); + } // Run webpack to build JS bundle (client) and static html files (server). await compile([clientConfig, serverConfig]); diff --git a/packages/docusaurus/src/webpack/client.ts b/packages/docusaurus/src/webpack/client.ts index 67e3edf778..2765cde852 100644 --- a/packages/docusaurus/src/webpack/client.ts +++ b/packages/docusaurus/src/webpack/client.ts @@ -10,7 +10,7 @@ import merge from 'webpack-merge'; import {Props} from '@docusaurus/types'; import {createBaseConfig} from './base'; -import ChunkManifestPlugin from './plugins/ChunkManifestPlugin'; +import ChunkAssetPlugin from './plugins/ChunkAssetPlugin'; import LogPlugin from './plugins/LogPlugin'; export function createClientConfig(props: Props): Configuration { @@ -30,13 +30,7 @@ export function createClientConfig(props: Props): Configuration { runtimeChunk: true, }, plugins: [ - // Generate chunk-map.json (mapping of chunk names to their corresponding chunk assets) - new ChunkManifestPlugin({ - filename: 'chunk-map.json', - outputPath: props.generatedFilesDir, - manifestVariable: '__chunkMapping', - inlineManifest: !isProd, - }), + new ChunkAssetPlugin(), // Show compilation progress bar and build time. new LogPlugin({ name: 'Client', diff --git a/packages/docusaurus/src/webpack/plugins/ChunkAssetPlugin.ts b/packages/docusaurus/src/webpack/plugins/ChunkAssetPlugin.ts new file mode 100644 index 0000000000..88465b894d --- /dev/null +++ b/packages/docusaurus/src/webpack/plugins/ChunkAssetPlugin.ts @@ -0,0 +1,44 @@ +/** + * 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 {Template, Compiler} from 'webpack'; + +const pluginName = 'chunk-asset-plugin'; + +class ChunkAssetPlugin { + apply(compiler: Compiler) { + compiler.hooks.thisCompilation.tap(pluginName, ({mainTemplate}) => { + /* We modify webpack runtime to add an extra function called "__webpack_require__.gca" + that will allow us to get the corresponding chunk asset for a webpack chunk. + Pass it the chunkName or chunkId you want to load. + For example: if you have a chunk named "my-chunk-name" that will map to "/0a84b5e7.c8e35c7a.js" as its corresponding output path + __webpack_require__.gca("my-chunk-name") will return "/0a84b5e7.c8e35c7a.js"*/ + mainTemplate.hooks.requireExtensions.tap(pluginName, (source, chunk) => { + const chunkIdToName = chunk.getChunkMaps(false).name; + const chunkNameToId = Object.create(null); + for (const chunkId of Object.keys(chunkIdToName)) { + const chunkName = chunkIdToName[chunkId]; + chunkNameToId[chunkName] = chunkId; + } + const buf = [source]; + buf.push(''); + buf.push('// function to get chunk assets'); + buf.push( + mainTemplate.requireFn + + // If chunkName is passed, we convert it to chunk id + // Note that jsonpScriptSrc is an internal webpack function + `.gca = function(chunkId) { chunkId = ${JSON.stringify( + chunkNameToId, + )}[chunkId]||chunkId; return jsonpScriptSrc(chunkId); };`, + ); + return Template.asString(buf); + }); + }); + } +} + +export default ChunkAssetPlugin; diff --git a/packages/docusaurus/src/webpack/plugins/ChunkManifestPlugin.js b/packages/docusaurus/src/webpack/plugins/ChunkManifestPlugin.js deleted file mode 100644 index ce15ba7ad7..0000000000 --- a/packages/docusaurus/src/webpack/plugins/ChunkManifestPlugin.js +++ /dev/null @@ -1,89 +0,0 @@ -/** - * 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. - */ - -const HtmlWebpackPlugin = require('html-webpack-plugin'); -const fs = require('fs-extra'); -const path = require('path'); - -const pluginName = 'ChunkManifestPlugin'; - -class ChunkManifestPlugin { - constructor(options) { - this.options = { - filename: 'manifest.json', - outputPath: null, - manifestVariable: 'webpackManifest', - inlineManifest: false, - ...options, - }; - } - - apply(compiler) { - let chunkManifest; - const {path: defaultOutputPath, publicPath} = compiler.options.output; - - // Build the chunk mapping - compiler.hooks.afterCompile.tapAsync(pluginName, (compilation, done) => { - const assets = {}; - const assetsMap = {}; - // eslint-disable-next-line - for (const chunkGroup of compilation.chunkGroups) { - if (chunkGroup.name) { - const files = []; - // eslint-disable-next-line - for (const chunk of chunkGroup.chunks) { - files.push(...chunk.files); - } - assets[chunkGroup.name] = files.filter(f => f.slice(-4) !== '.map'); - assetsMap[chunkGroup.name] = files - .filter( - f => - f.slice(-4) !== '.map' && - f.slice(0, chunkGroup.name.length) === chunkGroup.name, - ) - .map(filename => `${publicPath}${filename}`); - } - } - chunkManifest = assetsMap; - if (!this.options.inlineManifest) { - const outputPath = this.options.outputPath || defaultOutputPath; - const finalPath = path.resolve(outputPath, this.options.filename); - fs.ensureDir(path.dirname(finalPath), () => { - fs.writeFile(finalPath, JSON.stringify(chunkManifest, null, 2), done); - }); - } else { - done(); - } - }); - - compiler.hooks.compilation.tap(pluginName, compilation => { - // inline to html-webpack-plugin tag - if (this.options.inlineManifest) { - const hooks = HtmlWebpackPlugin.getHooks(compilation); - const {manifestVariable} = this.options; - - hooks.alterAssetTagGroups.tap(pluginName, assets => { - if (chunkManifest) { - const newTag = { - tagName: 'script', - closeTag: true, - attributes: { - type: 'text/javascript', - }, - innerHTML: `/**/`, - }; - assets.headTags.unshift(newTag); - } - }); - } - }); - } -} - -module.exports = ChunkManifestPlugin;