From a21473675c81dc4ac2ec8112741cbd52a2756dcc Mon Sep 17 00:00:00 2001 From: Alexander Akait <4567934+alexander-akait@users.noreply.github.com> Date: Wed, 21 Jun 2023 20:48:50 +0300 Subject: [PATCH] fix: reemit assets from loaders (#1811) --- index.js | 27 +++++++++++++------ lib/cached-child-compiler.js | 6 ++--- lib/child-compiler.js | 52 +++++++++++++++++++++++++----------- 3 files changed, 59 insertions(+), 26 deletions(-) diff --git a/index.js b/index.js index 9e9e1bb6..2e98833f 100644 --- a/index.js +++ b/index.js @@ -21,7 +21,7 @@ const getHtmlWebpackPluginHooks = require('./lib/hooks.js').getHtmlWebpackPlugin /** @typedef {import("webpack").Compiler} Compiler */ /** @typedef {ReturnType} Logger */ /** @typedef {import("webpack/lib/Compilation.js")} Compilation */ -/** @typedef {Array<{ source: import('webpack').sources.Source, name: string }>} PreviousEmittedAssets */ +/** @typedef {Array<{ name: string, source: import('webpack').sources.Source, info?: import('webpack').AssetInfo }>} PreviousEmittedAssets */ /** @typedef {{ publicPath: string, js: Array, css: Array, manifest?: string, favicon?: string }} AssetsInformationByGroups */ class HtmlWebpackPlugin { @@ -1023,8 +1023,8 @@ class HtmlWebpackPlugin { const newAssetJson = JSON.stringify(this.getAssetFiles(assetsInformationByGroups)); if (isCompilationCached && this.options.cache && assetJson.value === newAssetJson) { - previousEmittedAssets.forEach(({ name, source }) => { - compilation.emitAsset(name, source); + previousEmittedAssets.forEach(({ name, source, info }) => { + compilation.emitAsset(name, source, info); }); return callback(); } else { @@ -1084,15 +1084,26 @@ class HtmlWebpackPlugin { if ('error' in templateResult) { return this.options.showErrors ? prettyError(templateResult.error, compiler.context).toHtml() : 'ERROR'; } + // Allow to use a custom function / string instead if (this.options.templateContent !== false) { return this.options.templateContent; } - // Once everything is compiled evaluate the html factory - // and replace it with its content - return ('compiledEntry' in templateResult) - ? this.evaluateCompilationResult(templateResult.compiledEntry.content, assetsInformationByGroups.publicPath, this.options.template) - : Promise.reject(new Error('Child compilation contained no compiledEntry')); + + // Once everything is compiled evaluate the html factory and replace it with its content + if ('compiledEntry' in templateResult) { + const compiledEntry = templateResult.compiledEntry; + const assets = compiledEntry.assets; + + // Store assets from child compiler to reemit them later + for (const name in assets) { + previousEmittedAssets.push({ name, source: assets[name].source, info: assets[name].info }); + } + + return this.evaluateCompilationResult(compiledEntry.content, assetsInformationByGroups.publicPath, this.options.template); + } + + return Promise.reject(new Error('Child compilation contained no compiledEntry')); }); const templateExectutionPromise = Promise.all([assetsPromise, assetTagGroupsPromise, templateEvaluationPromise]) // Execute the template diff --git a/lib/cached-child-compiler.js b/lib/cached-child-compiler.js index 36ecabf6..f6e4c733 100644 --- a/lib/cached-child-compiler.js +++ b/lib/cached-child-compiler.js @@ -27,11 +27,11 @@ /** @typedef {import("webpack").Compiler} Compiler */ /** @typedef {import("webpack").Compilation} Compilation */ /** @typedef {import("webpack/lib/FileSystemInfo").Snapshot} Snapshot */ -/** @typedef {{hash: string, entry: any, content: string }} ChildCompilationResultEntry */ +/** @typedef {import("./child-compiler").ChildCompilationTemplateResult} ChildCompilationTemplateResult */ /** @typedef {{fileDependencies: string[], contextDependencies: string[], missingDependencies: string[]}} FileDependencies */ /** @typedef {{ dependencies: FileDependencies, - compiledEntries: {[entryName: string]: ChildCompilationResultEntry} + compiledEntries: {[entryName: string]: ChildCompilationTemplateResult} } | { dependencies: FileDependencies, error: Error @@ -96,7 +96,7 @@ class CachedChildCompilation { * @param {string} entry * @returns { | { mainCompilationHash: string, error: Error } - | { mainCompilationHash: string, compiledEntry: ChildCompilationResultEntry } + | { mainCompilationHash: string, compiledEntry: ChildCompilationTemplateResult } } */ getCompilationEntryResult (entry) { diff --git a/lib/child-compiler.js b/lib/child-compiler.js index e5d433b0..8bea9bec 100644 --- a/lib/child-compiler.js +++ b/lib/child-compiler.js @@ -11,6 +11,7 @@ /** @typedef {import("webpack").Chunk} Chunk */ /** @typedef {import("webpack").sources.Source} Source */ +/** @typedef {{hash: string, entry: Chunk, content: string, assets: {[name: string]: { source: Source, info: import("webpack").AssetInfo }}}} ChildCompilationTemplateResult */ let instanceId = 0; /** @@ -30,17 +31,11 @@ class HtmlWebpackChildCompiler { * The template array will allow us to keep track which input generated which output */ this.templates = templates; - /** - * @type {Promise<{[templatePath: string]: { content: string, hash: string, entry: Chunk }}>} - */ + /** @type {Promise<{[templatePath: string]: ChildCompilationTemplateResult}>} */ this.compilationPromise; // eslint-disable-line - /** - * @type {number} - */ + /** @type {number | undefined} */ this.compilationStartedTimestamp; // eslint-disable-line - /** - * @type {number} - */ + /** @type {number | undefined} */ this.compilationEndedTimestamp; // eslint-disable-line /** * All file dependencies of the child compiler @@ -51,6 +46,7 @@ class HtmlWebpackChildCompiler { /** * Returns true if the childCompiler is currently compiling + * * @returns {boolean} */ isCompiling () { @@ -59,6 +55,8 @@ class HtmlWebpackChildCompiler { /** * Returns true if the childCompiler is done compiling + * + * @returns {boolean} */ didCompile () { return this.compilationEndedTimestamp !== undefined; @@ -69,7 +67,7 @@ class HtmlWebpackChildCompiler { * once it is started no more templates can be added * * @param {import('webpack').Compilation} mainCompilation - * @returns {Promise<{[templatePath: string]: { content: string, hash: string, entry: Chunk }}>} + * @returns {Promise<{[templatePath: string]: ChildCompilationTemplateResult}>} */ compileTemplates (mainCompilation) { const webpack = mainCompilation.compiler.webpack; @@ -125,13 +123,17 @@ class HtmlWebpackChildCompiler { // The following config enables relative URL support for the child compiler childCompiler.options.module = { ...childCompiler.options.module }; childCompiler.options.module.parser = { ...childCompiler.options.module.parser }; - childCompiler.options.module.parser.javascript = { ...childCompiler.options.module.parser.javascript, - url: 'relative' }; + childCompiler.options.module.parser.javascript = { + ...childCompiler.options.module.parser.javascript, + url: 'relative' + }; this.compilationStartedTimestamp = new Date().getTime(); + /** @type {Promise<{[templatePath: string]: ChildCompilationTemplateResult}>} */ this.compilationPromise = new Promise((resolve, reject) => { /** @type {Source[]} */ const extractedAssets = []; + childCompiler.hooks.thisCompilation.tap('HtmlWebpackPlugin', (compilation) => { compilation.hooks.processAssets.tap( { @@ -142,6 +144,7 @@ class HtmlWebpackChildCompiler { temporaryTemplateNames.forEach((temporaryTemplateName) => { if (assets[temporaryTemplateName]) { extractedAssets.push(assets[temporaryTemplateName]); + compilation.deleteAsset(temporaryTemplateName); } }); @@ -151,13 +154,16 @@ class HtmlWebpackChildCompiler { childCompiler.runAsChild((err, entries, childCompilation) => { // Extract templates + // TODO fine a better way to store entries and results, to avoid duplicate chunks and assets const compiledTemplates = entries ? extractedAssets.map((asset) => asset.source()) : []; + // Extract file dependencies if (entries && childCompilation) { this.fileDependencies = { fileDependencies: Array.from(childCompilation.fileDependencies), contextDependencies: Array.from(childCompilation.contextDependencies), missingDependencies: Array.from(childCompilation.missingDependencies) }; } + // Reject the promise if the childCompilation contains error if (childCompilation && childCompilation.errors && childCompilation.errors.length) { const errorDetails = childCompilation.errors.map(error => { @@ -167,34 +173,50 @@ class HtmlWebpackChildCompiler { } return message; }).join('\n'); + reject(new Error('Child compilation failed:\n' + errorDetails)); + return; } + // Reject if the error object contains errors if (err) { reject(err); return; } + if (!childCompilation || !entries) { reject(new Error('Empty child compilation')); return; } + /** - * @type {{[templatePath: string]: { content: string, hash: string, entry: Chunk }}} + * @type {{[templatePath: string]: ChildCompilationTemplateResult}} */ const result = {}; + + /** @type {{[name: string]: { source: Source, info: import("webpack").AssetInfo }}} */ + const assets = {}; + + for (const asset of childCompilation.getAssets()) { + assets[asset.name] = { source: asset.source, info: asset.info }; + } + compiledTemplates.forEach((templateSource, entryIndex) => { // The compiledTemplates are generated from the entries added in // the addTemplate function. - // Therefore the array index of this.templates should be the as entryIndex. + // Therefore, the array index of this.templates should be the as entryIndex. result[this.templates[entryIndex]] = { // TODO, can we have Buffer here? content: /** @type {string} */ (templateSource), hash: childCompilation.hash || 'XXXX', - entry: entries[entryIndex] + entry: entries[entryIndex], + assets }; }); + this.compilationEndedTimestamp = new Date().getTime(); + resolve(result); }); });