webpack/lib/web/JsonpTemplatePlugin.js

329 lines
11 KiB
JavaScript

/*
MIT License http://www.opensource.org/licenses/mit-license.php
Author Tobias Koppers @sokra
*/
"use strict";
const { SyncWaterfallHook } = require("tapable");
const { ConcatSource } = require("webpack-sources");
const Compilation = require("../Compilation");
const HotUpdateChunk = require("../HotUpdateChunk");
const RuntimeGlobals = require("../RuntimeGlobals");
const Template = require("../Template");
const JavascriptModulesPlugin = require("../javascript/JavascriptModulesPlugin");
const JsonpChunkLoadingRuntimeModule = require("./JsonpChunkLoadingRuntimeModule");
const getEntryInfo = require("./JsonpHelpers").getEntryInfo;
/** @typedef {import("../Chunk")} Chunk */
/** @typedef {import("../Compilation")} Compilation */
/** @typedef {import("../Compiler")} Compiler */
/**
* @typedef {Object} JsonpCompilationPluginHooks
* @property {SyncWaterfallHook<[string, Chunk, string]>} jsonpScript
* @property {SyncWaterfallHook<[string, Chunk, string]>} linkPreload
* @property {SyncWaterfallHook<[string, Chunk, string]>} linkPrefetch
*/
/** @type {WeakMap<Compilation, JsonpCompilationPluginHooks>} */
const compilationHooksMap = new WeakMap();
class JsonpTemplatePlugin {
/**
* @param {Compilation} compilation the compilation
* @returns {JsonpCompilationPluginHooks} 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 = {
jsonpScript: new SyncWaterfallHook(["source", "chunk", "hash"]),
linkPreload: new SyncWaterfallHook(["source", "chunk", "hash"]),
linkPrefetch: new SyncWaterfallHook(["source", "chunk", "hash"])
};
compilationHooksMap.set(compilation, hooks);
}
return hooks;
}
/**
* @param {Compiler} compiler the compiler instance
* @returns {void}
*/
apply(compiler) {
compiler.hooks.thisCompilation.tap("JsonpTemplatePlugin", compilation => {
const hooks = JavascriptModulesPlugin.getCompilationHooks(compilation);
hooks.renderChunk.tap("JsonpTemplatePlugin", (modules, renderContext) => {
const { chunk, chunkGraph, runtimeTemplate } = renderContext;
const hotUpdateChunk = chunk instanceof HotUpdateChunk ? chunk : null;
const globalObject = runtimeTemplate.outputOptions.globalObject;
const source = new ConcatSource();
const runtimeModules = chunkGraph.getChunkRuntimeModulesInOrder(chunk);
const runtimePart =
runtimeModules.length > 0 &&
Template.renderChunkRuntimeModules(runtimeModules, renderContext);
if (hotUpdateChunk) {
const jsonpFunction = runtimeTemplate.outputOptions.hotUpdateFunction;
source.add(`${globalObject}[${JSON.stringify(jsonpFunction)}](`);
source.add(`${JSON.stringify(chunk.id)},`);
source.add(modules);
if (runtimePart) {
source.add(",\n");
source.add(runtimePart);
}
source.add(")");
} else {
const jsonpFunction = runtimeTemplate.outputOptions.jsonpFunction;
source.add(
`(${globalObject}[${JSON.stringify(
jsonpFunction
)}] = ${globalObject}[${JSON.stringify(
jsonpFunction
)}] || []).push([`
);
source.add(`${JSON.stringify(chunk.ids)},`);
source.add(modules);
const entries = getEntryInfo(chunkGraph, chunk);
const prefetchChunks = chunk.getChildIdsByOrders(chunkGraph).prefetch;
const entriesPart =
entries.length > 0 && `,${JSON.stringify(entries)}`;
const prefetchPart =
prefetchChunks &&
prefetchChunks.length > 0 &&
`,${JSON.stringify(prefetchChunks)}`;
if (entriesPart || runtimePart || prefetchPart) {
source.add(entriesPart || ",0");
}
if (runtimePart || prefetchPart) {
source.add(",\n");
source.add(runtimePart || "0");
}
if (prefetchPart) {
source.add(prefetchPart);
}
source.add("])");
}
return source;
});
hooks.chunkHash.tap(
"JsonpTemplatePlugin",
(chunk, hash, { chunkGraph, runtimeTemplate }) => {
if (chunk.hasRuntime()) return;
hash.update("JsonpTemplatePlugin");
hash.update("1");
hash.update(JSON.stringify(getEntryInfo(chunkGraph, chunk)));
hash.update(
JSON.stringify(chunk.getChildIdsByOrders(chunkGraph).prefetch) || ""
);
hash.update(`${runtimeTemplate.outputOptions.jsonpFunction}`);
hash.update(`${runtimeTemplate.outputOptions.hotUpdateFunction}`);
hash.update(`${runtimeTemplate.outputOptions.globalObject}`);
}
);
const needEntryDeferringCode = chunk => {
for (const chunkGroup of chunk.groupsIterable) {
if (chunkGroup.chunks.length > 1) return true;
}
return false;
};
const needPrefetchingCode = chunk => {
const allPrefetchChunks = chunk.getChildIdsByOrdersMap(
compilation.chunkGraph,
true
).prefetch;
return allPrefetchChunks && Object.keys(allPrefetchChunks).length;
};
const {
jsonpScript,
linkPreload,
linkPrefetch
} = JsonpTemplatePlugin.getCompilationHooks(compilation);
const { runtimeTemplate } = compilation;
jsonpScript.tap("JsonpTemplatePlugin", (_, chunk, hash) => {
const {
crossOriginLoading,
chunkLoadTimeout,
jsonpScriptType
} = compilation.outputOptions;
return Template.asString([
"var script = document.createElement('script');",
"var onScriptComplete;",
jsonpScriptType
? `script.type = ${JSON.stringify(jsonpScriptType)};`
: "",
"script.charset = 'utf-8';",
`script.timeout = ${chunkLoadTimeout / 1000};`,
`if (${RuntimeGlobals.scriptNonce}) {`,
Template.indent(
`script.setAttribute("nonce", ${RuntimeGlobals.scriptNonce});`
),
"}",
`script.src = url;`,
crossOriginLoading
? Template.asString([
"if (script.src.indexOf(window.location.origin + '/') !== 0) {",
Template.indent(
`script.crossOrigin = ${JSON.stringify(crossOriginLoading)};`
),
"}"
])
: "",
"// create error before stack unwound to get useful stacktrace later",
"var error = new Error();",
"onScriptComplete = " +
runtimeTemplate.basicFunction(
"event",
Template.asString([
`onScriptComplete = ${runtimeTemplate.basicFunction("", "")}`,
"// avoid mem leaks in IE.",
"script.onerror = script.onload = null;",
"clearTimeout(timeout);",
"var reportError = loadingEnded();",
"if(reportError) {",
Template.indent([
"var errorType = event && (event.type === 'load' ? 'missing' : event.type);",
"var realSrc = event && event.target && event.target.src;",
"error.message = 'Loading chunk ' + chunkId + ' failed.\\n(' + errorType + ': ' + realSrc + ')';",
"error.name = 'ChunkLoadError';",
"error.type = errorType;",
"error.request = realSrc;",
"reportError(error);"
]),
"}"
])
),
";",
`var timeout = setTimeout(${runtimeTemplate.basicFunction(
"",
"onScriptComplete({ type: 'timeout', target: script })"
)}, ${chunkLoadTimeout});`,
"script.onerror = script.onload = onScriptComplete;"
]);
});
linkPreload.tap("JsonpTemplatePlugin", (_, chunk, hash) => {
const {
crossOriginLoading,
jsonpScriptType
} = compilation.outputOptions;
return Template.asString([
"var link = document.createElement('link');",
jsonpScriptType
? `link.type = ${JSON.stringify(jsonpScriptType)};`
: "",
"link.charset = 'utf-8';",
`if (${RuntimeGlobals.scriptNonce}) {`,
Template.indent(
`link.setAttribute("nonce", ${RuntimeGlobals.scriptNonce});`
),
"}",
'link.rel = "preload";',
'link.as = "script";',
`link.href = ${RuntimeGlobals.publicPath} + ${RuntimeGlobals.getChunkScriptFilename}(chunkId);`,
crossOriginLoading
? Template.asString([
"if (link.href.indexOf(window.location.origin + '/') !== 0) {",
Template.indent(
`link.crossOrigin = ${JSON.stringify(crossOriginLoading)};`
),
"}"
])
: ""
]);
});
linkPrefetch.tap("JsonpTemplatePlugin", (_, chunk, hash) => {
const { crossOriginLoading } = compilation.outputOptions;
return Template.asString([
"var link = document.createElement('link');",
crossOriginLoading
? `link.crossOrigin = ${JSON.stringify(crossOriginLoading)};`
: "",
`if (${RuntimeGlobals.scriptNonce}) {`,
Template.indent(
`link.setAttribute("nonce", ${RuntimeGlobals.scriptNonce});`
),
"}",
'link.rel = "prefetch";',
'link.as = "script";',
`link.href = ${RuntimeGlobals.publicPath} + ${RuntimeGlobals.getChunkScriptFilename}(chunkId);`
]);
});
const onceForChunkSet = new WeakSet();
const handler = (chunk, set) => {
if (onceForChunkSet.has(chunk)) return;
onceForChunkSet.add(chunk);
set.add(RuntimeGlobals.moduleFactoriesAddOnly);
set.add(RuntimeGlobals.hasOwnProperty);
compilation.addRuntimeModule(
chunk,
new JsonpChunkLoadingRuntimeModule(
set,
jsonpScript,
linkPreload,
linkPrefetch
)
);
};
compilation.hooks.runtimeRequirementInTree
.for(RuntimeGlobals.ensureChunkHandlers)
.tap("JsonpTemplatePlugin", handler);
compilation.hooks.runtimeRequirementInTree
.for(RuntimeGlobals.hmrDownloadUpdateHandlers)
.tap("JsonpTemplatePlugin", handler);
compilation.hooks.runtimeRequirementInTree
.for(RuntimeGlobals.hmrDownloadManifest)
.tap("JsonpTemplatePlugin", handler);
compilation.hooks.runtimeRequirementInTree
.for(RuntimeGlobals.ensureChunkHandlers)
.tap("JsonpTemplatePlugin", (chunk, set) => {
set.add(RuntimeGlobals.publicPath);
set.add(RuntimeGlobals.getChunkScriptFilename);
});
compilation.hooks.runtimeRequirementInTree
.for(RuntimeGlobals.hmrDownloadUpdateHandlers)
.tap("JsonpTemplatePlugin", (chunk, set) => {
set.add(RuntimeGlobals.publicPath);
set.add(RuntimeGlobals.getChunkUpdateScriptFilename);
set.add(RuntimeGlobals.moduleCache);
set.add(RuntimeGlobals.hmrModuleData);
set.add(RuntimeGlobals.moduleFactoriesAddOnly);
});
compilation.hooks.runtimeRequirementInTree
.for(RuntimeGlobals.hmrDownloadManifest)
.tap("JsonpTemplatePlugin", (chunk, set) => {
set.add(RuntimeGlobals.publicPath);
set.add(RuntimeGlobals.getUpdateManifestFilename);
});
compilation.hooks.additionalTreeRuntimeRequirements.tap(
"JsonpTemplatePlugin",
(chunk, set) => {
const withDefer = needEntryDeferringCode(chunk);
if (withDefer || needPrefetchingCode(chunk)) {
set.add(RuntimeGlobals.startup);
if (withDefer) set.add(RuntimeGlobals.startupNoDefault);
handler(chunk, set);
}
if (withDefer) {
set.add(RuntimeGlobals.require);
}
}
);
});
}
}
module.exports = JsonpTemplatePlugin;