Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

added new fat-html=3 mode with async chunks and shadowing scripts #1484

Draft
wants to merge 22 commits into
base: v4
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build/monic/attach-component-dependencies.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
* @returns {Promise<string>}
*/
module.exports = async function attachComponentDependencies(str, filePath) {
if (webpack.fatHTML()) {
if (webpack.fatHTML() && webpack.fatHTML() != 3) {

Check failure on line 30 in build/monic/attach-component-dependencies.js

View workflow job for this annotation

GitHub Actions / linters (20.x)

Expected '!==' and instead saw '!='

Check failure on line 30 in build/monic/attach-component-dependencies.js

View workflow job for this annotation

GitHub Actions / linters (20.x)

Expected '!==' and instead saw '!='
return str;
}

Expand Down
6 changes: 4 additions & 2 deletions build/monic/dynamic-component-import.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,9 +99,11 @@
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;

Check failure on line 104 in build/monic/dynamic-component-import.js

View workflow job for this annotation

GitHub Actions / linters (20.x)

Expected '===' and instead saw '=='

Check failure on line 104 in build/monic/dynamic-component-import.js

View workflow job for this annotation

GitHub Actions / linters (20.x)

Expected '===' and instead saw '=='

if (isThirdFatHTMLMode || !fatHTML) {
const
stylPath = `${fullPath}.styl`;

Expand Down
19 changes: 13 additions & 6 deletions build/webpack/module/rules/ess.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@
* @returns {Promise<import('webpack').RuleSetRule>}
*/
module.exports = async function essRules() {
const g = await projectGraph;
const
g = await projectGraph,
isThirdFatHTMLMode = config.webpack.fatHTML() == 3;

Check failure on line 30 in build/webpack/module/rules/ess.js

View workflow job for this annotation

GitHub Actions / linters (20.x)

Expected '===' and instead saw '=='

Check failure on line 30 in build/webpack/module/rules/ess.js

View workflow job for this annotation

GitHub Actions / linters (20.x)

Expected '===' and instead saw '=='

return {
test: /\.ess$/,
Expand All @@ -37,12 +39,17 @@
}
},

'extract-loader',
...(!isThirdFatHTMLMode ?
[
'extract-loader',

{
loader: 'html-loader',
options: config.html()
},
{
loader: 'html-loader',
options: config.html()
}
] :
[]
),

{
loader: 'monic-loader',
Expand Down
2 changes: 1 addition & 1 deletion build/webpack/module/rules/ts.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@
loader: 'monic-loader',
options: inherit(monic.typescript, {
replacers: [].concat(
fatHTML ?
fatHTML && fatHTML != 3 ?

Check failure on line 68 in build/webpack/module/rules/ts.js

View workflow job for this annotation

GitHub Actions / linters (20.x)

Expected '!==' and instead saw '!='

Check failure on line 68 in build/webpack/module/rules/ts.js

View workflow job for this annotation

GitHub Actions / linters (20.x)

Expected '!==' and instead saw '!='
[] :
include('build/monic/attach-component-dependencies'),

Expand Down
9 changes: 8 additions & 1 deletion build/webpack/plugins.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
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([
Expand Down Expand Up @@ -67,11 +68,17 @@
plugins.set('progress-plugin', createProgressPlugin(name));
}

if (config.webpack.fatHTML() || config.webpack.storybook() || config.webpack.ssr) {
const isThirdFatHTMLMode = config.webpack.fatHTML() == 3;

Check failure on line 71 in build/webpack/plugins.js

View workflow job for this annotation

GitHub Actions / linters (20.x)

Expected '===' and instead saw '=='

Check failure on line 71 in build/webpack/plugins.js

View workflow job for this annotation

GitHub Actions / linters (20.x)

Expected '===' and instead saw '=='

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;
};
11 changes: 11 additions & 0 deletions build/webpack/plugins/async-chunks-plugin/README.md
Original file line number Diff line number Diff line change
@@ -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.
120 changes: 120 additions & 0 deletions build/webpack/plugins/async-chunks-plugin/index.js
Original file line number Diff line number Diff line change
@@ -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;
20 changes: 19 additions & 1 deletion config/default.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
*
Expand Down
16 changes: 9 additions & 7 deletions src/components/super/i-static-page/i-static-page.html.ss
Original file line number Diff line number Diff line change
Expand Up @@ -140,14 +140,8 @@
<! :: STYLES

- block headScripts
+= h.getPageAsyncScripts()
+= await h.loadLibs(deps.headScripts, {assets, wrap: inlineDepsDeclarations, js: inlineDepsDeclarations})
+= h.getScriptDeclByName('std', {assets, optional: true, wrap: inlineDepsDeclarations, js: inlineDepsDeclarations})
+= await h.loadLibs(deps.scripts, {assets, wrap: inlineDepsDeclarations, js: inlineDepsDeclarations})

+= h.getScriptDeclByName('vendor', {assets, optional: true, wrap: inlineDepsDeclarations, js: inlineDepsDeclarations})
+= h.getScriptDeclByName('index-core', {assets, optional: true, wrap: inlineDepsDeclarations, js: inlineDepsDeclarations})

+= h.getPageScriptDepsDecl(ownDeps, {assets, wrap: inlineDepsDeclarations, js: inlineDepsDeclarations})

< body ${rootAttrs|!html}
<! :: SSR
Expand All @@ -172,3 +166,11 @@
- if inlineDepsDeclarations
+= await h.loadStyles(deps.styles, {assets, wrap: true, js: true})
+= h.getPageStyleDepsDecl(ownDeps, {assets, wrap: true, js: true})

- block scripts
+= h.getScriptDeclByName('std', {assets, optional: true, wrap: inlineDepsDeclarations, js: inlineDepsDeclarations})
+= await h.loadLibs(deps.scripts, {assets, wrap: inlineDepsDeclarations, js: inlineDepsDeclarations})

+= h.getScriptDeclByName('vendor', {assets, optional: true, wrap: inlineDepsDeclarations, js: inlineDepsDeclarations})
+= h.getScriptDeclByName('index-core', {assets, optional: true, wrap: inlineDepsDeclarations, js: inlineDepsDeclarations})
+= h.getPageScriptDepsDecl(ownDeps, {assets, wrap: inlineDepsDeclarations, js: inlineDepsDeclarations})
25 changes: 25 additions & 0 deletions src/components/super/i-static-page/modules/ss-helpers/page.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,31 @@ function getPageScriptDepsDecl(dependencies, {assets, wrap, js} = {}) {
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);

return `${asyncChunks.reduce((result, chunk) => `${result}<script id="${chunk.id}">${
chunk.files.map((fileName) => `include('${src.clientOutput(fileName)}');\n`).join()
}</script>`, '')}`;

} catch (e) {
return '';
}
}

exports.getPageStyleDepsDecl = getPageStyleDepsDecl;

/**
Expand Down
Loading