/* MIT License http://www.opensource.org/licenses/mit-license.php */ "use strict"; const { SyncWaterfallHook } = require("tapable"); const Compilation = require("../Compilation"); const RuntimeGlobals = require("../RuntimeGlobals"); const Template = require("../Template"); const HelperRuntimeModule = require("./HelperRuntimeModule"); /** @typedef {import("../Chunk")} Chunk */ /** @typedef {import("../Compiler")} Compiler */ /** * @typedef {Object} LoadScriptCompilationHooks * @property {SyncWaterfallHook<[string, Chunk]>} createScript */ /** @type {WeakMap} */ const compilationHooksMap = new WeakMap(); class LoadScriptRuntimeModule extends HelperRuntimeModule { /** * @param {Compilation} compilation the compilation * @returns {LoadScriptCompilationHooks} hooks */ static getCompilationHooks(compilation) { if (!(compilation instanceof Compilation)) { throw new TypeError( "The 'compilation' argument must be an instance of Compilation" ); } let hooks = compilationHooksMap.get(compilation); if (hooks === undefined) { hooks = { createScript: new SyncWaterfallHook(["source", "chunk"]) }; compilationHooksMap.set(compilation, hooks); } return hooks; } constructor() { super("load script"); } /** * @returns {string} runtime code */ generate() { const { compilation } = this; const { runtimeTemplate, outputOptions } = compilation; const { scriptType, chunkLoadTimeout: loadTimeout, crossOriginLoading, uniqueName, charset } = outputOptions; const fn = RuntimeGlobals.loadScript; const { createScript } = LoadScriptRuntimeModule.getCompilationHooks( compilation ); const code = Template.asString([ "script = document.createElement('script');", scriptType ? `script.type = ${JSON.stringify(scriptType)};` : "", charset ? "script.charset = 'utf-8';" : "", `script.timeout = ${loadTimeout / 1000};`, `if (${RuntimeGlobals.scriptNonce}) {`, Template.indent( `script.setAttribute("nonce", ${RuntimeGlobals.scriptNonce});` ), "}", uniqueName ? 'script.setAttribute("data-webpack", dataWebpackPrefix + key);' : "", `script.src = url;`, crossOriginLoading ? Template.asString([ "if (script.src.indexOf(window.location.origin + '/') !== 0) {", Template.indent( `script.crossOrigin = ${JSON.stringify(crossOriginLoading)};` ), "}" ]) : "" ]); return Template.asString([ "var inProgress = {};", uniqueName ? `var dataWebpackPrefix = ${JSON.stringify(uniqueName + ":")};` : "// data-webpack is not used as build has no uniqueName", "// loadScript function to load a script via script tag", `${fn} = ${runtimeTemplate.basicFunction("url, done, key, chunkId", [ "if(inProgress[url]) { inProgress[url].push(done); return; }", "var script, needAttach;", "if(key !== undefined) {", Template.indent([ 'var scripts = document.getElementsByTagName("script");', "for(var i = 0; i < scripts.length; i++) {", Template.indent([ "var s = scripts[i];", `if(s.getAttribute("src") == url${ uniqueName ? ' || s.getAttribute("data-webpack") == dataWebpackPrefix + key' : "" }) { script = s; break; }` ]), "}" ]), "}", "if(!script) {", Template.indent([ "needAttach = true;", createScript.call(code, this.chunk) ]), "}", "inProgress[url] = [done];", "var onScriptComplete = " + runtimeTemplate.basicFunction( "prev, event", Template.asString([ "// avoid mem leaks in IE.", "script.onerror = script.onload = null;", "clearTimeout(timeout);", "var doneFns = inProgress[url];", "delete inProgress[url];", "script.parentNode && script.parentNode.removeChild(script);", `doneFns && doneFns.forEach(${runtimeTemplate.returningFunction( "fn(event)", "fn" )});`, "if(prev) return prev(event);" ]) ), ";", `var timeout = setTimeout(onScriptComplete.bind(null, undefined, { type: 'timeout', target: script }), ${loadTimeout});`, "script.onerror = onScriptComplete.bind(null, script.onerror);", "script.onload = onScriptComplete.bind(null, script.onload);", "needAttach && document.head.appendChild(script);" ])};` ]); } } module.exports = LoadScriptRuntimeModule;