LoadScriptRuntimeModule.js 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. */
  4. "use strict";
  5. const { SyncWaterfallHook } = require("tapable");
  6. const Compilation = require("../Compilation");
  7. const RuntimeGlobals = require("../RuntimeGlobals");
  8. const Template = require("../Template");
  9. const HelperRuntimeModule = require("./HelperRuntimeModule");
  10. /** @typedef {import("../Chunk")} Chunk */
  11. /**
  12. * @typedef {object} LoadScriptCompilationHooks
  13. * @property {SyncWaterfallHook<[string, Chunk]>} createScript
  14. */
  15. /** @type {WeakMap<Compilation, LoadScriptCompilationHooks>} */
  16. const compilationHooksMap = new WeakMap();
  17. class LoadScriptRuntimeModule extends HelperRuntimeModule {
  18. /**
  19. * @param {Compilation} compilation the compilation
  20. * @returns {LoadScriptCompilationHooks} hooks
  21. */
  22. static getCompilationHooks(compilation) {
  23. if (!(compilation instanceof Compilation)) {
  24. throw new TypeError(
  25. "The 'compilation' argument must be an instance of Compilation"
  26. );
  27. }
  28. let hooks = compilationHooksMap.get(compilation);
  29. if (hooks === undefined) {
  30. hooks = {
  31. createScript: new SyncWaterfallHook(["source", "chunk"])
  32. };
  33. compilationHooksMap.set(compilation, hooks);
  34. }
  35. return hooks;
  36. }
  37. /**
  38. * @param {boolean=} withCreateScriptUrl use create script url for trusted types
  39. * @param {boolean=} withFetchPriority use `fetchPriority` attribute
  40. */
  41. constructor(withCreateScriptUrl, withFetchPriority) {
  42. super("load script");
  43. /** @type {boolean | undefined} */
  44. this._withCreateScriptUrl = withCreateScriptUrl;
  45. /** @type {boolean | undefined} */
  46. this._withFetchPriority = withFetchPriority;
  47. }
  48. /**
  49. * Generates runtime code for this runtime module.
  50. * @returns {string | null} runtime code
  51. */
  52. generate() {
  53. const compilation = /** @type {Compilation} */ (this.compilation);
  54. const { runtimeTemplate, outputOptions } = compilation;
  55. const {
  56. scriptType,
  57. chunkLoadTimeout: loadTimeout,
  58. crossOriginLoading,
  59. uniqueName,
  60. charset
  61. } = outputOptions;
  62. const fn = RuntimeGlobals.loadScript;
  63. const { createScript } =
  64. LoadScriptRuntimeModule.getCompilationHooks(compilation);
  65. const code = Template.asString([
  66. "script = document.createElement('script');",
  67. scriptType ? `script.type = ${JSON.stringify(scriptType)};` : "",
  68. charset ? "script.charset = 'utf-8';" : "",
  69. `if (${RuntimeGlobals.scriptNonce}) {`,
  70. Template.indent(
  71. `script.setAttribute("nonce", ${RuntimeGlobals.scriptNonce});`
  72. ),
  73. "}",
  74. uniqueName
  75. ? 'script.setAttribute("data-webpack", dataWebpackPrefix + key);'
  76. : "",
  77. this._withFetchPriority
  78. ? Template.asString([
  79. "if(fetchPriority) {",
  80. Template.indent(
  81. 'script.setAttribute("fetchpriority", fetchPriority);'
  82. ),
  83. "}"
  84. ])
  85. : "",
  86. `script.src = ${
  87. this._withCreateScriptUrl
  88. ? `${RuntimeGlobals.createScriptUrl}(url)`
  89. : "url"
  90. };`,
  91. crossOriginLoading
  92. ? crossOriginLoading === "use-credentials"
  93. ? 'script.crossOrigin = "use-credentials";'
  94. : Template.asString([
  95. "if (script.src.indexOf(window.location.origin + '/') !== 0) {",
  96. Template.indent(
  97. `script.crossOrigin = ${JSON.stringify(crossOriginLoading)};`
  98. ),
  99. "}"
  100. ])
  101. : ""
  102. ]);
  103. return Template.asString([
  104. "var inProgress = {};",
  105. uniqueName
  106. ? `var dataWebpackPrefix = ${JSON.stringify(`${uniqueName}:`)};`
  107. : "// data-webpack is not used as build has no uniqueName",
  108. "// loadScript function to load a script via script tag",
  109. `${fn} = ${runtimeTemplate.basicFunction(
  110. `url, done, key, chunkId${
  111. this._withFetchPriority ? ", fetchPriority" : ""
  112. }`,
  113. [
  114. "if(inProgress[url]) { inProgress[url].push(done); return; }",
  115. "var script, needAttach;",
  116. "if(key !== undefined) {",
  117. Template.indent([
  118. 'var scripts = document.getElementsByTagName("script");',
  119. "for(var i = 0; i < scripts.length; i++) {",
  120. Template.indent([
  121. "var s = scripts[i];",
  122. `if(s.getAttribute("src") == url${
  123. uniqueName
  124. ? ' || s.getAttribute("data-webpack") == dataWebpackPrefix + key'
  125. : ""
  126. }) { script = s; break; }`
  127. ]),
  128. "}"
  129. ]),
  130. "}",
  131. "if(!script) {",
  132. Template.indent([
  133. "needAttach = true;",
  134. createScript.call(code, /** @type {Chunk} */ (this.chunk))
  135. ]),
  136. "}",
  137. "inProgress[url] = [done];",
  138. `var onScriptComplete = ${runtimeTemplate.basicFunction(
  139. "prev, event",
  140. Template.asString([
  141. "// avoid mem leaks in IE.",
  142. "script.onerror = script.onload = null;",
  143. "clearTimeout(timeout);",
  144. "var doneFns = inProgress[url];",
  145. "delete inProgress[url];",
  146. "script.parentNode && script.parentNode.removeChild(script);",
  147. `doneFns && doneFns.forEach(${runtimeTemplate.returningFunction(
  148. "fn(event)",
  149. "fn"
  150. )});`,
  151. "if(prev) return prev(event);"
  152. ])
  153. )}`,
  154. `var timeout = setTimeout(onScriptComplete.bind(null, undefined, { type: 'timeout', target: script }), ${loadTimeout});`,
  155. "script.onerror = onScriptComplete.bind(null, script.onerror);",
  156. "script.onload = onScriptComplete.bind(null, script.onload);",
  157. "needAttach && document.head.appendChild(script);"
  158. ]
  159. )};`
  160. ]);
  161. }
  162. }
  163. module.exports = LoadScriptRuntimeModule;