UNPKG

webpack

Version:

Packs ECMAScript/CommonJs/AMD modules for the browser. Allows you to split your codebase into multiple bundles, which can be loaded on demand. Supports loaders to preprocess files, i.e. json, jsx, es7, css, less, ... and your custom stuff.

655 lines (635 loc) 23.6 kB
/* MIT License http://www.opensource.org/licenses/mit-license.php Author Tobias Koppers @sokra */ "use strict"; const { SyncWaterfallHook } = require("tapable"); /** @import Compilation from "../Compilation" */ const RuntimeGlobals = require("../RuntimeGlobals"); const RuntimeModule = require("../RuntimeModule"); const Template = require("../Template"); const compileBooleanMatcher = require("../util/compileBooleanMatcher"); const createHooksRegistry = require("../util/createHooksRegistry"); const { chunkHasCss } = require("./CssModulesPlugin"); /** @import Chunk, { ChunkId } from "../Chunk" */ /** @import { ReadOnlyRuntimeRequirements } from "../Module" */ const createCompilationHooks = () => ({ /** * @type {SyncWaterfallHook<[string, Chunk]>} * @since 5.66.0 */ createStylesheet: new SyncWaterfallHook(["source", "chunk"]), /** * @type {SyncWaterfallHook<[string, Chunk]>} * @since 5.91.0 */ linkPreload: new SyncWaterfallHook(["source", "chunk"]), /** * @type {SyncWaterfallHook<[string, Chunk]>} * @since 5.91.0 */ linkPrefetch: new SyncWaterfallHook(["source", "chunk"]), /** * @type {SyncWaterfallHook<[string, Chunk]>} * @since 5.107.0 */ linkInsert: new SyncWaterfallHook(["source", "chunk"]) }); /** * @typedef {ReturnType<typeof createCompilationHooks>} CssLoadingRuntimeModulePluginHooks */ class CssLoadingRuntimeModule extends RuntimeModule { /** * @param {ReadOnlyRuntimeRequirements} runtimeRequirements runtime requirements */ constructor(runtimeRequirements) { super("css loading", 10); /** @type {ReadOnlyRuntimeRequirements} */ this._runtimeRequirements = runtimeRequirements; } /** * Generates runtime code for this runtime module. * @returns {string | null} runtime code */ generate() { const { _runtimeRequirements } = this; const compilation = /** @type {Compilation} */ (this.compilation); const chunk = /** @type {Chunk} */ (this.chunk); const { chunkGraph, runtimeTemplate, outputOptions: { crossOriginLoading, uniqueName, chunkLoadTimeout: loadTimeout, charset, resourceHints } } = compilation; const dedupePrefetch = Boolean(resourceHints && resourceHints.dedupe); const fn = RuntimeGlobals.ensureChunkHandlers; /** @type {Set<ChunkId>} */ const initialChunkIds = new Set(); for (const c of chunk.getAllInitialChunks()) { if (chunkHasCss(c, chunkGraph)) { initialChunkIds.add(/** @type {ChunkId} */ (c.id)); } } // The predicate the stylesheet is emitted from: an `@import` external gives a // chunk a css asset without a `CSS_TYPE` module, and it still has to load. const conditionMap = chunkGraph.getChunkConditionMap(chunk, chunkHasCss); // An initial chunk is installed before the handler can run, so it never // reaches the loading path — with no other one there is nothing to load. const loadableConditionMap = { ...conditionMap }; for (const id of initialChunkIds) { loadableConditionMap[id] = false; } const hasCssMatcher = compileBooleanMatcher(loadableConditionMap) === false ? false : compileBooleanMatcher(conditionMap); const withLoading = _runtimeRequirements.has(RuntimeGlobals.ensureChunkHandlers) && hasCssMatcher !== false; /** @type {boolean} */ const withHmr = _runtimeRequirements.has( RuntimeGlobals.hmrDownloadUpdateHandlers ); if (!withLoading && !withHmr) { return null; } const environment = compilation.outputOptions.environment; const isNeutralPlatform = runtimeTemplate.isNeutralPlatform(); const withPrefetch = this._runtimeRequirements.has(RuntimeGlobals.prefetchChunkHandlers) && (environment.document || isNeutralPlatform) && chunk.hasChildByOrder(chunkGraph, "prefetch", true, chunkHasCss); const withPreload = this._runtimeRequirements.has(RuntimeGlobals.preloadChunkHandlers) && (environment.document || isNeutralPlatform) && (chunk.hasChildByOrder(chunkGraph, "preload", true, chunkHasCss) || // `parser.javascript.dynamicImportCssPreload` — CSS-only preload order. chunk.hasChildByOrder(chunkGraph, "cssPreload", true, chunkHasCss)); // Under module output each stylesheet is a known file, so the urls are written // out and read by id rather than built from the chunk id at runtime. const cssUrls = runtimeTemplate.analyzableCssChunkUrls( chunk, chunkGraph, _runtimeRequirements, this ); const { linkPreload, linkPrefetch, createStylesheet, linkInsert } = CssLoadingRuntimeModule.getCompilationHooks(compilation); const withFetchPriority = _runtimeRequirements.has( RuntimeGlobals.hasFetchPriority ); const stateExpression = withHmr ? `${RuntimeGlobals.hmrRuntimeStatePrefix}_css` : undefined; const code = Template.asString([ "link = document.createElement('link');", charset ? "link.charset = 'utf-8';" : "", `if (${RuntimeGlobals.scriptNonce}) {`, Template.indent( `link.setAttribute("nonce", ${RuntimeGlobals.scriptNonce});` ), "}", uniqueName ? 'link.setAttribute("data-webpack", uniqueName + ":" + key);' : "", withFetchPriority ? Template.asString([ "if(fetchPriority) {", Template.indent( 'link.setAttribute("fetchpriority", fetchPriority);' ), "}" ]) : "", "link.setAttribute(loadingAttribute, 1);", 'link.rel = "stylesheet";', "link.href = url;", crossOriginLoading ? crossOriginLoading === "use-credentials" ? 'link.crossOrigin = "use-credentials";' : Template.asString([ "if (link.href.indexOf(window.location.origin + '/') !== 0) {", Template.indent( `link.crossOrigin = ${JSON.stringify(crossOriginLoading)};` ), "}" ]) : "" ]); const cst = runtimeTemplate.renderConst(); const lt = runtimeTemplate.renderLet(); const installedChunksObject = `{\n${Template.indent( Array.from(initialChunkIds, (id) => `${JSON.stringify(id)}: 0`).join( ",\n" ) )}\n}`; /** * @param {string} id expression naming the chunk whose stylesheet is wanted * @returns {string} expression building its url at runtime */ const runtimeCssUrl = (id) => `${RuntimeGlobals.publicPath} + ${RuntimeGlobals.getChunkCssFilename}(${id})`; // The url is read through a thunk so a runtime carrying many stylesheets builds // only the one it loads. /** * @param {string} id expression naming the chunk whose stylesheet is wanted * @returns {string} expression evaluating to its url */ const cssUrl = (id) => { if (!cssUrls) return runtimeCssUrl(id); // An id the incomplete map lacks still resolves through the runtime lookup, // which the plugin kept required for exactly this case. return cssUrls.complete ? `cssUrls[${id}]()` : `(cssUrls[${id}] ? cssUrls[${id}]() : ${runtimeCssUrl(id)})`; }; return Template.asString([ ...(cssUrls ? [ `${cst} cssUrls = {\n${Template.indent( Array.from( cssUrls.urls, ([id, url]) => `${JSON.stringify(String(id))}: ${runtimeTemplate.returningFunction(url)}` ).join(",\n") )}\n};` ] : []), "// object to store loaded and loading chunks", "// undefined = chunk not loaded, null = chunk preloaded/prefetched", "// [resolve, reject, Promise] = chunk loading, 0 = chunk loaded", `${cst} installedChunks = ${ stateExpression ? runtimeTemplate.assignOr(stateExpression, installedChunksObject) : installedChunksObject };`, "", uniqueName ? `${cst} uniqueName = ${JSON.stringify( runtimeTemplate.outputOptions.uniqueName )};` : "// data-webpack is not used as build has no uniqueName", withLoading || withHmr ? Template.asString([ `${cst} loadingAttribute = "data-webpack-loading";`, `${cst} loadStylesheet = ${runtimeTemplate.basicFunction( `chunkId, url, done${ withFetchPriority ? ", fetchPriority" : "" }${withHmr ? ", hmr" : ""}`, [ `${lt} link, needAttach, key = "chunk-" + chunkId;`, withHmr ? "if(!hmr) {" : "", `${cst} links = document.getElementsByTagName("link");`, "for(var i = 0; i < links.length; i++) {", Template.indent([ `${cst} l = links[i];`, `if(l.rel == "stylesheet" && (${ withHmr ? 'l.href.startsWith(url) || l.getAttribute("href").startsWith(url)' : 'l.href == url || l.getAttribute("href") == url' }${ uniqueName ? ' || l.getAttribute("data-webpack") == uniqueName + ":" + key' : "" })) { link = l; break; }` ]), "}", "if(!done) return link;", withHmr ? "}" : "", "if(!link) {", Template.indent([ "needAttach = true;", createStylesheet.call(code, /** @type {Chunk} */ (this.chunk)) ]), "}", `${lt} timeout;`, `${cst} onLinkComplete = ${runtimeTemplate.basicFunction( "prev, event", Template.asString([ "link.onerror = link.onload = null;", "link.removeAttribute(loadingAttribute);", "clearTimeout(timeout);", 'if(event && event.type != "load") link.parentNode.removeChild(link)', "done(event);", "if(prev) return prev(event);" ]) )};`, "if(link.getAttribute(loadingAttribute)) {", Template.indent([ `timeout = setTimeout(onLinkComplete.bind(null, undefined, { type: 'timeout', target: link }), ${loadTimeout});`, "link.onerror = onLinkComplete.bind(null, link.onerror);", "link.onload = onLinkComplete.bind(null, link.onload);" ]), "} else onLinkComplete(undefined, { type: 'load', target: link });", // We assume any existing stylesheet is render blocking withHmr && withFetchPriority ? 'if (hmr && hmr.getAttribute("fetchpriority")) link.setAttribute("fetchpriority", hmr.getAttribute("fetchpriority"));' : "", linkInsert.call( withHmr ? Template.asString([ "if (hmr) {", Template.indent( "hmr.parentNode.insertBefore(link, hmr);" ), "} else if (needAttach) {", Template.indent("document.head.appendChild(link);"), "}" ]) : Template.asString([ "if (needAttach) {", Template.indent("document.head.appendChild(link);"), "}" ]), /** @type {Chunk} */ (this.chunk) ), "return link;" ] )};` ]) : "", withLoading ? Template.asString([ `${fn}.css = ${runtimeTemplate.basicFunction( `chunkId, promises${withFetchPriority ? " , fetchPriority" : ""}`, [ "// css chunk loading", `${lt} installedChunkData = ${RuntimeGlobals.hasOwnProperty}(installedChunks, chunkId) ? installedChunks[chunkId] : undefined;`, 'if(installedChunkData !== 0) { // 0 means "already installed".', Template.indent([ "", '// a Promise means "currently loading".', "if(installedChunkData) {", Template.indent(["promises.push(installedChunkData[2]);"]), "} else {", Template.indent([ hasCssMatcher === true ? "if(true) { // all chunks have CSS" : `if(${hasCssMatcher("chunkId")}) {`, Template.indent([ "// setup Promise in chunk cache", `${cst} promise = new Promise(${runtimeTemplate.expressionFunction( "installedChunkData = installedChunks[chunkId] = [resolve, reject]", "resolve, reject" )});`, "promises.push(installedChunkData[2] = promise);", "", "// start chunk loading", `${cst} url = ${cssUrl("chunkId")};`, "// create error before stack unwound to get useful stacktrace later", `${cst} error = new Error();`, `${cst} loadingEnded = ${runtimeTemplate.basicFunction( "event", [ `if(${RuntimeGlobals.hasOwnProperty}(installedChunks, chunkId)) {`, Template.indent([ "installedChunkData = installedChunks[chunkId];", "if(installedChunkData !== 0) installedChunks[chunkId] = undefined;", "if(installedChunkData) {", Template.indent([ 'if(event.type !== "load") {', Template.indent([ `${cst} errorType = event && event.type;`, `${cst} realHref = event && event.target && event.target.href;`, "error.message = 'Loading css chunk ' + chunkId + ' failed.\\n(' + errorType + ': ' + realHref + ')';", "error.name = 'ChunkLoadError';", "error.type = errorType;", "error.request = realHref;", "error.event = event;", "installedChunkData[1](error);" ]), "} else {", Template.indent([ "installedChunks[chunkId] = 0;", "installedChunkData[0]();" ]), "}" ]), "}" ]), "}" ] )};`, isNeutralPlatform ? "if (typeof document !== 'undefined') {" : "", Template.indent([ `loadStylesheet(chunkId, url, loadingEnded${ withFetchPriority ? ", fetchPriority" : "" });` ]), isNeutralPlatform ? Template.asString([ "} else {", Template.indent([ // no DOM (Node SSR): read the emitted CSS via dynamic import('fs') (works on every node), collect it; never reject on a missing file `Promise.all([import('fs'), import('url')]).then(${runtimeTemplate.basicFunction( "[{ readFile }, { URL }]", [ `readFile(${runtimeTemplate.importMetaUrl("url")}, 'utf8', ${runtimeTemplate.basicFunction( "err, content", [ `if (!err) ${runtimeTemplate.cssServerStyleRegistry()}["chunk-" + chunkId] = content;`, "loadingEnded({ type: 'load' });" ] )});` ] )});` ]), "}" ]) : "" ]), "} else installedChunks[chunkId] = 0;" ]), "}" ]), "}" ] )};` ]) : "// no chunk loading", "", withPrefetch && hasCssMatcher !== false ? `${ RuntimeGlobals.prefetchChunkHandlers }.s = ${runtimeTemplate.basicFunction("chunkId", [ `if((!${ RuntimeGlobals.hasOwnProperty }(installedChunks, chunkId) || installedChunks[chunkId] === undefined) && ${ hasCssMatcher === true ? "true" : hasCssMatcher("chunkId") }) {`, Template.indent([ "installedChunks[chunkId] = null;", // prefetch is a browser-only resource hint; no-op without a DOM (e.g. node side of a universal build) isNeutralPlatform ? "if (typeof document === 'undefined') return;" : "", linkPrefetch.call( Template.asString([ `${cst} link = document.createElement('link');`, charset ? "link.charset = 'utf-8';" : "", crossOriginLoading ? `link.crossOrigin = ${JSON.stringify( crossOriginLoading )};` : "", `if (${RuntimeGlobals.scriptNonce}) {`, Template.indent( `link.setAttribute("nonce", ${RuntimeGlobals.scriptNonce});` ), "}", 'link.rel = "prefetch";', 'link.as = "style";', `link.href = ${cssUrl("chunkId")};` ]), chunk ), dedupePrefetch ? Template.asString([ // Chrome re-requests a resource when a prefetch link is added // after it was already (pre)loaded via markup; skip in that case. `${cst} links = document.getElementsByTagName("link");`, `for(${lt} i = 0; i < links.length; i++) {`, Template.indent([ `${cst} l = links[i];`, 'if(l.href === link.href && (l.rel === "prefetch" || l.rel === "preload")) return;' ]), "}" ]) : "", "document.head.appendChild(link);" ]), "}" ])};` : "// no prefetching", "", withPreload && hasCssMatcher !== false ? `${ RuntimeGlobals.preloadChunkHandlers }.s = ${runtimeTemplate.basicFunction("chunkId", [ `if((!${ RuntimeGlobals.hasOwnProperty }(installedChunks, chunkId) || installedChunks[chunkId] === undefined) && ${ hasCssMatcher === true ? "true" : hasCssMatcher("chunkId") }) {`, Template.indent([ "installedChunks[chunkId] = null;", // preload is a browser-only resource hint; no-op without a DOM (e.g. node side of a universal build) isNeutralPlatform ? "if (typeof document === 'undefined') return;" : "", linkPreload.call( Template.asString([ `${cst} link = document.createElement('link');`, charset ? "link.charset = 'utf-8';" : "", `if (${RuntimeGlobals.scriptNonce}) {`, Template.indent( `link.setAttribute("nonce", ${RuntimeGlobals.scriptNonce});` ), "}", 'link.rel = "preload";', 'link.as = "style";', `link.href = ${cssUrl("chunkId")};`, crossOriginLoading ? crossOriginLoading === "use-credentials" ? 'link.crossOrigin = "use-credentials";' : Template.asString([ "if (link.href.indexOf(window.location.origin + '/') !== 0) {", Template.indent( `link.crossOrigin = ${JSON.stringify( crossOriginLoading )};` ), "}" ]) : "" ]), chunk ), "document.head.appendChild(link);" ]), "}" ])};` : "// no preloaded", withHmr ? Template.asString([ `${cst} oldTags = [];`, `${cst} newTags = [];`, `${cst} applyHandler = ${runtimeTemplate.basicFunction("options", [ `return { ${runtimeTemplate.method("dispose", "", [ "while(oldTags.length) {", Template.indent([ `${cst} oldTag = oldTags.pop();`, `if(${runtimeTemplate.optionalChaining("oldTag", "parentNode")}) oldTag.parentNode.removeChild(oldTag);` ]), "}" ])}, ${runtimeTemplate.method("apply", "", [ "while(newTags.length) {", Template.indent([ `${cst} newTag = newTags.pop();`, "newTag.sheet.disabled = false" ]), "}" ])} };` ])}`, `${cst} cssTextKey = ${runtimeTemplate.returningFunction( `Array.from(link.sheet.cssRules, ${runtimeTemplate.returningFunction( "r.cssText", "r" )}).join()`, "link" )};`, `${ RuntimeGlobals.hmrDownloadUpdateHandlers }.css = ${runtimeTemplate.basicFunction( "chunkIds, removedChunks, removedModules, promises, applyHandlers, updatedModulesList, css", [ isNeutralPlatform ? Template.asString([ "if (typeof document === 'undefined') {", Template.indent([ // node SSR: refresh the server style registry from the re-emitted CSS instead of touching the DOM `${cst} cssRemovedChunks = css && css.r;`, `${cst} registry = ${runtimeTemplate.cssServerStyleRegistry()};`, `chunkIds.forEach(${runtimeTemplate.basicFunction( "chunkId", [ `${cst} key = "chunk-" + chunkId;`, `if(${runtimeTemplate.optionalChaining( "cssRemovedChunks", "indexOf(chunkId)" )} >= 0) { delete registry[key]; return; }`, `${cst} url = ${RuntimeGlobals.publicPath} + ${RuntimeGlobals.getChunkCssFilename}(chunkId);`, `promises.push(Promise.all([import('fs'), import('url')]).then(${runtimeTemplate.basicFunction( "[{ readFile }, { URL }]", [ `return new Promise(${runtimeTemplate.basicFunction( "resolve", [ // best-effort: a non-file publicPath (e.g. a CDN) can't be read from disk, so skip "try {", Template.indent([ `readFile(${runtimeTemplate.importMetaUrl("url")}, 'utf8', ${runtimeTemplate.basicFunction( "err, content", [ "if (!err) registry[key] = content;", "resolve();" ] )});` ]), "} catch (e) { resolve(); }" ] )});` ] )}));` ] )});`, "return;" ]), "}" ]) : "", "applyHandlers.push(applyHandler);", "// Read CSS removed chunks from update manifest", `${cst} cssRemovedChunks = css && css.r;`, `chunkIds.forEach(${runtimeTemplate.basicFunction("chunkId", [ `${cst} url = ${RuntimeGlobals.publicPath} + ${RuntimeGlobals.getChunkCssFilename}(chunkId);`, `${cst} oldTag = loadStylesheet(chunkId, url);`, `if(!oldTag && !${withHmr} ) return;`, "// Skip if CSS was removed", `if(${runtimeTemplate.optionalChaining( "cssRemovedChunks", "indexOf(chunkId)" )} >= 0) {`, Template.indent(["oldTags.push(oldTag);", "return;"]), "}", "", "// create error before stack unwound to get useful stacktrace later", `${cst} error = new Error();`, `promises.push(new Promise(${runtimeTemplate.basicFunction( "resolve, reject", [ `${cst} link = loadStylesheet(chunkId, url + (url.indexOf("?") < 0 ? "?" : "&") + "hmr=" + Date.now(), ${runtimeTemplate.basicFunction( "event", [ 'if(event.type !== "load") {', Template.indent([ `${cst} errorType = event && event.type;`, `${cst} realHref = event && event.target && event.target.href;`, "error.message = 'Loading css hot update chunk ' + chunkId + ' failed.\\n(' + errorType + ': ' + realHref + ')';", "error.name = 'ChunkLoadError';", "error.type = errorType;", "error.request = realHref;", "error.event = event;", "reject(error);" ]), "} else {", Template.indent([ "try { if(cssTextKey(oldTag) == cssTextKey(link)) { if(link.parentNode) link.parentNode.removeChild(link); return resolve(); } } catch(e) {}", "link.sheet.disabled = true;", "oldTags.push(oldTag);", "newTags.push(link);", "resolve();" ]), "}" ] )}, ${withFetchPriority ? "undefined," : ""} oldTag);` ] )}));` ])});` ] )}` ]) : "// no hmr" ]); } } CssLoadingRuntimeModule.getCompilationHooks = createHooksRegistry( createCompilationHooks ); module.exports = CssLoadingRuntimeModule;