diff --git a/build/monic/attach-component-dependencies.js b/build/monic/attach-component-dependencies.js index 24e1550f60..daf31954f6 100644 --- a/build/monic/attach-component-dependencies.js +++ b/build/monic/attach-component-dependencies.js @@ -27,7 +27,7 @@ const * @returns {Promise} */ module.exports = async function attachComponentDependencies(str, filePath) { - if (webpack.fatHTML()) { + if (webpack.fatHTML() && webpack.fatHTML() != 3) { return str; } diff --git a/build/monic/dynamic-component-import.js b/build/monic/dynamic-component-import.js index 5bc18ae3c8..2294fa1106 100644 --- a/build/monic/dynamic-component-import.js +++ b/build/monic/dynamic-component-import.js @@ -99,9 +99,11 @@ module.exports = async function dynamicComponentImportReplacer(str) { imports.push(decl); } - // In FatHTML, we do not include dynamically loaded CSS because it leads to duplication + // In FatHTML (excepting 3rd mode), we do not include dynamically loaded CSS because it leads to duplication // of the CSS and its associated assets - if (!fatHTML) { + const isThirdFatHTMLMode = fatHTML == 3; + + if (isThirdFatHTMLMode || !fatHTML) { const stylPath = `${fullPath}.styl`; diff --git a/build/webpack/module/rules/ess.js b/build/webpack/module/rules/ess.js index ce079f8065..c6db197ebc 100644 --- a/build/webpack/module/rules/ess.js +++ b/build/webpack/module/rules/ess.js @@ -25,7 +25,9 @@ const * @returns {Promise} */ module.exports = async function essRules() { - const g = await projectGraph; + const + g = await projectGraph, + isThirdFatHTMLMode = config.webpack.fatHTML() == 3; return { test: /\.ess$/, @@ -37,12 +39,17 @@ module.exports = async function essRules() { } }, - 'extract-loader', + ...(!isThirdFatHTMLMode ? + [ + 'extract-loader', - { - loader: 'html-loader', - options: config.html() - }, + { + loader: 'html-loader', + options: config.html() + } + ] : + [] + ), { loader: 'monic-loader', diff --git a/build/webpack/module/rules/ts.js b/build/webpack/module/rules/ts.js index 7d3c7f4617..bf4cca5523 100644 --- a/build/webpack/module/rules/ts.js +++ b/build/webpack/module/rules/ts.js @@ -65,7 +65,7 @@ module.exports = function tsRules() { loader: 'monic-loader', options: inherit(monic.typescript, { replacers: [].concat( - fatHTML ? + fatHTML && fatHTML != 3 ? [] : include('build/monic/attach-component-dependencies'), diff --git a/build/webpack/plugins.js b/build/webpack/plugins.js index 5e2972aebd..3baf1da329 100644 --- a/build/webpack/plugins.js +++ b/build/webpack/plugins.js @@ -33,6 +33,7 @@ module.exports = async function plugins({name}) { IgnoreInvalidWarningsPlugin = include('build/webpack/plugins/ignore-invalid-warnings'), I18NGeneratorPlugin = include('build/webpack/plugins/i18n-plugin'), InvalidateExternalCachePlugin = include('build/webpack/plugins/invalidate-external-cache'), + AsyncChunksPlugin = include('build/webpack/plugins/async-chunks-plugin'), StatoscopeWebpackPlugin = require('@statoscope/webpack-plugin').default; const plugins = new Map([ @@ -67,11 +68,17 @@ module.exports = async function plugins({name}) { plugins.set('progress-plugin', createProgressPlugin(name)); } - if (config.webpack.fatHTML() || config.webpack.storybook() || config.webpack.ssr) { + const isThirdFatHTMLMode = config.webpack.fatHTML() == 3; + + if ((config.webpack.fatHTML() || config.webpack.storybook() || config.webpack.ssr) && !isThirdFatHTMLMode) { plugins.set('limit-chunk-count-plugin', new webpack.optimize.LimitChunkCountPlugin({ maxChunks: 1 })); } + if (isThirdFatHTMLMode) { + 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..777ac4b3b5 --- /dev/null +++ b/build/webpack/plugins/async-chunks-plugin/README.md @@ -0,0 +1,11 @@ +# 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..7d4eda6885 --- /dev/null +++ b/build/webpack/plugins/async-chunks-plugin/index.js @@ -0,0 +1,120 @@ +/*! + * V4Fire Client Core + * https://github.com/V4Fire/Client + * + * Released under the MIT license + * https://github.com/V4Fire/Client/blob/master/LICENSE + */ + +'use strict'; + +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) { + const tpl = document.getElementById(id); + + if (tpl != null) { + const + js = new Blob([tpl.textContent], {type: 'text/javascript'}), + src = URL.createObjectURL(js), + script = document.createElement('script'); + + script.src = src; + document.body.appendChild(script); + cb(); + } else { + loadScript(path, cb, chunk, id); + } +} +${RuntimeGlobals.loadScript} = loadScriptReplacement`; + } +} + +class Index { + apply(compiler) { + debugger; + 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) => { + const pathToFile = path.join(compiler.options.output.path, file); + if (fs.existsSync(pathToFile)) { + fs.rmSync(path.join(compiler.options.output.path, file)); + } + }); + }); + } + + callback(); + }); + } +} + +module.exports = Index; diff --git a/config/default.js b/config/default.js index 93272e0ac3..705cd4d5a7 100644 --- a/config/default.js +++ b/config/default.js @@ -435,7 +435,8 @@ module.exports = config.createConfig({dirs: [__dirname, 'client']}, { * Returns * 1. `1` if all resources from the build should be embedded in HTML files; * 2. `2` if all scripts and links from the build should be embedded in HTML files; - * 3. `0` if resources from the build should not be embedded in HTML files. + * 3. `3` if some scripts and components should be embedded in shadow HTML [TBD]; + * 4. `0` if resources from the build should not be embedded in HTML files. * * @cli fat-html * @env FAT_HTML @@ -824,6 +825,23 @@ module.exports = config.createConfig({dirs: [__dirname, 'client']}, { 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'] + * } + * ] + * + * @returns {string} + */ + asyncAssetsJSON() { + return 'async-chunks-to-inline.json'; + }, + /** * Returns options for displaying webpack build progress * diff --git a/src/components/super/i-static-page/i-static-page.html.ss b/src/components/super/i-static-page/i-static-page.html.ss index 6b16e54636..e8e93c237a 100644 --- a/src/components/super/i-static-page/i-static-page.html.ss +++ b/src/components/super/i-static-page/i-static-page.html.ss @@ -140,14 +140,8 @@ `${result}`, '')}`; + + } catch (e) { + return ''; + } +} + exports.getPageStyleDepsDecl = getPageStyleDepsDecl; /**