From dcdd09442ebacfbf226d9a67a1f8221c5143ae78 Mon Sep 17 00:00:00 2001 From: Tobias Koppers Date: Thu, 24 Sep 2020 11:20:45 +0200 Subject: [PATCH] refactor(runtime): move runtime logic for webpack 5 into runtime module --- src/CssLoadingRuntimeModule.js | 126 ++++++++++++++ src/index.js | 306 ++++++++++++++++++--------------- 2 files changed, 289 insertions(+), 143 deletions(-) create mode 100644 src/CssLoadingRuntimeModule.js diff --git a/src/CssLoadingRuntimeModule.js b/src/CssLoadingRuntimeModule.js new file mode 100644 index 00000000..1b1ea244 --- /dev/null +++ b/src/CssLoadingRuntimeModule.js @@ -0,0 +1,126 @@ +import { RuntimeGlobals, RuntimeModule, Template, util } from 'webpack'; + +import { MODULE_TYPE } from './utils'; + +const { + comparators: { compareModulesByIdentifier }, +} = util; + +const getCssChunkObject = (mainChunk, compilation) => { + const obj = {}; + const { chunkGraph } = compilation; + + for (const chunk of mainChunk.getAllAsyncChunks()) { + const modules = chunkGraph.getOrderedChunkModulesIterable( + chunk, + compareModulesByIdentifier + ); + for (const module of modules) { + if (module.type === MODULE_TYPE) { + obj[chunk.id] = 1; + break; + } + } + } + + return obj; +}; + +module.exports = class CssLoadingRuntimeModule extends RuntimeModule { + constructor(runtimeRequirements) { + super('css loading', 10); + this.runtimeRequirements = runtimeRequirements; + } + + generate() { + const { chunk, compilation, runtimeRequirements } = this; + const { + runtimeTemplate, + outputOptions: { crossOriginLoading }, + } = compilation; + const chunkMap = getCssChunkObject(chunk, compilation); + + if (Object.keys(chunkMap).length === 0) return null; + + const withLoading = runtimeRequirements.has( + RuntimeGlobals.ensureChunkHandlers + ); + + return Template.asString([ + '// object to store loaded CSS chunks', + 'var installedCssChunks = {', + Template.indent( + chunk.ids.map((id) => `${JSON.stringify(id)}: 0`).join(',\n') + ), + '};', + '', + withLoading + ? Template.asString([ + `${ + RuntimeGlobals.ensureChunkHandlers + }.miniCss = ${runtimeTemplate.basicFunction('chunkId, promises', [ + `var cssChunks = ${JSON.stringify(chunkMap)};`, + 'if(installedCssChunks[chunkId]) promises.push(installedCssChunks[chunkId]);', + 'else if(installedCssChunks[chunkId] !== 0 && cssChunks[chunkId]) {', + Template.indent([ + 'promises.push(installedCssChunks[chunkId] = new Promise(function(resolve, reject) {', + Template.indent([ + `var href = ${RuntimeGlobals.require}.miniCssF(chunkId);`, + `var fullhref = ${RuntimeGlobals.publicPath} + href;`, + 'var existingLinkTags = document.getElementsByTagName("link");', + 'for(var i = 0; i < existingLinkTags.length; i++) {', + Template.indent([ + 'var tag = existingLinkTags[i];', + 'var dataHref = tag.getAttribute("data-href") || tag.getAttribute("href");', + 'if(tag.rel === "stylesheet" && (dataHref === href || dataHref === fullhref)) return resolve();', + ]), + '}', + 'var existingStyleTags = document.getElementsByTagName("style");', + 'for(var i = 0; i < existingStyleTags.length; i++) {', + Template.indent([ + 'var tag = existingStyleTags[i];', + 'var dataHref = tag.getAttribute("data-href");', + 'if(dataHref === href || dataHref === fullhref) return resolve();', + ]), + '}', + 'var linkTag = document.createElement("link");', + 'linkTag.rel = "stylesheet";', + 'linkTag.type = "text/css";', + 'linkTag.onload = resolve;', + 'linkTag.onerror = function(event) {', + Template.indent([ + 'var request = event && event.target && event.target.src || fullhref;', + 'var err = new Error("Loading CSS chunk " + chunkId + " failed.\\n(" + request + ")");', + 'err.code = "CSS_CHUNK_LOAD_FAILED";', + 'err.request = request;', + 'delete installedCssChunks[chunkId]', + 'linkTag.parentNode.removeChild(linkTag)', + 'reject(err);', + ]), + '};', + 'linkTag.href = fullhref;', + crossOriginLoading + ? Template.asString([ + `if (linkTag.href.indexOf(window.location.origin + '/') !== 0) {`, + Template.indent( + `linkTag.crossOrigin = ${JSON.stringify( + crossOriginLoading + )};` + ), + '}', + ]) + : '', + 'var head = document.getElementsByTagName("head")[0];', + 'head.appendChild(linkTag);', + ]), + '}).then(function() {', + Template.indent(['installedCssChunks[chunkId] = 0;']), + '}));', + ]), + '}', + ])};`, + ]) + : '// no chunk loading', + ]); + } +}; diff --git a/src/index.js b/src/index.js index 6d71668d..117bb087 100644 --- a/src/index.js +++ b/src/index.js @@ -225,169 +225,189 @@ class MiniCssExtractPlugin { const { mainTemplate } = compilation; - mainTemplate.hooks.localVars.tap(pluginName, (source, chunk) => { - const chunkMap = this.getCssChunkObject(chunk, compilation); - - if (Object.keys(chunkMap).length > 0) { - return Template.asString([ - source, - '', - '// object to store loaded CSS chunks', - 'var installedCssChunks = {', - Template.indent( - chunk.ids.map((id) => `${JSON.stringify(id)}: 0`).join(',\n') - ), - '};', - ]); - } - - return source; - }); - - mainTemplate.hooks.requireEnsure.tap( - pluginName, - (source, chunk, hash) => { + if (isWebpack4) { + mainTemplate.hooks.localVars.tap(pluginName, (source, chunk) => { const chunkMap = this.getCssChunkObject(chunk, compilation); if (Object.keys(chunkMap).length > 0) { - const maintemplateObject = isWebpack4 ? mainTemplate : compilation; - const chunkMaps = chunk.getChunkMaps(); - const { crossOriginLoading } = maintemplateObject.outputOptions; - const linkHrefPath = maintemplateObject.getAssetPath( - JSON.stringify(this.options.chunkFilename), - { - hash: isWebpack4 - ? `" + ${mainTemplate.renderCurrentHashCode(hash)} + "` - : `" + ${webpack.RuntimeGlobals.getFullHash} + "`, - hashWithLength: (length) => - isWebpack4 - ? `" + ${mainTemplate.renderCurrentHashCode( - hash, - length - )} + "` - : `" + ${webpack.RuntimeGlobals.getFullHash} + "`, - chunk: { - id: '" + chunkId + "', - hash: `" + ${JSON.stringify(chunkMaps.hash)}[chunkId] + "`, - hashWithLength(length) { - const shortChunkHashMap = Object.create(null); - - for (const chunkId of Object.keys(chunkMaps.hash)) { - if (typeof chunkMaps.hash[chunkId] === 'string') { - shortChunkHashMap[chunkId] = chunkMaps.hash[ - chunkId - ].substring(0, length); - } - } + return Template.asString([ + source, + '', + '// object to store loaded CSS chunks', + 'var installedCssChunks = {', + Template.indent( + chunk.ids.map((id) => `${JSON.stringify(id)}: 0`).join(',\n') + ), + '};', + ]); + } - return `" + ${JSON.stringify( - shortChunkHashMap - )}[chunkId] + "`; - }, - contentHash: { - [MODULE_TYPE]: `" + ${JSON.stringify( - chunkMaps.contentHash[MODULE_TYPE] - )}[chunkId] + "`, - }, - contentHashWithLength: { - [MODULE_TYPE]: (length) => { - const shortContentHashMap = {}; - const contentHash = chunkMaps.contentHash[MODULE_TYPE]; - - for (const chunkId of Object.keys(contentHash)) { - if (typeof contentHash[chunkId] === 'string') { - shortContentHashMap[chunkId] = contentHash[ + return source; + }); + + mainTemplate.hooks.requireEnsure.tap( + pluginName, + (source, chunk, hash) => { + const chunkMap = this.getCssChunkObject(chunk, compilation); + + if (Object.keys(chunkMap).length > 0) { + const chunkMaps = chunk.getChunkMaps(); + const { crossOriginLoading } = mainTemplate.outputOptions; + const linkHrefPath = mainTemplate.getAssetPath( + JSON.stringify(this.options.chunkFilename), + { + hash: `" + ${mainTemplate.renderCurrentHashCode(hash)} + "`, + hashWithLength: (length) => + `" + ${mainTemplate.renderCurrentHashCode( + hash, + length + )} + "`, + chunk: { + id: '" + chunkId + "', + hash: `" + ${JSON.stringify(chunkMaps.hash)}[chunkId] + "`, + hashWithLength(length) { + const shortChunkHashMap = Object.create(null); + + for (const chunkId of Object.keys(chunkMaps.hash)) { + if (typeof chunkMaps.hash[chunkId] === 'string') { + shortChunkHashMap[chunkId] = chunkMaps.hash[ chunkId ].substring(0, length); } } return `" + ${JSON.stringify( - shortContentHashMap + shortChunkHashMap )}[chunkId] + "`; }, + contentHash: { + [MODULE_TYPE]: `" + ${JSON.stringify( + chunkMaps.contentHash[MODULE_TYPE] + )}[chunkId] + "`, + }, + contentHashWithLength: { + [MODULE_TYPE]: (length) => { + const shortContentHashMap = {}; + const contentHash = chunkMaps.contentHash[MODULE_TYPE]; + + for (const chunkId of Object.keys(contentHash)) { + if (typeof contentHash[chunkId] === 'string') { + shortContentHashMap[chunkId] = contentHash[ + chunkId + ].substring(0, length); + } + } + + return `" + ${JSON.stringify( + shortContentHashMap + )}[chunkId] + "`; + }, + }, + name: `" + (${JSON.stringify( + chunkMaps.name + )}[chunkId]||chunkId) + "`, }, - name: `" + (${JSON.stringify( - chunkMaps.name - )}[chunkId]||chunkId) + "`, - }, - contentHashType: MODULE_TYPE, - } - ); + contentHashType: MODULE_TYPE, + } + ); - return Template.asString([ - source, - '', - `// ${pluginName} CSS loading`, - `var cssChunks = ${JSON.stringify(chunkMap)};`, - 'if(installedCssChunks[chunkId]) promises.push(installedCssChunks[chunkId]);', - 'else if(installedCssChunks[chunkId] !== 0 && cssChunks[chunkId]) {', - Template.indent([ - 'promises.push(installedCssChunks[chunkId] = new Promise(function(resolve, reject) {', + return Template.asString([ + source, + '', + `// ${pluginName} CSS loading`, + `var cssChunks = ${JSON.stringify(chunkMap)};`, + 'if(installedCssChunks[chunkId]) promises.push(installedCssChunks[chunkId]);', + 'else if(installedCssChunks[chunkId] !== 0 && cssChunks[chunkId]) {', Template.indent([ - `var href = ${linkHrefPath};`, - `var fullhref = ${ - isWebpack4 - ? mainTemplate.requireFn - : webpack.RuntimeGlobals.require - }.p + href;`, - 'var existingLinkTags = document.getElementsByTagName("link");', - 'for(var i = 0; i < existingLinkTags.length; i++) {', - Template.indent([ - 'var tag = existingLinkTags[i];', - 'var dataHref = tag.getAttribute("data-href") || tag.getAttribute("href");', - 'if(tag.rel === "stylesheet" && (dataHref === href || dataHref === fullhref)) return resolve();', - ]), - '}', - 'var existingStyleTags = document.getElementsByTagName("style");', - 'for(var i = 0; i < existingStyleTags.length; i++) {', + 'promises.push(installedCssChunks[chunkId] = new Promise(function(resolve, reject) {', Template.indent([ - 'var tag = existingStyleTags[i];', - 'var dataHref = tag.getAttribute("data-href");', - 'if(dataHref === href || dataHref === fullhref) return resolve();', + `var href = ${linkHrefPath};`, + `var fullhref = ${mainTemplate.requireFn}.p + href;`, + 'var existingLinkTags = document.getElementsByTagName("link");', + 'for(var i = 0; i < existingLinkTags.length; i++) {', + Template.indent([ + 'var tag = existingLinkTags[i];', + 'var dataHref = tag.getAttribute("data-href") || tag.getAttribute("href");', + 'if(tag.rel === "stylesheet" && (dataHref === href || dataHref === fullhref)) return resolve();', + ]), + '}', + 'var existingStyleTags = document.getElementsByTagName("style");', + 'for(var i = 0; i < existingStyleTags.length; i++) {', + Template.indent([ + 'var tag = existingStyleTags[i];', + 'var dataHref = tag.getAttribute("data-href");', + 'if(dataHref === href || dataHref === fullhref) return resolve();', + ]), + '}', + 'var linkTag = document.createElement("link");', + 'linkTag.rel = "stylesheet";', + 'linkTag.type = "text/css";', + 'linkTag.onload = resolve;', + 'linkTag.onerror = function(event) {', + Template.indent([ + 'var request = event && event.target && event.target.src || fullhref;', + 'var err = new Error("Loading CSS chunk " + chunkId + " failed.\\n(" + request + ")");', + 'err.code = "CSS_CHUNK_LOAD_FAILED";', + 'err.request = request;', + 'delete installedCssChunks[chunkId]', + 'linkTag.parentNode.removeChild(linkTag)', + 'reject(err);', + ]), + '};', + 'linkTag.href = fullhref;', + crossOriginLoading + ? Template.asString([ + `if (linkTag.href.indexOf(window.location.origin + '/') !== 0) {`, + Template.indent( + `linkTag.crossOrigin = ${JSON.stringify( + crossOriginLoading + )};` + ), + '}', + ]) + : '', + 'var head = document.getElementsByTagName("head")[0];', + 'head.appendChild(linkTag);', ]), - '}', - 'var linkTag = document.createElement("link");', - 'linkTag.rel = "stylesheet";', - 'linkTag.type = "text/css";', - 'linkTag.onload = resolve;', - 'linkTag.onerror = function(event) {', - Template.indent([ - 'var request = event && event.target && event.target.src || fullhref;', - 'var err = new Error("Loading CSS chunk " + chunkId + " failed.\\n(" + request + ")");', - 'err.code = "CSS_CHUNK_LOAD_FAILED";', - 'err.request = request;', - 'delete installedCssChunks[chunkId]', - 'linkTag.parentNode.removeChild(linkTag)', - 'reject(err);', - ]), - '};', - 'linkTag.href = fullhref;', - crossOriginLoading - ? Template.asString([ - `if (linkTag.href.indexOf(window.location.origin + '/') !== 0) {`, - Template.indent( - `linkTag.crossOrigin = ${JSON.stringify( - crossOriginLoading - )};` - ), - '}', - ]) - : '', - 'var head = document.getElementsByTagName("head")[0];', - 'head.appendChild(linkTag);', + '}).then(function() {', + Template.indent(['installedCssChunks[chunkId] = 0;']), + '}));', ]), - '}).then(function() {', - Template.indent(['installedCssChunks[chunkId] = 0;']), - '}));', - ]), - '}', - ]); + '}', + ]); + } + + return source; } + ); + } else { + // eslint-disable-next-line global-require + const CssLoadingRuntimeModule = require('./CssLoadingRuntimeModule'); - return source; - } - ); + compilation.hooks.additionalTreeRuntimeRequirements.tap( + pluginName, + (chunk, set) => { + set.add(webpack.RuntimeGlobals.publicPath); + compilation.addRuntimeModule( + chunk, + new webpack.runtime.GetChunkFilenameRuntimeModule( + MODULE_TYPE, + 'mini-css', + `${webpack.RuntimeGlobals.require}.miniCssF`, + (referencedChunk) => + referencedChunk.canBeInitial() + ? ({ chunk: chunkData }) => + this.options.moduleFilename(chunkData) + : this.options.chunkFilename + ) + ); + compilation.addRuntimeModule( + chunk, + new CssLoadingRuntimeModule(set) + ); + } + ); + } }); }