CssLoadingRuntimeModule.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Tobias Koppers @sokra
  4. */
  5. "use strict";
  6. const { SyncWaterfallHook } = require("tapable");
  7. const Compilation = require("../Compilation");
  8. const { CSS_TYPE } = require("../ModuleSourceTypeConstants");
  9. const RuntimeGlobals = require("../RuntimeGlobals");
  10. const RuntimeModule = require("../RuntimeModule");
  11. const Template = require("../Template");
  12. const compileBooleanMatcher = require("../util/compileBooleanMatcher");
  13. const { chunkHasCss } = require("./CssModulesPlugin");
  14. /** @typedef {import("../Chunk")} Chunk */
  15. /** @typedef {import("../Chunk").ChunkId} ChunkId */
  16. /** @typedef {import("../ChunkGraph")} ChunkGraph */
  17. /** @typedef {import("../Module").ReadOnlyRuntimeRequirements} ReadOnlyRuntimeRequirements */
  18. /**
  19. * @typedef {object} CssLoadingRuntimeModulePluginHooks
  20. * @property {SyncWaterfallHook<[string, Chunk]>} createStylesheet
  21. * @property {SyncWaterfallHook<[string, Chunk]>} linkPreload
  22. * @property {SyncWaterfallHook<[string, Chunk]>} linkPrefetch
  23. */
  24. /** @type {WeakMap<Compilation, CssLoadingRuntimeModulePluginHooks>} */
  25. const compilationHooksMap = new WeakMap();
  26. class CssLoadingRuntimeModule extends RuntimeModule {
  27. /**
  28. * @param {Compilation} compilation the compilation
  29. * @returns {CssLoadingRuntimeModulePluginHooks} hooks
  30. */
  31. static getCompilationHooks(compilation) {
  32. if (!(compilation instanceof Compilation)) {
  33. throw new TypeError(
  34. "The 'compilation' argument must be an instance of Compilation"
  35. );
  36. }
  37. let hooks = compilationHooksMap.get(compilation);
  38. if (hooks === undefined) {
  39. hooks = {
  40. createStylesheet: new SyncWaterfallHook(["source", "chunk"]),
  41. linkPreload: new SyncWaterfallHook(["source", "chunk"]),
  42. linkPrefetch: new SyncWaterfallHook(["source", "chunk"])
  43. };
  44. compilationHooksMap.set(compilation, hooks);
  45. }
  46. return hooks;
  47. }
  48. /**
  49. * @param {ReadOnlyRuntimeRequirements} runtimeRequirements runtime requirements
  50. */
  51. constructor(runtimeRequirements) {
  52. super("css loading", 10);
  53. this._runtimeRequirements = runtimeRequirements;
  54. }
  55. /**
  56. * Generates runtime code for this runtime module.
  57. * @returns {string | null} runtime code
  58. */
  59. generate() {
  60. const { _runtimeRequirements } = this;
  61. const compilation = /** @type {Compilation} */ (this.compilation);
  62. const chunk = /** @type {Chunk} */ (this.chunk);
  63. const {
  64. chunkGraph,
  65. runtimeTemplate,
  66. outputOptions: {
  67. crossOriginLoading,
  68. uniqueName,
  69. chunkLoadTimeout: loadTimeout,
  70. charset
  71. }
  72. } = compilation;
  73. const fn = RuntimeGlobals.ensureChunkHandlers;
  74. const conditionMap = chunkGraph.getChunkConditionMap(
  75. chunk,
  76. /**
  77. * @param {Chunk} chunk the chunk
  78. * @param {ChunkGraph} chunkGraph the chunk graph
  79. * @returns {boolean} true, if the chunk has css
  80. */
  81. (chunk, chunkGraph) =>
  82. Boolean(chunkGraph.getChunkModulesIterableBySourceType(chunk, CSS_TYPE))
  83. );
  84. const hasCssMatcher = compileBooleanMatcher(conditionMap);
  85. const withLoading =
  86. _runtimeRequirements.has(RuntimeGlobals.ensureChunkHandlers) &&
  87. hasCssMatcher !== false;
  88. /** @type {boolean} */
  89. const withHmr = _runtimeRequirements.has(
  90. RuntimeGlobals.hmrDownloadUpdateHandlers
  91. );
  92. /** @type {Set<ChunkId>} */
  93. const initialChunkIds = new Set();
  94. for (const c of chunk.getAllInitialChunks()) {
  95. if (chunkHasCss(c, chunkGraph)) {
  96. initialChunkIds.add(/** @type {ChunkId} */ (c.id));
  97. }
  98. }
  99. if (!withLoading && !withHmr) {
  100. return null;
  101. }
  102. const environment = compilation.outputOptions.environment;
  103. const isNeutralPlatform = runtimeTemplate.isNeutralPlatform();
  104. const withPrefetch =
  105. this._runtimeRequirements.has(RuntimeGlobals.prefetchChunkHandlers) &&
  106. (environment.document || isNeutralPlatform) &&
  107. chunk.hasChildByOrder(chunkGraph, "prefetch", true, chunkHasCss);
  108. const withPreload =
  109. this._runtimeRequirements.has(RuntimeGlobals.preloadChunkHandlers) &&
  110. (environment.document || isNeutralPlatform) &&
  111. chunk.hasChildByOrder(chunkGraph, "preload", true, chunkHasCss);
  112. const { linkPreload, linkPrefetch, createStylesheet } =
  113. CssLoadingRuntimeModule.getCompilationHooks(compilation);
  114. const withFetchPriority = _runtimeRequirements.has(
  115. RuntimeGlobals.hasFetchPriority
  116. );
  117. const stateExpression = withHmr
  118. ? `${RuntimeGlobals.hmrRuntimeStatePrefix}_css`
  119. : undefined;
  120. const code = Template.asString([
  121. "link = document.createElement('link');",
  122. charset ? "link.charset = 'utf-8';" : "",
  123. `if (${RuntimeGlobals.scriptNonce}) {`,
  124. Template.indent(
  125. `link.setAttribute("nonce", ${RuntimeGlobals.scriptNonce});`
  126. ),
  127. "}",
  128. uniqueName
  129. ? 'link.setAttribute("data-webpack", uniqueName + ":" + key);'
  130. : "",
  131. withFetchPriority
  132. ? Template.asString([
  133. "if(fetchPriority) {",
  134. Template.indent(
  135. 'link.setAttribute("fetchpriority", fetchPriority);'
  136. ),
  137. "}"
  138. ])
  139. : "",
  140. "link.setAttribute(loadingAttribute, 1);",
  141. 'link.rel = "stylesheet";',
  142. "link.href = url;",
  143. crossOriginLoading
  144. ? crossOriginLoading === "use-credentials"
  145. ? 'link.crossOrigin = "use-credentials";'
  146. : Template.asString([
  147. "if (link.href.indexOf(window.location.origin + '/') !== 0) {",
  148. Template.indent(
  149. `link.crossOrigin = ${JSON.stringify(crossOriginLoading)};`
  150. ),
  151. "}"
  152. ])
  153. : ""
  154. ]);
  155. return Template.asString([
  156. "// object to store loaded and loading chunks",
  157. "// undefined = chunk not loaded, null = chunk preloaded/prefetched",
  158. "// [resolve, reject, Promise] = chunk loading, 0 = chunk loaded",
  159. `var installedChunks = ${
  160. stateExpression ? `${stateExpression} = ${stateExpression} || ` : ""
  161. }{`,
  162. Template.indent(
  163. Array.from(initialChunkIds, (id) => `${JSON.stringify(id)}: 0`).join(
  164. ",\n"
  165. )
  166. ),
  167. "};",
  168. "",
  169. uniqueName
  170. ? `var uniqueName = ${JSON.stringify(
  171. runtimeTemplate.outputOptions.uniqueName
  172. )};`
  173. : "// data-webpack is not used as build has no uniqueName",
  174. withLoading || withHmr
  175. ? Template.asString([
  176. 'var loadingAttribute = "data-webpack-loading";',
  177. `var loadStylesheet = ${runtimeTemplate.basicFunction(
  178. `chunkId, url, done${
  179. withFetchPriority ? ", fetchPriority" : ""
  180. }${withHmr ? ", hmr" : ""}`,
  181. [
  182. 'var link, needAttach, key = "chunk-" + chunkId;',
  183. withHmr ? "if(!hmr) {" : "",
  184. 'var links = document.getElementsByTagName("link");',
  185. "for(var i = 0; i < links.length; i++) {",
  186. Template.indent([
  187. "var l = links[i];",
  188. `if(l.rel == "stylesheet" && (${
  189. withHmr
  190. ? 'l.href.startsWith(url) || l.getAttribute("href").startsWith(url)'
  191. : 'l.href == url || l.getAttribute("href") == url'
  192. }${
  193. uniqueName
  194. ? ' || l.getAttribute("data-webpack") == uniqueName + ":" + key'
  195. : ""
  196. })) { link = l; break; }`
  197. ]),
  198. "}",
  199. "if(!done) return link;",
  200. withHmr ? "}" : "",
  201. "if(!link) {",
  202. Template.indent([
  203. "needAttach = true;",
  204. createStylesheet.call(code, /** @type {Chunk} */ (this.chunk))
  205. ]),
  206. "}",
  207. `var onLinkComplete = ${runtimeTemplate.basicFunction(
  208. "prev, event",
  209. Template.asString([
  210. "link.onerror = link.onload = null;",
  211. "link.removeAttribute(loadingAttribute);",
  212. "clearTimeout(timeout);",
  213. 'if(event && event.type != "load") link.parentNode.removeChild(link)',
  214. "done(event);",
  215. "if(prev) return prev(event);"
  216. ])
  217. )};`,
  218. "if(link.getAttribute(loadingAttribute)) {",
  219. Template.indent([
  220. `var timeout = setTimeout(onLinkComplete.bind(null, undefined, { type: 'timeout', target: link }), ${loadTimeout});`,
  221. "link.onerror = onLinkComplete.bind(null, link.onerror);",
  222. "link.onload = onLinkComplete.bind(null, link.onload);"
  223. ]),
  224. "} else onLinkComplete(undefined, { type: 'load', target: link });", // We assume any existing stylesheet is render blocking
  225. withHmr && withFetchPriority
  226. ? 'if (hmr && hmr.getAttribute("fetchpriority")) link.setAttribute("fetchpriority", hmr.getAttribute("fetchpriority"));'
  227. : "",
  228. withHmr ? "hmr ? document.head.insertBefore(link, hmr) :" : "",
  229. "needAttach && document.head.appendChild(link);",
  230. "return link;"
  231. ]
  232. )};`
  233. ])
  234. : "",
  235. withLoading
  236. ? Template.asString([
  237. `${fn}.css = ${runtimeTemplate.basicFunction(
  238. `chunkId, promises${withFetchPriority ? " , fetchPriority" : ""}`,
  239. [
  240. "// css chunk loading",
  241. `var installedChunkData = ${RuntimeGlobals.hasOwnProperty}(installedChunks, chunkId) ? installedChunks[chunkId] : undefined;`,
  242. 'if(installedChunkData !== 0) { // 0 means "already installed".',
  243. Template.indent([
  244. "",
  245. '// a Promise means "currently loading".',
  246. "if(installedChunkData) {",
  247. Template.indent(["promises.push(installedChunkData[2]);"]),
  248. "} else {",
  249. Template.indent([
  250. hasCssMatcher === true
  251. ? "if(true) { // all chunks have CSS"
  252. : `if(${hasCssMatcher("chunkId")}) {`,
  253. Template.indent([
  254. "// setup Promise in chunk cache",
  255. `var promise = new Promise(${runtimeTemplate.expressionFunction(
  256. "installedChunkData = installedChunks[chunkId] = [resolve, reject]",
  257. "resolve, reject"
  258. )});`,
  259. "promises.push(installedChunkData[2] = promise);",
  260. "",
  261. "// start chunk loading",
  262. `var url = ${RuntimeGlobals.publicPath} + ${RuntimeGlobals.getChunkCssFilename}(chunkId);`,
  263. "// create error before stack unwound to get useful stacktrace later",
  264. "var error = new Error();",
  265. `var loadingEnded = ${runtimeTemplate.basicFunction(
  266. "event",
  267. [
  268. `if(${RuntimeGlobals.hasOwnProperty}(installedChunks, chunkId)) {`,
  269. Template.indent([
  270. "installedChunkData = installedChunks[chunkId];",
  271. "if(installedChunkData !== 0) installedChunks[chunkId] = undefined;",
  272. "if(installedChunkData) {",
  273. Template.indent([
  274. 'if(event.type !== "load") {',
  275. Template.indent([
  276. "var errorType = event && event.type;",
  277. "var realHref = event && event.target && event.target.href;",
  278. "error.message = 'Loading css chunk ' + chunkId + ' failed.\\n(' + errorType + ': ' + realHref + ')';",
  279. "error.name = 'ChunkLoadError';",
  280. "error.type = errorType;",
  281. "error.request = realHref;",
  282. "installedChunkData[1](error);"
  283. ]),
  284. "} else {",
  285. Template.indent([
  286. "installedChunks[chunkId] = 0;",
  287. "installedChunkData[0]();"
  288. ]),
  289. "}"
  290. ]),
  291. "}"
  292. ]),
  293. "}"
  294. ]
  295. )};`,
  296. isNeutralPlatform
  297. ? "if (typeof document !== 'undefined') {"
  298. : "",
  299. Template.indent([
  300. `loadStylesheet(chunkId, url, loadingEnded${
  301. withFetchPriority ? ", fetchPriority" : ""
  302. });`
  303. ]),
  304. isNeutralPlatform
  305. ? "} else { loadingEnded({ type: 'load' }); }"
  306. : ""
  307. ]),
  308. "} else installedChunks[chunkId] = 0;"
  309. ]),
  310. "}"
  311. ]),
  312. "}"
  313. ]
  314. )};`
  315. ])
  316. : "// no chunk loading",
  317. "",
  318. withPrefetch && hasCssMatcher !== false
  319. ? `${
  320. RuntimeGlobals.prefetchChunkHandlers
  321. }.s = ${runtimeTemplate.basicFunction("chunkId", [
  322. `if((!${
  323. RuntimeGlobals.hasOwnProperty
  324. }(installedChunks, chunkId) || installedChunks[chunkId] === undefined) && ${
  325. hasCssMatcher === true ? "true" : hasCssMatcher("chunkId")
  326. }) {`,
  327. Template.indent([
  328. "installedChunks[chunkId] = null;",
  329. isNeutralPlatform
  330. ? "if (typeof document === 'undefined') return;"
  331. : "",
  332. linkPrefetch.call(
  333. Template.asString([
  334. "var link = document.createElement('link');",
  335. charset ? "link.charset = 'utf-8';" : "",
  336. crossOriginLoading
  337. ? `link.crossOrigin = ${JSON.stringify(
  338. crossOriginLoading
  339. )};`
  340. : "",
  341. `if (${RuntimeGlobals.scriptNonce}) {`,
  342. Template.indent(
  343. `link.setAttribute("nonce", ${RuntimeGlobals.scriptNonce});`
  344. ),
  345. "}",
  346. 'link.rel = "prefetch";',
  347. 'link.as = "style";',
  348. `link.href = ${RuntimeGlobals.publicPath} + ${RuntimeGlobals.getChunkCssFilename}(chunkId);`
  349. ]),
  350. chunk
  351. ),
  352. "document.head.appendChild(link);"
  353. ]),
  354. "}"
  355. ])};`
  356. : "// no prefetching",
  357. "",
  358. withPreload && hasCssMatcher !== false
  359. ? `${
  360. RuntimeGlobals.preloadChunkHandlers
  361. }.s = ${runtimeTemplate.basicFunction("chunkId", [
  362. `if((!${
  363. RuntimeGlobals.hasOwnProperty
  364. }(installedChunks, chunkId) || installedChunks[chunkId] === undefined) && ${
  365. hasCssMatcher === true ? "true" : hasCssMatcher("chunkId")
  366. }) {`,
  367. Template.indent([
  368. "installedChunks[chunkId] = null;",
  369. isNeutralPlatform
  370. ? "if (typeof document === 'undefined') return;"
  371. : "",
  372. linkPreload.call(
  373. Template.asString([
  374. "var link = document.createElement('link');",
  375. charset ? "link.charset = 'utf-8';" : "",
  376. `if (${RuntimeGlobals.scriptNonce}) {`,
  377. Template.indent(
  378. `link.setAttribute("nonce", ${RuntimeGlobals.scriptNonce});`
  379. ),
  380. "}",
  381. 'link.rel = "preload";',
  382. 'link.as = "style";',
  383. `link.href = ${RuntimeGlobals.publicPath} + ${RuntimeGlobals.getChunkCssFilename}(chunkId);`,
  384. crossOriginLoading
  385. ? crossOriginLoading === "use-credentials"
  386. ? 'link.crossOrigin = "use-credentials";'
  387. : Template.asString([
  388. "if (link.href.indexOf(window.location.origin + '/') !== 0) {",
  389. Template.indent(
  390. `link.crossOrigin = ${JSON.stringify(
  391. crossOriginLoading
  392. )};`
  393. ),
  394. "}"
  395. ])
  396. : ""
  397. ]),
  398. chunk
  399. ),
  400. "document.head.appendChild(link);"
  401. ]),
  402. "}"
  403. ])};`
  404. : "// no preloaded",
  405. withHmr
  406. ? Template.asString([
  407. "var oldTags = [];",
  408. "var newTags = [];",
  409. `var applyHandler = ${runtimeTemplate.basicFunction("options", [
  410. `return { dispose: ${runtimeTemplate.basicFunction("", [
  411. "while(oldTags.length) {",
  412. Template.indent([
  413. "var oldTag = oldTags.pop();",
  414. "if(oldTag && oldTag.parentNode) oldTag.parentNode.removeChild(oldTag);"
  415. ]),
  416. "}"
  417. ])}, apply: ${runtimeTemplate.basicFunction("", [
  418. "while(newTags.length) {",
  419. Template.indent([
  420. "var newTag = newTags.pop();",
  421. "newTag.sheet.disabled = false"
  422. ]),
  423. "}"
  424. ])} };`
  425. ])}`,
  426. `var cssTextKey = ${runtimeTemplate.returningFunction(
  427. `Array.from(link.sheet.cssRules, ${runtimeTemplate.returningFunction(
  428. "r.cssText",
  429. "r"
  430. )}).join()`,
  431. "link"
  432. )};`,
  433. `${
  434. RuntimeGlobals.hmrDownloadUpdateHandlers
  435. }.css = ${runtimeTemplate.basicFunction(
  436. "chunkIds, removedChunks, removedModules, promises, applyHandlers, updatedModulesList, css",
  437. [
  438. isNeutralPlatform
  439. ? "if (typeof document === 'undefined') return;"
  440. : "",
  441. "applyHandlers.push(applyHandler);",
  442. "// Read CSS removed chunks from update manifest",
  443. "var cssRemovedChunks = css && css.r;",
  444. `chunkIds.forEach(${runtimeTemplate.basicFunction("chunkId", [
  445. `var filename = ${RuntimeGlobals.getChunkCssFilename}(chunkId);`,
  446. `var url = ${RuntimeGlobals.publicPath} + filename;`,
  447. "var oldTag = loadStylesheet(chunkId, url);",
  448. `if(!oldTag && !${withHmr} ) return;`,
  449. "// Skip if CSS was removed",
  450. "if(cssRemovedChunks && cssRemovedChunks.indexOf(chunkId) >= 0) {",
  451. Template.indent(["oldTags.push(oldTag);", "return;"]),
  452. "}",
  453. "",
  454. "// create error before stack unwound to get useful stacktrace later",
  455. "var error = new Error();",
  456. `promises.push(new Promise(${runtimeTemplate.basicFunction(
  457. "resolve, reject",
  458. [
  459. `var link = loadStylesheet(chunkId, url + (url.indexOf("?") < 0 ? "?" : "&") + "hmr=" + Date.now(), ${runtimeTemplate.basicFunction(
  460. "event",
  461. [
  462. 'if(event.type !== "load") {',
  463. Template.indent([
  464. "var errorType = event && event.type;",
  465. "var realHref = event && event.target && event.target.href;",
  466. "error.message = 'Loading css hot update chunk ' + chunkId + ' failed.\\n(' + errorType + ': ' + realHref + ')';",
  467. "error.name = 'ChunkLoadError';",
  468. "error.type = errorType;",
  469. "error.request = realHref;",
  470. "reject(error);"
  471. ]),
  472. "} else {",
  473. Template.indent([
  474. "try { if(cssTextKey(oldTag) == cssTextKey(link)) { if(link.parentNode) link.parentNode.removeChild(link); return resolve(); } } catch(e) {}",
  475. "link.sheet.disabled = true;",
  476. "oldTags.push(oldTag);",
  477. "newTags.push(link);",
  478. "resolve();"
  479. ]),
  480. "}"
  481. ]
  482. )}, ${withFetchPriority ? "undefined," : ""} oldTag);`
  483. ]
  484. )}));`
  485. ])});`
  486. ]
  487. )}`
  488. ])
  489. : "// no hmr"
  490. ]);
  491. }
  492. }
  493. module.exports = CssLoadingRuntimeModule;