diff --git a/build/monic/attach-component-dependencies.js b/build/monic/attach-component-dependencies.js index 12ddb2e57d..36343e0286 100644 --- a/build/monic/attach-component-dependencies.js +++ b/build/monic/attach-component-dependencies.js @@ -11,8 +11,7 @@ */ const - $C = require('collection.js'), - {webpack} = require('@config/config'); + $C = require('collection.js'); const path = require('upath'), @@ -29,10 +28,6 @@ const * @returns {!Promise} */ module.exports = async function attachComponentDependencies(str, filePath) { - if (webpack.fatHTML()) { - return str; - } - const {components} = await graph; diff --git a/build/monic/dynamic-component-import.js b/build/monic/dynamic-component-import.js index 07ae2cc69d..f589da0f1e 100644 --- a/build/monic/dynamic-component-import.js +++ b/build/monic/dynamic-component-import.js @@ -9,7 +9,7 @@ */ const - {typescript, webpack} = require('@config/config'), + {typescript} = require('@config/config'), {commentModuleExpr: commentExpr} = include('build/const'); const importRgxp = new RegExp( @@ -19,8 +19,7 @@ const importRgxp = new RegExp( const hasImport = importRgxp.removeFlags('g'), - isESImport = typescript().client.compilerOptions.module === 'ES2020', - fatHTML = webpack.fatHTML(); + isESImport = typescript().client.compilerOptions.module === 'ES2020'; /** * Monic replacer to enable dynamic imports of components @@ -63,7 +62,7 @@ module.exports = function dynamicComponentImportReplacer(str) { imports.push(decl); } - if (!fatHTML) { + { let decl; diff --git a/build/webpack/module.js b/build/webpack/module.js index 90297a35af..778f4d79dc 100644 --- a/build/webpack/module.js +++ b/build/webpack/module.js @@ -60,9 +60,6 @@ module.exports = async function module({plugins}) { g = await projectGraph, isProd = webpack.mode() === 'production'; - const - fatHTML = webpack.fatHTML(); - const loaders = { rules: new Map() }; @@ -80,18 +77,13 @@ module.exports = async function module({plugins}) { { loader: 'monic-loader', options: inherit(monic.typescript, { - replacers: [].concat( - fatHTML ? - [] : + replacers: [ include('build/monic/attach-component-dependencies'), - - [ include('build/monic/require-context'), include('build/monic/super-import'), include('build/monic/ts-import'), include('build/monic/dynamic-component-import') ] - ) }) } ]; @@ -243,13 +235,6 @@ module.exports = async function module({plugins}) { } }, - 'extract-loader', - - { - loader: 'html-loader', - options: config.html() - }, - { loader: 'monic-loader', options: inherit(monic.html, { diff --git a/build/webpack/plugins.js b/build/webpack/plugins.js index 84fe7031ff..62eac6c049 100644 --- a/build/webpack/plugins.js +++ b/build/webpack/plugins.js @@ -15,6 +15,8 @@ const config = require('@config/config'), webpack = require('webpack'); +const AsyncChunksPlugin = include('build/webpack/plugins/async-chunks-plugin'); + /** * Returns options for `webpack.plugins` * @returns {!Map} @@ -54,9 +56,7 @@ module.exports = async function plugins({name}) { } if (config.webpack.fatHTML()) { - plugins.set('limit-chunk-count-plugin', new webpack.optimize.LimitChunkCountPlugin({ - maxChunks: 1 - })); + plugins.set('async-chunk-plugin', new AsyncChunksPlugin()); } return plugins; diff --git a/build/webpack/plugins/async-chunks-plugin/README.md b/build/webpack/plugins/async-chunks-plugin/README.md new file mode 100644 index 0000000000..060f0f7013 --- /dev/null +++ b/build/webpack/plugins/async-chunks-plugin/README.md @@ -0,0 +1,12 @@ +# build/webpack/plugins/async-chunks-plugin + +This module provides a plugin that gathers information about asynchronous chunks and modifies the webpack runtime to load asynchronous modules from shadow storage in fat-html. + +## Gathering Information + +During the initial phase, the plugin gathers information about all emitted asynchronous chunks. This information is stored in a JSON file within the output directory and later used to inline those scripts into the HTML using a special template tag. + +## Patching the Webpack Runtime + +The plugin replaces the standard RuntimeGlobals.loadScript script. The new script attempts to locate a template tag with the ID of the chunk name and adds the located script to the page. If there is no such template with the script, the standard method is called to load the chunk from the network. + diff --git a/build/webpack/plugins/async-chunks-plugin/index.js b/build/webpack/plugins/async-chunks-plugin/index.js new file mode 100644 index 0000000000..1931856940 --- /dev/null +++ b/build/webpack/plugins/async-chunks-plugin/index.js @@ -0,0 +1,109 @@ +'use strict'; + +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +const fs = require('fs'); +const path = require('path'); +const RuntimeModule = require('webpack/lib/RuntimeModule'); +const RuntimeGlobals = require('webpack/lib/RuntimeGlobals'); + +const {webpack} = require('@config/config'); + +class AsyncPlugRuntimeModule extends RuntimeModule { + constructor() { + super('async chunk loader for fat-html', RuntimeModule.STAGE_ATTACH); + } + + generate() { + return `var loadScript = ${RuntimeGlobals.loadScript}; +function loadScriptReplacement(path, cb, chunk, id) { + var tpl = document.getElementById(id); + if (tpl && tpl.content) { + document.body.appendChild(tpl.content.cloneNode(true)); + cb(); + } else { + loadScript(path, cb, chunk, id); + } +} +${RuntimeGlobals.loadScript} = loadScriptReplacement`; + } +} + +class Index { + apply(compiler) { + compiler.hooks.thisCompilation.tap( + 'AsyncChunksPlugin', + (compilation) => { + const onceForChunkSet = new WeakSet(); + + compilation.hooks.runtimeRequirementInTree + .for(RuntimeGlobals.ensureChunkHandlers) + .tap('AsyncChunksPlugin', (chunk, set) => { + if (onceForChunkSet.has(chunk)) { + return; + } + + onceForChunkSet.add(chunk); + + const runtimeModule = new AsyncPlugRuntimeModule(); + set.add(RuntimeGlobals.loadScript); + compilation.addRuntimeModule(chunk, runtimeModule); + }); + } + ); + + compiler.hooks.emit.tapAsync('AsyncChunksPlugin', (compilation, callback) => { + const asyncChunks = []; + if (compilation.name !== 'runtime') { + callback(); + return; + } + + compilation.chunks.forEach((chunk) => { + if (chunk.canBeInitial()) { + return; + } + + asyncChunks.push({ + id: chunk.id, + files: chunk.files.map((filename) => filename) + }); + }); + + const outputPath = path.join(compiler.options.output.path, webpack.asyncAssetsJSON()); + + fs.writeFile(outputPath, JSON.stringify(asyncChunks, null, 2), (err) => { + if (err) { + compilation.errors.push(new Error(`Error write async chunks list to ${outputPath}`)); + } + + callback(); + }); + }); + + compiler.hooks.done.tapAsync('AsyncChunksPlugin', (stat, callback) => { + if (stat.compilation.name === 'html') { + const + filePath = path.join(compiler.options.output.path, webpack.asyncAssetsJSON()), + fileContent = fs.readFileSync(filePath, 'utf-8'), + asyncChunks = JSON.parse(fileContent); + + asyncChunks.forEach((chunk) => { + chunk.files.forEach((file) => { + fs.rmSync(path.join(compiler.options.output.path, file)); + }); + }); + } + + callback(); + }); + } +} + +module.exports = Index; diff --git a/config/default.js b/config/default.js index 5a12fae7d1..3929f5941d 100644 --- a/config/default.js +++ b/config/default.js @@ -777,6 +777,23 @@ module.exports = config.createConfig({dirs: [__dirname, 'client']}, { */ assetsJS() { return path.changeExt(this.assetsJSON(), '.js'); + }, + + /** + * Returns the path to the generated async assets chunks list within the output directory. + * It contains an array of async chunks and their file names to inline them into fat-html. + * ... + * [ + * { + * id: 'chunk_id', + * files: ['filename.ext'] + * } + * ] + * + * @return {string} + */ + asyncAssetsJSON() { + return 'async-chunks-to-inline.json'; } }, diff --git a/src/super/i-static-page/i-static-page.interface.ss b/src/super/i-static-page/i-static-page.interface.ss index 5eb85f3ddc..0e982e35e5 100644 --- a/src/super/i-static-page/i-static-page.interface.ss +++ b/src/super/i-static-page/i-static-page.interface.ss @@ -162,6 +162,7 @@ += h.getPageStyleDepsDecl(ownDeps, {assets, wrap: true}) - block scripts + += h.getPageAsyncScripts() += h.getScriptDeclByName('std', {assets, optional: true, wrap: true}) += await h.loadLibs(deps.scripts, {assets, wrap: true, js: true}) diff --git a/src/super/i-static-page/modules/ss-helpers/page.js b/src/super/i-static-page/modules/ss-helpers/page.js index e245f62e39..afd77197bf 100644 --- a/src/super/i-static-page/modules/ss-helpers/page.js +++ b/src/super/i-static-page/modules/ss-helpers/page.js @@ -81,6 +81,37 @@ function getPageScriptDepsDecl(dependencies, {assets, wrap} = {}) { return decl; } +exports.getPageAsyncScripts = getPageAsyncScripts; + +function getPageAsyncScripts() { + if (!needInline()) { + return ''; + } + + const + fileName = webpack.asyncAssetsJSON(), + filePath = src.clientOutput(fileName); + + try { + const + fileContent = fs.readFileSync(filePath, 'utf-8'), + asyncChunks = JSON.parse(fileContent); + + if (webpack.mode() === 'production') { + return asyncChunks.reduce((result, chunk) => `${result}`, ''); + } + + return `
${asyncChunks.reduce((result, chunk) => `${result}`, '')}
`; + + } catch (e) { + return ''; + } +} + exports.getPageStyleDepsDecl = getPageStyleDepsDecl; /**