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.
344 lines (325 loc) • 10.2 kB
JavaScript
/*
MIT License http://www.opensource.org/licenses/mit-license.php
Author Tobias Koppers @sokra
*/
;
const { CachedSource, ConcatSource, RawSource } = require("webpack-sources");
const CssModulesPlugin = require("../css/CssModulesPlugin");
const { UsageState } = require("../graph/ExportsInfo");
const JavascriptModulesPlugin = require("../javascript/JavascriptModulesPlugin");
const Template = require("./Template");
/** @import { Source } from "webpack-sources" */
/** @import Compiler from "../Compiler" */
/** @import ExportsInfo, { ExportInfo } from "../graph/ExportsInfo" */
/** @import Module, { BuildMeta } from "../module/Module" */
/** @import ModuleGraph from "../graph/ModuleGraph" */
/** @import RequestShortener from "../util/RequestShortener" */
/**
* Join iterable with comma.
* @template T
* @param {Iterable<T>} iterable iterable
* @returns {string} joined with comma
*/
const joinIterableWithComma = (iterable) => {
// This is more performant than Array.from().join(", ")
// as it doesn't create an array
let str = "";
let first = true;
for (const item of iterable) {
if (first) {
first = false;
} else {
str += ", ";
}
str += item;
}
return str;
};
/**
* Print exports info to source.
* @param {ConcatSource} source output
* @param {string} indent spacing
* @param {ExportsInfo} exportsInfo data
* @param {ModuleGraph} moduleGraph moduleGraph
* @param {RequestShortener} requestShortener requestShortener
* @param {Set<InstanceType<ExportInfo>>} alreadyPrinted deduplication set
* @returns {void}
*/
const printExportsInfoToSource = (
source,
indent,
exportsInfo,
moduleGraph,
requestShortener,
alreadyPrinted = new Set()
) => {
const otherExportsInfo = exportsInfo.otherExportsInfo;
let alreadyPrintedExports = 0;
// determine exports to print
/** @type {InstanceType<ExportInfo>[]} */
const printedExports = [];
for (const exportInfo of exportsInfo.orderedExports) {
if (!alreadyPrinted.has(exportInfo)) {
alreadyPrinted.add(exportInfo);
printedExports.push(exportInfo);
} else {
alreadyPrintedExports++;
}
}
let showOtherExports = false;
if (!alreadyPrinted.has(otherExportsInfo)) {
alreadyPrinted.add(otherExportsInfo);
showOtherExports = true;
} else {
alreadyPrintedExports++;
}
// print the exports
for (const exportInfo of printedExports) {
const target = exportInfo.getTarget(moduleGraph);
source.add(
`${Template.toComment(
`${indent}export ${JSON.stringify(exportInfo.name).slice(
1,
-1
)} [${exportInfo.getProvidedInfo()}] [${exportInfo.getUsedInfo()}] [${exportInfo.getRenameInfo()}]${
target
? ` -> ${target.module.readableIdentifier(requestShortener)}${
target.export
? ` .${target.export
.map((e) => JSON.stringify(e).slice(1, -1))
.join(".")}`
: ""
}`
: ""
}`
)}\n`
);
if (exportInfo.exportsInfo) {
printExportsInfoToSource(
source,
`${indent} `,
exportInfo.exportsInfo,
moduleGraph,
requestShortener,
alreadyPrinted
);
}
}
if (alreadyPrintedExports) {
source.add(
`${Template.toComment(
`${indent}... (${alreadyPrintedExports} already listed exports)`
)}\n`
);
}
if (showOtherExports) {
const target = otherExportsInfo.getTarget(moduleGraph);
if (
target ||
otherExportsInfo.provided !== false ||
otherExportsInfo.getUsed(undefined) !== UsageState.Unused
) {
const title =
printedExports.length > 0 || alreadyPrintedExports > 0
? "other exports"
: "exports";
source.add(
`${Template.toComment(
`${indent}${title} [${otherExportsInfo.getProvidedInfo()}] [${otherExportsInfo.getUsedInfo()}]${
target
? ` -> ${target.module.readableIdentifier(requestShortener)}`
: ""
}`
)}\n`
);
}
}
};
// `generateHeader`'s shape, as it reads back. A header names the module of the
// build that wrote it, so a source webpack reads its own output from sheds the
// one it arrives with and is headed again — see `stripHeaders`.
const HEADER_REGEXP =
/^\/\*!\*+!\*\\\r?\n {2}!\*\*\* .* \*\*\*!\r?\n {2}\\\*+\/\r?\n/;
/** @typedef {{ header: RawSource | undefined, full: WeakMap<Source, CachedSource> }} CacheEntry */
/** @type {WeakMap<RequestShortener, WeakMap<Module, CacheEntry>>} */
const caches = new WeakMap();
const PLUGIN_NAME = "ModuleInfoHeaderPlugin";
class ModuleInfoHeaderPlugin {
/**
* Creates an instance of ModuleInfoHeaderPlugin.
* @param {boolean=} verbose add more information like exports, runtime requirements and bailouts
*/
constructor(verbose = true) {
/** @type {boolean} */
this._verbose = verbose;
}
/**
* Applies the plugin by registering its hooks on the compiler.
* @param {Compiler} compiler the compiler
* @returns {void}
*/
apply(compiler) {
const { _verbose: verbose } = this;
compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => {
const javascriptHooks =
JavascriptModulesPlugin.getCompilationHooks(compilation);
javascriptHooks.renderModulePackage.tap(
PLUGIN_NAME,
(
moduleSource,
module,
{ chunk, chunkGraph, moduleGraph, runtimeTemplate }
) => {
const { requestShortener } = runtimeTemplate;
/** @type {undefined | CacheEntry} */
let cacheEntry;
let cache = caches.get(requestShortener);
if (cache === undefined) {
caches.set(requestShortener, (cache = new WeakMap()));
cache.set(
module,
(cacheEntry = { header: undefined, full: new WeakMap() })
);
} else {
cacheEntry = cache.get(module);
if (cacheEntry === undefined) {
cache.set(
module,
(cacheEntry = { header: undefined, full: new WeakMap() })
);
} else if (!verbose) {
const cachedSource = cacheEntry.full.get(moduleSource);
if (cachedSource !== undefined) return cachedSource;
}
}
const source = new ConcatSource();
let header = cacheEntry.header;
if (header === undefined) {
header = this.generateHeader(module, requestShortener);
cacheEntry.header = header;
}
source.add(header);
if (verbose) {
const exportsType = /** @type {BuildMeta} */ (module.buildMeta)
.exportsType;
source.add(
`${Template.toComment(
exportsType
? `${exportsType} exports`
: "unknown exports (runtime-defined)"
)}\n`
);
if (exportsType) {
const exportsInfo = moduleGraph.getExportsInfo(module);
printExportsInfoToSource(
source,
"",
exportsInfo,
moduleGraph,
requestShortener
);
}
source.add(
`${Template.toComment(
`runtime requirements: ${joinIterableWithComma(
chunkGraph.getModuleRuntimeRequirements(module, chunk.runtime)
)}`
)}\n`
);
const optimizationBailout =
moduleGraph.getOptimizationBailout(module);
if (optimizationBailout) {
for (const text of optimizationBailout) {
const code =
typeof text === "function" ? text(requestShortener) : text;
source.add(`${Template.toComment(`${code}`)}\n`);
}
}
source.add(moduleSource);
return source;
}
source.add(moduleSource);
const cachedSource = new CachedSource(source);
cacheEntry.full.set(moduleSource, cachedSource);
return cachedSource;
}
);
javascriptHooks.chunkHash.tap(PLUGIN_NAME, (_chunk, hash) => {
hash.update(PLUGIN_NAME);
hash.update("1");
});
const cssHooks = CssModulesPlugin.getCompilationHooks(compilation);
cssHooks.renderModulePackage.tap(
PLUGIN_NAME,
(moduleSource, module, { runtimeTemplate }) => {
const { requestShortener } = runtimeTemplate;
/** @type {undefined | CacheEntry} */
let cacheEntry;
let cache = caches.get(requestShortener);
if (cache === undefined) {
caches.set(requestShortener, (cache = new WeakMap()));
cache.set(
module,
(cacheEntry = { header: undefined, full: new WeakMap() })
);
} else {
cacheEntry = cache.get(module);
if (cacheEntry === undefined) {
cache.set(
module,
(cacheEntry = { header: undefined, full: new WeakMap() })
);
} else if (!verbose) {
const cachedSource = cacheEntry.full.get(moduleSource);
if (cachedSource !== undefined) return cachedSource;
}
}
const source = new ConcatSource();
let header = cacheEntry.header;
if (header === undefined) {
header = this.generateHeader(module, requestShortener);
cacheEntry.header = header;
}
source.add(header);
source.add(moduleSource);
const cachedSource = new CachedSource(source);
cacheEntry.full.set(moduleSource, cachedSource);
return cachedSource;
}
);
cssHooks.chunkHash.tap(PLUGIN_NAME, (_chunk, hash) => {
hash.update(PLUGIN_NAME);
hash.update("1");
});
});
}
/**
* Drop the headers a build wrote at the top of a source that is read back
* in, so this build's own header heads the module rather than the last
* build's.
* @param {string} source source that may have been written by a build
* @returns {string} the source with any header of ours taken off the front
*/
static stripHeaders(source) {
let stripped = source;
// A page built before this stripped may carry a stack of them.
while (HEADER_REGEXP.test(stripped)) {
stripped = stripped.replace(HEADER_REGEXP, "");
}
return stripped;
}
/**
* Returns the header.
* @param {Module} module the module
* @param {RequestShortener} requestShortener request shortener
* @returns {RawSource} the header
*/
generateHeader(module, requestShortener) {
const req = module.readableIdentifier(requestShortener);
const reqStr = req.replace(/\*\//g, "*_/");
const reqStrStar = "*".repeat(reqStr.length);
const headerStr = `/*!****${reqStrStar}****!*\\\n !*** ${reqStr} ***!\n \\****${reqStrStar}****/\n`;
return new RawSource(headerStr);
}
}
module.exports = ModuleInfoHeaderPlugin;