webpack
Version:
Packs ECMAScript/CommonJs/AMD modules for the browser. Allows you to split your codebase into multiple bundles, which can be loaded on demand. Supports loaders to preprocess files, i.e. json, jsx, es7, css, less, ... and your custom stuff.
471 lines (431 loc) • 14.6 kB
JavaScript
/*
MIT License http://www.opensource.org/licenses/mit-license.php
Author Yuta Hiroto @hiroppy
*/
;
const { RawSource } = require("webpack-sources");
const HotUpdateChunk = require("../HotUpdateChunk");
const {
ASSET_MODULE_TYPE,
ASSET_MODULE_TYPE_BYTES,
ASSET_MODULE_TYPE_INLINE,
ASSET_MODULE_TYPE_RESOURCE,
ASSET_MODULE_TYPE_SOURCE,
ASSET_MODULE_TYPE_WEBMANIFEST
} = require("../ModuleTypeConstants");
const { compareModulesByFullName } = require("../util/comparators");
const createHash = require("../util/createHash");
const { getUndoPath } = require("../util/identifier");
const memoize = require("../util/memoize");
const { PUBLIC_PATH_AUTO } = require("../util/publicPathPlaceholder");
/** @typedef {import("webpack-sources").Source} Source */
/** @typedef {import("schema-utils").Schema} Schema */
/** @typedef {import("../../declarations/WebpackOptions").AssetGeneratorDataUrl} AssetGeneratorDataUrl */
/** @typedef {import("../../declarations/WebpackOptions").AssetModuleOutputPath} AssetModuleOutputPath */
/** @typedef {import("../../declarations/WebpackOptions").RawPublicPath} RawPublicPath */
/** @typedef {import("../../declarations/WebpackOptions").AssetModuleFilename} AssetModuleFilename */
/** @typedef {import("../Compilation").AssetInfo} AssetInfo */
/** @typedef {import("../Compiler")} Compiler */
/** @typedef {import("./AssetModule").AssetModuleBuildInfo} AssetModuleBuildInfo */
/** @typedef {import("../Module").CodeGenerationResult} CodeGenerationResult */
/** @typedef {import("../NormalModule")} NormalModule */
/**
* Returns definition.
* @param {string} name name of definitions
* @returns {Schema} definition
*/
const getSchema = (name) => {
const { definitions } =
/** @type {EXPECTED_ANY} */
(require("../../schemas/WebpackOptions.json"));
return {
definitions,
oneOf: [{ $ref: `#/definitions/${name}` }]
};
};
const generatorValidationOptions = {
name: "Asset Modules Plugin",
baseDataPath: "generator"
};
const getAssetGenerator = memoize(() => require("./AssetGenerator"));
const getAssetParser = memoize(() => require("./AssetParser"));
const getAssetSourceParser = memoize(() => require("./AssetSourceParser"));
const getAssetBytesParser = memoize(() => require("./AssetBytesParser"));
const getAssetSourceGenerator = memoize(() =>
require("./AssetSourceGenerator")
);
const getAssetBytesGenerator = memoize(() => require("./AssetBytesGenerator"));
const getAssetModule = memoize(() => require("./AssetModule"));
const getWebManifestParser = memoize(() => require("./WebManifestParser"));
const getWebManifestGenerator = memoize(() =>
require("./WebManifestGenerator")
);
const type = ASSET_MODULE_TYPE;
// Source type produced by `WebManifestGenerator` (its emitted, rewritten JSON).
const WEBMANIFEST_SOURCE_TYPE = "webmanifest";
const PLUGIN_NAME = "AssetModulesPlugin";
/**
* Represents the asset modules plugin runtime component.
* @typedef {object} AssetModulesPluginOptions
* @property {boolean=} sideEffectFree
*/
class AssetModulesPlugin {
/**
* Creates an instance of AssetModulesPlugin.
* @param {AssetModulesPluginOptions} options options
*/
constructor(options) {
/** @type {AssetModulesPluginOptions} */
this.options = options;
}
/**
* Applies the plugin by registering its hooks on the compiler.
* @param {Compiler} compiler the compiler instance
* @returns {void}
*/
apply(compiler) {
compiler.hooks.compilation.tap(
PLUGIN_NAME,
(compilation, { normalModuleFactory }) => {
const AssetModule = getAssetModule();
for (const type of [
ASSET_MODULE_TYPE,
ASSET_MODULE_TYPE_BYTES,
ASSET_MODULE_TYPE_INLINE,
ASSET_MODULE_TYPE_RESOURCE,
ASSET_MODULE_TYPE_SOURCE
]) {
normalModuleFactory.hooks.createModuleClass
.for(type)
.tap(
PLUGIN_NAME,
(createData, _resolveData) =>
new AssetModule(createData, this.options.sideEffectFree)
);
}
normalModuleFactory.hooks.createParser
.for(ASSET_MODULE_TYPE)
.tap(PLUGIN_NAME, (parserOptions) => {
compiler.validate(
() => getSchema("AssetParserOptions"),
parserOptions,
{
name: "Asset Modules Plugin",
baseDataPath: "parser"
},
(options) =>
require("../../schemas/plugins/asset/AssetParserOptions.check")(
options
)
);
let dataUrlCondition = parserOptions.dataUrlCondition;
if (!dataUrlCondition || typeof dataUrlCondition === "object") {
dataUrlCondition = {
maxSize: 8096,
...dataUrlCondition
};
}
const AssetParser = getAssetParser();
return new AssetParser(dataUrlCondition);
});
normalModuleFactory.hooks.createParser
.for(ASSET_MODULE_TYPE_INLINE)
.tap(PLUGIN_NAME, (_parserOptions) => {
const AssetParser = getAssetParser();
return new AssetParser(true);
});
normalModuleFactory.hooks.createParser
.for(ASSET_MODULE_TYPE_RESOURCE)
.tap(PLUGIN_NAME, (_parserOptions) => {
const AssetParser = getAssetParser();
return new AssetParser(false);
});
normalModuleFactory.hooks.createParser
.for(ASSET_MODULE_TYPE_SOURCE)
.tap(PLUGIN_NAME, (_parserOptions) => {
const AssetSourceParser = getAssetSourceParser();
return new AssetSourceParser();
});
normalModuleFactory.hooks.createParser
.for(ASSET_MODULE_TYPE_BYTES)
.tap(PLUGIN_NAME, (_parserOptions) => {
const AssetBytesParser = getAssetBytesParser();
return new AssetBytesParser();
});
for (const type of [
ASSET_MODULE_TYPE,
ASSET_MODULE_TYPE_INLINE,
ASSET_MODULE_TYPE_RESOURCE
]) {
normalModuleFactory.hooks.createGenerator
.for(type)
.tap(PLUGIN_NAME, (generatorOptions) => {
switch (type) {
case ASSET_MODULE_TYPE: {
compiler.validate(
() => getSchema("AssetGeneratorOptions"),
generatorOptions,
generatorValidationOptions,
(options) =>
require("../../schemas/plugins/asset/AssetGeneratorOptions.check")(
options
)
);
break;
}
case ASSET_MODULE_TYPE_RESOURCE: {
compiler.validate(
() => getSchema("AssetResourceGeneratorOptions"),
generatorOptions,
generatorValidationOptions,
(options) =>
require("../../schemas/plugins/asset/AssetResourceGeneratorOptions.check")(
options
)
);
break;
}
case ASSET_MODULE_TYPE_INLINE: {
compiler.validate(
() => getSchema("AssetInlineGeneratorOptions"),
generatorOptions,
generatorValidationOptions,
(options) =>
require("../../schemas/plugins/asset/AssetInlineGeneratorOptions.check")(
options
)
);
break;
}
}
/** @type {undefined | AssetGeneratorDataUrl} */
let dataUrl;
if (type !== ASSET_MODULE_TYPE_RESOURCE) {
dataUrl = generatorOptions.dataUrl;
if (!dataUrl || typeof dataUrl === "object") {
dataUrl = {
encoding: undefined,
mimetype: undefined,
...dataUrl
};
}
}
/** @type {undefined | AssetModuleFilename} */
let filename;
/** @type {undefined | RawPublicPath} */
let publicPath;
/** @type {undefined | AssetModuleOutputPath} */
let outputPath;
if (type !== ASSET_MODULE_TYPE_INLINE) {
filename = generatorOptions.filename;
publicPath = generatorOptions.publicPath;
outputPath = generatorOptions.outputPath;
}
const AssetGenerator = getAssetGenerator();
return new AssetGenerator(
compilation.moduleGraph,
dataUrl,
filename,
publicPath,
outputPath,
generatorOptions.emit !== false,
compilation
);
});
}
normalModuleFactory.hooks.createGenerator
.for(ASSET_MODULE_TYPE_SOURCE)
.tap(PLUGIN_NAME, () => {
const AssetSourceGenerator = getAssetSourceGenerator();
return new AssetSourceGenerator(compilation.moduleGraph);
});
normalModuleFactory.hooks.createGenerator
.for(ASSET_MODULE_TYPE_BYTES)
.tap(PLUGIN_NAME, () => {
const AssetBytesGenerator = getAssetBytesGenerator();
return new AssetBytesGenerator(compilation.moduleGraph);
});
compilation.hooks.renderManifest.tap(PLUGIN_NAME, (result, options) => {
const { chunkGraph } = compilation;
const { chunk, codeGenerationResults, runtimeTemplate } = options;
const modules = chunkGraph.getOrderedChunkModulesIterableBySourceType(
chunk,
ASSET_MODULE_TYPE,
compareModulesByFullName(compilation.compiler)
);
if (modules) {
for (const module of modules) {
try {
const codeGenResult = codeGenerationResults.get(
module,
chunk.runtime
);
const buildInfo = /** @type {AssetModuleBuildInfo} */ (
module.buildInfo
);
const data =
/** @type {NonNullable<CodeGenerationResult["data"]>} */
(codeGenResult.data);
const errored = module.getNumberOfErrors() > 0;
/** @type {string} */
let entryFilename;
/** @type {AssetInfo} */
let entryInfo;
/** @type {string} */
let entryHash;
if (errored) {
const erroredModule = /** @type {NormalModule} */ (module);
const AssetGenerator = getAssetGenerator();
const [fullContentHash, contentHash] =
AssetGenerator.getFullContentHash(
erroredModule,
runtimeTemplate
);
const { filename, assetInfo } =
AssetGenerator.getFilenameWithInfo(
erroredModule,
{
filename:
erroredModule.generatorOptions &&
erroredModule.generatorOptions.filename,
outputPath:
erroredModule.generatorOptions &&
erroredModule.generatorOptions.outputPath
},
{
runtime: chunk.runtime,
runtimeTemplate,
chunkGraph
},
contentHash,
fullContentHash
);
entryFilename = filename;
entryInfo = assetInfo;
entryHash = fullContentHash;
} else {
entryFilename =
/** @type {string} */
(buildInfo.filename || data.get("filename"));
entryInfo =
/** @type {AssetInfo} */
(buildInfo.assetInfo || data.get("assetInfo"));
entryHash =
/** @type {string} */
(buildInfo.fullContentHash || data.get("fullContentHash"));
}
result.push({
render: () =>
/** @type {Source} */ (codeGenResult.sources.get(type)),
filename: entryFilename,
info: entryInfo,
auxiliary: true,
identifier: `assetModule${chunkGraph.getModuleId(module)}`,
hash: entryHash
});
} catch (err) {
/** @type {Error} */ (err).message +=
`\nduring rendering of asset ${module.identifier()}`;
throw err;
}
}
}
return result;
});
// A Web App Manifest (`asset/webmanifest`, gated on `experiments.html`)
// parses/emits like an asset, but its icon URLs are rewritten in place
// and the emitted file needs its own per-file public-path resolution.
if (compilation.options.experiments.html) {
normalModuleFactory.hooks.createParser
.for(ASSET_MODULE_TYPE_WEBMANIFEST)
.tap(PLUGIN_NAME, () => {
const WebManifestParser = getWebManifestParser();
return new WebManifestParser();
});
normalModuleFactory.hooks.createGenerator
.for(ASSET_MODULE_TYPE_WEBMANIFEST)
.tap(PLUGIN_NAME, () => {
const WebManifestGenerator = getWebManifestGenerator();
return new WebManifestGenerator(compilation.moduleGraph);
});
compilation.hooks.renderManifest.tap(
PLUGIN_NAME,
(result, options) => {
const { chunk, codeGenerationResults } = options;
if (chunk instanceof HotUpdateChunk) return result;
const { chunkGraph, outputOptions } = compilation;
const modules =
chunkGraph.getOrderedChunkModulesIterableBySourceType(
chunk,
WEBMANIFEST_SOURCE_TYPE,
compareModulesByFullName(compilation.compiler)
);
if (!modules) return result;
for (const module of modules) {
const codeGenResult = codeGenerationResults.get(
module,
chunk.runtime
);
const source = codeGenResult.sources.get(
WEBMANIFEST_SOURCE_TYPE
);
if (!source) continue;
const filename = /** @type {string} */ (
codeGenResult.data && codeGenResult.data.get("filename")
);
if (!filename) continue;
// Icon URLs resolve relative to the manifest's own location.
const undoPath = getUndoPath(
filename,
/** @type {string} */ (outputOptions.path),
false
);
const content = source
.source()
.toString()
.split(PUBLIC_PATH_AUTO)
.join(undoPath);
const hash = createHash(outputOptions.hashFunction || "md4");
hash.update(content);
const contentHash = /** @type {string} */ (
hash.digest(outputOptions.hashDigest || "hex")
).slice(0, outputOptions.hashDigestLength || 20);
result.push({
render: () => new RawSource(content),
filename,
info: { immutable: true },
auxiliary: true,
identifier: `webManifestModule${chunkGraph.getModuleId(
module
)}|${filename}`,
hash: contentHash
});
}
return result;
}
);
}
compilation.hooks.prepareModuleExecution.tap(
PLUGIN_NAME,
(options, context) => {
const { codeGenerationResult } = options;
const source = codeGenerationResult.sources.get(ASSET_MODULE_TYPE);
if (source === undefined) return;
const data =
/** @type {NonNullable<CodeGenerationResult["data"]>} */
(codeGenerationResult.data);
context.assets.set(
/** @type {string} */
(data.get("filename")),
{
source,
info: data.get("assetInfo")
}
);
}
);
}
);
}
}
module.exports = AssetModulesPlugin;