CssLoadingRuntimeModule.js 17 KB

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