LoadScriptRuntimeModule.js 4.7 KB

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