mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-27 19:00:56 +00:00
929 lines
34 KiB
JavaScript
929 lines
34 KiB
JavaScript
/*
|
||
MIT License http://www.opensource.org/licenses/mit-license.php
|
||
Author Tobias Koppers @sokra
|
||
*/
|
||
|
||
"use strict";
|
||
|
||
const asyncLib = require("neo-async");
|
||
const { ConcatSource, RawSource } = require("webpack-sources");
|
||
const Compilation = require("./Compilation");
|
||
const ModuleFilenameHelpers = require("./ModuleFilenameHelpers");
|
||
const ProgressPlugin = require("./ProgressPlugin");
|
||
const SourceMapDevToolModuleOptionsPlugin = require("./SourceMapDevToolModuleOptionsPlugin");
|
||
const createHash = require("./util/createHash");
|
||
const { dirname, relative } = require("./util/fs");
|
||
const generateDebugId = require("./util/generateDebugId");
|
||
const { makePathsAbsolute } = require("./util/identifier");
|
||
|
||
/** @typedef {import("webpack-sources").MapOptions} MapOptions */
|
||
/** @typedef {import("webpack-sources").Source} Source */
|
||
/** @typedef {import("../declarations/WebpackOptions").DevtoolNamespace} DevtoolNamespace */
|
||
/** @typedef {import("../declarations/WebpackOptions").DevtoolModuleFilenameTemplate} DevtoolModuleFilenameTemplate */
|
||
/** @typedef {import("../declarations/WebpackOptions").DevtoolFallbackModuleFilenameTemplate} DevtoolFallbackModuleFilenameTemplate */
|
||
/** @typedef {import("../declarations/plugins/SourceMapDevToolPlugin").SourceMapDevToolPluginOptions} SourceMapDevToolPluginOptions */
|
||
/** @typedef {import("../declarations/plugins/SourceMapDevToolPlugin").Rules} Rules */
|
||
/** @typedef {import("./CacheFacade").ItemCacheFacade} ItemCacheFacade */
|
||
/** @typedef {import("./Chunk")} Chunk */
|
||
/** @typedef {import("./Compilation").Asset} Asset */
|
||
/** @typedef {import("./Compilation").AssetInfo} AssetInfo */
|
||
/** @typedef {import("./Compiler")} Compiler */
|
||
/** @typedef {import("./Module")} Module */
|
||
/** @typedef {import("./NormalModule").RawSourceMap} RawSourceMap */
|
||
/** @typedef {import("./TemplatedPathPlugin").TemplatePath} SourceMappingURLComment */
|
||
/** @typedef {import("./util/fs").OutputFileSystem} OutputFileSystem */
|
||
|
||
/**
|
||
* Defines the source map task type used by this module.
|
||
* @typedef {object} SourceMapTask
|
||
* @property {AssetInfo} assetInfo
|
||
* @property {(string | Module)[]} modules
|
||
* @property {string} source
|
||
* @property {string} file
|
||
* @property {RawSourceMap} sourceMap
|
||
* @property {Source} mapSource the Source instance whose `sourceAndMap` we called (the current asset or, when its map was already stripped, the pinned original from `originalSources`) — what `clearCache` should target
|
||
* @property {ItemCacheFacade} cacheItem cache item
|
||
*/
|
||
|
||
const METACHARACTERS_REGEXP = /[-[\]\\/{}()*+?.^$|]/g;
|
||
const CONTENT_HASH_DETECT_REGEXP = /\[contenthash(?::\w+)?\]/;
|
||
const CSS_AND_JS_MODULE_EXTENSIONS_REGEXP = /\.((c|m)?js|css)($|\?)/i;
|
||
const CSS_EXTENSION_DETECT_REGEXP = /\.css(?:$|\?)/i;
|
||
const MAP_URL_COMMENT_REGEXP = /\[map\]/g;
|
||
const URL_COMMENT_REGEXP = /\[url\]/g;
|
||
const URL_FORMATTING_REGEXP = /^\n\/\/(.*)$/;
|
||
|
||
/**
|
||
* Reset's .lastIndex of stateful Regular Expressions
|
||
* For when `test` or `exec` is called on them
|
||
* @param {RegExp} regexp Stateful Regular Expression to be reset
|
||
* @returns {void}
|
||
*/
|
||
const resetRegexpState = (regexp) => {
|
||
regexp.lastIndex = -1;
|
||
};
|
||
|
||
/**
|
||
* Escapes regular expression metacharacters
|
||
* @param {string} str String to quote
|
||
* @returns {string} Escaped string
|
||
*/
|
||
const quoteMeta = (str) => str.replace(METACHARACTERS_REGEXP, "\\$&");
|
||
|
||
/**
|
||
* Compilation-scoped registry of original asset sources for multi-plugin
|
||
* cooperation. The first SourceMapDevToolPlugin instance to see a file pins a
|
||
* reference to the asset's still-unwrapped {@link Source} object; later
|
||
* instances whose `asset.source.sourceAndMap()` would now return `null` (the
|
||
* earlier instance replaced the asset with a `RawSource`) can re-extract the
|
||
* map from this pinned reference. We keep the registry on a module-scoped
|
||
* `WeakMap` so the entries are reclaimed automatically when the compilation
|
||
* itself becomes unreachable; we never store anything on the compilation
|
||
* object directly.
|
||
*
|
||
* Stashing the `Source` object itself rather than an extracted map keeps the
|
||
* fast path free of cloning and source-map serialization work — the
|
||
* extraction only happens if a subsequent plugin actually needs the map.
|
||
* @type {WeakMap<Compilation, Map<string, Source>>}
|
||
*/
|
||
const originalSourceRegistry = new WeakMap();
|
||
|
||
/**
|
||
* Returns (creating if necessary) the per-compilation registry of original
|
||
* asset {@link Source} objects.
|
||
* @param {Compilation} compilation compilation
|
||
* @returns {Map<string, Source>} registry
|
||
*/
|
||
const getOriginalSourceRegistry = (compilation) => {
|
||
let registry = originalSourceRegistry.get(compilation);
|
||
if (registry === undefined) {
|
||
registry = new Map();
|
||
originalSourceRegistry.set(compilation, registry);
|
||
}
|
||
return registry;
|
||
};
|
||
|
||
/**
|
||
* Extracts source and source map from a Source object, falling back to a
|
||
* registered original source for assets that another SourceMapDevToolPlugin
|
||
* instance has already wrapped (whose internal map is now `null`).
|
||
*
|
||
* The returned source is read from the asset as it currently stands — that way
|
||
* any `sourceMappingURL` comments appended by earlier plugin instances survive
|
||
* — while the map is taken from the pinned original Source when the current
|
||
* one no longer carries it. `mapSource` identifies which Source instance was
|
||
* actually queried for the map (the current asset, or the pinned original);
|
||
* that's the one whose internal caches the caller should release.
|
||
* @param {string} file file name
|
||
* @param {Source} asset source object as currently held by the compilation
|
||
* @param {MapOptions} options map extraction options
|
||
* @param {Map<string, Source>} registry compilation-scoped original-source registry
|
||
* @returns {{ source: string, sourceMap: RawSourceMap, mapSource: Source } | undefined} extracted pair or `undefined` when no map is recoverable
|
||
*/
|
||
const extractSourceAndMap = (file, asset, options, registry) => {
|
||
/** @type {string | Buffer} */
|
||
let source;
|
||
/** @type {null | RawSourceMap} */
|
||
let sourceMap;
|
||
if (asset.sourceAndMap) {
|
||
const sourceAndMap = asset.sourceAndMap(options);
|
||
source = sourceAndMap.source;
|
||
sourceMap = sourceAndMap.map;
|
||
} else {
|
||
source = asset.source();
|
||
sourceMap = asset.map(options);
|
||
}
|
||
// Bail before touching the registry if we can't return a usable string
|
||
// source — pinning a non-string-producing asset would only waste the slot.
|
||
if (typeof source !== "string") return;
|
||
if (sourceMap) {
|
||
// The current asset still owns the original map — pin a reference so
|
||
// that a later plugin instance (which will see a rewrapped asset
|
||
// without a map) can recover it on demand.
|
||
if (!registry.has(file)) registry.set(file, asset);
|
||
return { source, sourceMap, mapSource: asset };
|
||
}
|
||
// The current asset (typically a `RawSource` left by an earlier
|
||
// SourceMapDevToolPlugin instance) has no internal map. Re-extract
|
||
// the map from the original Source we pinned earlier. We keep using
|
||
// `source` from the current asset so that any prior wrappers (e.g.
|
||
// appended sourceMappingURL comments) are preserved.
|
||
const original = registry.get(file);
|
||
if (!original) return;
|
||
sourceMap = original.sourceAndMap
|
||
? original.sourceAndMap(options).map
|
||
: original.map(options);
|
||
if (!sourceMap) return;
|
||
return { source, sourceMap, mapSource: original };
|
||
};
|
||
|
||
/**
|
||
* Creating {@link SourceMapTask} for given file
|
||
* @param {string} file current compiled file
|
||
* @param {Source} asset the asset
|
||
* @param {AssetInfo} assetInfo the asset info
|
||
* @param {MapOptions} options source map options
|
||
* @param {Compilation} compilation compilation instance
|
||
* @param {ItemCacheFacade} cacheItem cache item
|
||
* @param {Map<string, Source>} registry compilation-scoped original-source registry
|
||
* @returns {SourceMapTask | undefined} created task instance or `undefined`
|
||
*/
|
||
const getTaskForFile = (
|
||
file,
|
||
asset,
|
||
assetInfo,
|
||
options,
|
||
compilation,
|
||
cacheItem,
|
||
registry
|
||
) => {
|
||
const extracted = extractSourceAndMap(file, asset, options, registry);
|
||
if (!extracted) return;
|
||
const { source, sourceMap, mapSource } = extracted;
|
||
const context = compilation.options.context;
|
||
const root = compilation.compiler.root;
|
||
const cachedAbsolutify = makePathsAbsolute.bindContextCache(context, root);
|
||
const modules = sourceMap.sources.map((source) => {
|
||
if (!source.startsWith("webpack://")) return source;
|
||
source = cachedAbsolutify(source.slice(10));
|
||
const module = compilation.findModule(source);
|
||
return module || source;
|
||
});
|
||
|
||
return {
|
||
file,
|
||
source: /** @type {string} */ (source),
|
||
assetInfo,
|
||
sourceMap,
|
||
mapSource,
|
||
modules,
|
||
cacheItem
|
||
};
|
||
};
|
||
|
||
const PLUGIN_NAME = "SourceMapDevToolPlugin";
|
||
|
||
/**
|
||
* Maps a configuration value (string, RegExp, function, nullish, or array of
|
||
* such) into a JSON-serializable form. Functions and RegExps are turned into
|
||
* their `.toString()` representation so that changes to inline callbacks
|
||
* invalidate caches; everything else is returned as-is so that the surrounding
|
||
* `JSON.stringify` does the escaping.
|
||
*
|
||
* The result is used through `JSON.stringify` to build cache identifiers, so
|
||
* we deliberately avoid any homemade `|` / `,` separators that could collide
|
||
* with characters appearing inside user-provided values such as `publicPath`,
|
||
* template strings, or `sourceRoot`.
|
||
* @param {EXPECTED_ANY} value option value
|
||
* @returns {EXPECTED_ANY} JSON-serializable representation
|
||
*/
|
||
const toCacheKeyValue = (value) => {
|
||
if (value === undefined || value === null) return value;
|
||
if (Array.isArray(value)) return value.map(toCacheKeyValue);
|
||
if (value instanceof RegExp || typeof value === "function") {
|
||
return value.toString();
|
||
}
|
||
return value;
|
||
};
|
||
|
||
class SourceMapDevToolPlugin {
|
||
/**
|
||
* Creates an instance of SourceMapDevToolPlugin.
|
||
* @param {SourceMapDevToolPluginOptions=} options options object
|
||
* @throws {Error} throws error, if got more than 1 arguments
|
||
*/
|
||
constructor(options = {}) {
|
||
/** @type {undefined | null | false | string} */
|
||
this.sourceMapFilename = options.filename;
|
||
/** @type {false | SourceMappingURLComment} */
|
||
this.sourceMappingURLComment =
|
||
options.append === false
|
||
? false
|
||
: // eslint-disable-next-line no-useless-concat
|
||
options.append || "\n//# source" + "MappingURL=[url]";
|
||
/** @type {DevtoolModuleFilenameTemplate} */
|
||
this.moduleFilenameTemplate =
|
||
options.moduleFilenameTemplate || "webpack://[namespace]/[resourcePath]";
|
||
/** @type {DevtoolFallbackModuleFilenameTemplate} */
|
||
this.fallbackModuleFilenameTemplate =
|
||
options.fallbackModuleFilenameTemplate ||
|
||
"webpack://[namespace]/[resourcePath]?[hash]";
|
||
/** @type {DevtoolNamespace} */
|
||
this.namespace = options.namespace || "";
|
||
/** @type {SourceMapDevToolPluginOptions} */
|
||
this.options = options;
|
||
// Cache salt derived from output-affecting options, so that two
|
||
// SourceMapDevToolPlugin instances (or `devtool` + a plugin) operating
|
||
// on the same asset don't share a cache entry. We serialize via
|
||
// `JSON.stringify` rather than a homemade separator so that any
|
||
// special characters (e.g. `|` inside a publicPath or sourceRoot)
|
||
// can't accidentally make two different option sets collide.
|
||
/** @type {string} */
|
||
this._cacheSalt = JSON.stringify([
|
||
toCacheKeyValue(options.filename),
|
||
toCacheKeyValue(options.append),
|
||
toCacheKeyValue(this.moduleFilenameTemplate),
|
||
toCacheKeyValue(this.fallbackModuleFilenameTemplate),
|
||
toCacheKeyValue(this.namespace),
|
||
options.module !== false,
|
||
options.columns !== false,
|
||
Boolean(options.noSources),
|
||
Boolean(options.debugIds),
|
||
options.sourceRoot || "",
|
||
toCacheKeyValue(options.ignoreList),
|
||
options.publicPath || "",
|
||
options.fileContext || ""
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* Applies the plugin by registering its hooks on the compiler.
|
||
* @param {Compiler} compiler compiler instance
|
||
* @returns {void}
|
||
*/
|
||
apply(compiler) {
|
||
compiler.hooks.validate.tap(PLUGIN_NAME, () => {
|
||
compiler.validate(
|
||
() => require("../schemas/plugins/SourceMapDevToolPlugin.json"),
|
||
this.options,
|
||
{
|
||
name: "SourceMap DevTool Plugin",
|
||
baseDataPath: "options"
|
||
},
|
||
(options) =>
|
||
require("../schemas/plugins/SourceMapDevToolPlugin.check")(options)
|
||
);
|
||
});
|
||
|
||
const outputFs =
|
||
/** @type {OutputFileSystem} */
|
||
(compiler.outputFileSystem);
|
||
const sourceMapFilename = this.sourceMapFilename;
|
||
const sourceMappingURLComment = this.sourceMappingURLComment;
|
||
const moduleFilenameTemplate = this.moduleFilenameTemplate;
|
||
const namespace = this.namespace;
|
||
const fallbackModuleFilenameTemplate = this.fallbackModuleFilenameTemplate;
|
||
const requestShortener = compiler.requestShortener;
|
||
const options = this.options;
|
||
options.test = options.test || CSS_AND_JS_MODULE_EXTENSIONS_REGEXP;
|
||
|
||
/** @type {(filename: string) => boolean} */
|
||
const matchObject = ModuleFilenameHelpers.matchObject.bind(
|
||
undefined,
|
||
options
|
||
);
|
||
|
||
compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => {
|
||
new SourceMapDevToolModuleOptionsPlugin(options).apply(compilation);
|
||
|
||
// All SourceMapDevToolPlugin instances on the same compilation share
|
||
// a registry of pristine asset sources, so the second instance to
|
||
// run can still recover the original map after the first instance
|
||
// has replaced the asset with a `RawSource`. The registry lives on a
|
||
// module-scoped `WeakMap` keyed by compilation so it is released
|
||
// automatically and never pollutes the compilation object.
|
||
const originalSources = getOriginalSourceRegistry(compilation);
|
||
|
||
compilation.hooks.processAssets.tapAsync(
|
||
{
|
||
name: PLUGIN_NAME,
|
||
stage: Compilation.PROCESS_ASSETS_STAGE_DEV_TOOLING,
|
||
additionalAssets: true
|
||
},
|
||
(assets, callback) => {
|
||
const chunkGraph = compilation.chunkGraph;
|
||
const cache = compilation.getCache(PLUGIN_NAME);
|
||
/** @type {Map<string | Module, string>} */
|
||
const moduleToSourceNameMapping = new Map();
|
||
const reportProgress =
|
||
ProgressPlugin.getReporter(compilation.compiler) || (() => {});
|
||
|
||
/** @type {Map<string, Chunk>} */
|
||
const fileToChunk = new Map();
|
||
for (const chunk of compilation.chunks) {
|
||
for (const file of chunk.files) {
|
||
fileToChunk.set(file, chunk);
|
||
}
|
||
for (const file of chunk.auxiliaryFiles) {
|
||
fileToChunk.set(file, chunk);
|
||
}
|
||
}
|
||
|
||
/** @type {string[]} */
|
||
const files = [];
|
||
for (const file of Object.keys(assets)) {
|
||
if (matchObject(file)) {
|
||
files.push(file);
|
||
}
|
||
}
|
||
|
||
reportProgress(0);
|
||
/** @type {SourceMapTask[]} */
|
||
const tasks = [];
|
||
let fileIndex = 0;
|
||
|
||
// Shared deduplication set for `Source#clearCache` calls below.
|
||
// Webpack chunks routinely share module-level `CachedSource`
|
||
// instances. A per-call WeakSet would re-walk those shared
|
||
// subtrees once per chunk — 50 chunks × thousands of shared
|
||
// modules in dev/non-minified setups — and worse, every
|
||
// chunk's `sourceAndMap` would have to recompute the cleared
|
||
// caches, churning allocations (measured: +700 MB peak RSS,
|
||
// +6 s wall time on a 50×1000 synthetic build).
|
||
//
|
||
// Sharing one set lets each shared subtree be walked exactly
|
||
// once. The trade-off is that subsequent chunks' `sourceAndMap`
|
||
// calls can repopulate a shared module's `_cachedMaps` after
|
||
// its own clear was skipped (because the module is already in
|
||
// the visited set), leaving at most one populated cache entry
|
||
// per shared module at the end of the run — bounded to a few
|
||
// MB even at the scale of #20961. That's strictly preferable
|
||
// to the alternative's hundreds of MB of transient peak RSS.
|
||
const clearCacheVisited = new WeakSet();
|
||
|
||
asyncLib.each(
|
||
files,
|
||
(file, callback) => {
|
||
const asset =
|
||
/** @type {Readonly<Asset>} */
|
||
(compilation.getAsset(file));
|
||
|
||
const chunk = fileToChunk.get(file);
|
||
const sourceMapNamespace = compilation.getPath(this.namespace, {
|
||
chunk
|
||
});
|
||
|
||
// The cache item identifier must include the per-instance
|
||
// salt so two SourceMapDevToolPlugin instances that target
|
||
// the same `file` don't collide in the persistent cache —
|
||
// they'd otherwise write different content to the same key
|
||
// and invalidate every pack on each build. We encode via
|
||
// `JSON.stringify` so that special characters (e.g. `|`)
|
||
// in an asset filename can't be spoofed to collide with the
|
||
// salt portion of the identifier.
|
||
const cacheItem = cache.getItemCache(
|
||
JSON.stringify([file, this._cacheSalt]),
|
||
cache.mergeEtags(
|
||
cache.getLazyHashedEtag(asset.source),
|
||
sourceMapNamespace
|
||
)
|
||
);
|
||
|
||
cacheItem.get((err, cacheEntry) => {
|
||
if (err) {
|
||
return callback(err);
|
||
}
|
||
/**
|
||
* If presented in cache, reassigns assets. Cache assets already have source maps.
|
||
*/
|
||
if (cacheEntry) {
|
||
// Pin the still-unwrapped asset source in the registry
|
||
// before `compilation.updateAsset` replaces it. This is a
|
||
// pointer assignment — no source-map extraction work — and
|
||
// it lets a subsequent SourceMapDevToolPlugin instance
|
||
// extract the original map on demand even though the
|
||
// persistent cache hit lets us skip processing here.
|
||
if (!originalSources.has(file)) {
|
||
originalSources.set(file, asset.source);
|
||
}
|
||
|
||
const { assets, assetsInfo } = cacheEntry;
|
||
for (const cachedFile of Object.keys(assets)) {
|
||
if (cachedFile === file) {
|
||
compilation.updateAsset(
|
||
cachedFile,
|
||
assets[cachedFile],
|
||
assetsInfo[cachedFile]
|
||
);
|
||
} else {
|
||
compilation.emitAsset(
|
||
cachedFile,
|
||
assets[cachedFile],
|
||
assetsInfo[cachedFile]
|
||
);
|
||
}
|
||
/**
|
||
* Add file to chunk, if not presented there
|
||
*/
|
||
if (cachedFile !== file && chunk !== undefined) {
|
||
chunk.auxiliaryFiles.add(cachedFile);
|
||
}
|
||
}
|
||
|
||
reportProgress(
|
||
(0.5 * ++fileIndex) / files.length,
|
||
file,
|
||
"restored cached SourceMap"
|
||
);
|
||
|
||
return callback();
|
||
}
|
||
|
||
reportProgress(
|
||
(0.5 * fileIndex) / files.length,
|
||
file,
|
||
"generate SourceMap"
|
||
);
|
||
|
||
/** @type {SourceMapTask | undefined} */
|
||
const task = getTaskForFile(
|
||
file,
|
||
asset.source,
|
||
asset.info,
|
||
{
|
||
module: options.module,
|
||
columns: options.columns
|
||
},
|
||
compilation,
|
||
cacheItem,
|
||
originalSources
|
||
);
|
||
|
||
// Release the per-instance caches that `sourceAndMap`
|
||
// just populated. The composed map (and, for
|
||
// `SourceMapSource`, the parsed `_sourceMapAsObject` /
|
||
// `_innerSourceMapAsObject`) otherwise sit on the
|
||
// CachedSource — and every shared child — until phase
|
||
// 2 replaces the asset, which is what causes the OOM
|
||
// spike on builds with thousands of chunks
|
||
// (webpack#20961). Keep `source` since downstream
|
||
// consumers reading the original asset still need it;
|
||
// `hash`/`size` default to retained because they're
|
||
// cheap to keep but expensive to rebuild.
|
||
// `clearCacheVisited` is shared across every call (see
|
||
// its declaration above for the rationale).
|
||
//
|
||
// Target `task.mapSource` (not `asset.source`): when
|
||
// `extractSourceAndMap` falls back to the pinned
|
||
// original (the current asset is a `RawSource` left
|
||
// by an earlier plugin instance), the `sourceAndMap`
|
||
// call populated the original's caches, not the
|
||
// current asset's.
|
||
//
|
||
// Feature-detected: `clearCache` landed in
|
||
// `webpack-sources` 3.5, but `compilation.assets` may
|
||
// hold `Source`-like instances from a third-party
|
||
// plugin built against an older copy of
|
||
// `webpack-sources` (or a hand-rolled implementation).
|
||
// Calling it unconditionally would throw on those.
|
||
if (task && typeof task.mapSource.clearCache === "function") {
|
||
task.mapSource.clearCache(
|
||
{
|
||
maps: true,
|
||
source: false,
|
||
parsedMap: true
|
||
},
|
||
clearCacheVisited
|
||
);
|
||
}
|
||
|
||
if (task) {
|
||
const modules = task.modules;
|
||
|
||
for (let idx = 0; idx < modules.length; idx++) {
|
||
const module = modules[idx];
|
||
|
||
if (
|
||
typeof module === "string" &&
|
||
/^(?:data|https?):/.test(module)
|
||
) {
|
||
moduleToSourceNameMapping.set(module, module);
|
||
continue;
|
||
}
|
||
|
||
if (!moduleToSourceNameMapping.get(module)) {
|
||
moduleToSourceNameMapping.set(
|
||
module,
|
||
ModuleFilenameHelpers.createFilename(
|
||
module,
|
||
{
|
||
moduleFilenameTemplate,
|
||
namespace: sourceMapNamespace
|
||
},
|
||
{
|
||
requestShortener,
|
||
chunkGraph,
|
||
hashFunction: compilation.outputOptions.hashFunction
|
||
}
|
||
)
|
||
);
|
||
}
|
||
}
|
||
|
||
tasks.push(task);
|
||
}
|
||
|
||
reportProgress(
|
||
(0.5 * ++fileIndex) / files.length,
|
||
file,
|
||
"generated SourceMap"
|
||
);
|
||
|
||
callback();
|
||
});
|
||
},
|
||
(err) => {
|
||
if (err) {
|
||
return callback(err);
|
||
}
|
||
|
||
reportProgress(0.5, "resolve sources");
|
||
/** @type {Set<string>} */
|
||
const usedNamesSet = new Set(moduleToSourceNameMapping.values());
|
||
/** @type {Set<string>} */
|
||
const conflictDetectionSet = new Set();
|
||
|
||
/**
|
||
* all modules in defined order (longest identifier first)
|
||
* @type {(string | Module)[]}
|
||
*/
|
||
const allModules = [...moduleToSourceNameMapping.keys()].sort(
|
||
(a, b) => {
|
||
const ai = typeof a === "string" ? a : a.identifier();
|
||
const bi = typeof b === "string" ? b : b.identifier();
|
||
return ai.length - bi.length;
|
||
}
|
||
);
|
||
|
||
// find modules with conflicting source names
|
||
for (let idx = 0; idx < allModules.length; idx++) {
|
||
const module = allModules[idx];
|
||
let sourceName =
|
||
/** @type {string} */
|
||
(moduleToSourceNameMapping.get(module));
|
||
let hasName = conflictDetectionSet.has(sourceName);
|
||
if (!hasName) {
|
||
conflictDetectionSet.add(sourceName);
|
||
continue;
|
||
}
|
||
|
||
// try the fallback name first
|
||
sourceName = ModuleFilenameHelpers.createFilename(
|
||
module,
|
||
{
|
||
moduleFilenameTemplate: fallbackModuleFilenameTemplate,
|
||
namespace
|
||
},
|
||
{
|
||
requestShortener,
|
||
chunkGraph,
|
||
hashFunction: compilation.outputOptions.hashFunction
|
||
}
|
||
);
|
||
hasName = usedNamesSet.has(sourceName);
|
||
if (!hasName) {
|
||
moduleToSourceNameMapping.set(module, sourceName);
|
||
usedNamesSet.add(sourceName);
|
||
continue;
|
||
}
|
||
|
||
// otherwise just append stars until we have a valid name
|
||
while (hasName) {
|
||
sourceName += "*";
|
||
hasName = usedNamesSet.has(sourceName);
|
||
}
|
||
moduleToSourceNameMapping.set(module, sourceName);
|
||
usedNamesSet.add(sourceName);
|
||
}
|
||
|
||
let taskIndex = 0;
|
||
|
||
asyncLib.each(
|
||
tasks,
|
||
(task, callback) => {
|
||
/** @type {Record<string, Source>} */
|
||
const assets = Object.create(null);
|
||
/** @type {Record<string, AssetInfo | undefined>} */
|
||
const assetsInfo = Object.create(null);
|
||
const file = task.file;
|
||
const chunk = fileToChunk.get(file);
|
||
const sourceMap = task.sourceMap;
|
||
const source = task.source;
|
||
const modules = task.modules;
|
||
|
||
reportProgress(
|
||
0.5 + (0.5 * taskIndex) / tasks.length,
|
||
file,
|
||
"attach SourceMap"
|
||
);
|
||
|
||
const moduleFilenames =
|
||
/** @type {string[]} */
|
||
(modules.map((m) => moduleToSourceNameMapping.get(m)));
|
||
// We deliberately do NOT mutate `sourceMap` in place: the
|
||
// task's `sourceMap` reference may be shared with a
|
||
// `SourceMapSource` whose internal map cache is the same
|
||
// object (webpack-sources keeps it cached). A second
|
||
// `SourceMapDevToolPlugin` instance that reads the original
|
||
// source through the registry would otherwise see our
|
||
// rewrites. Instead we build a fresh `outputSourceMap` for
|
||
// the .map file and leave the original alone.
|
||
/** @type {number[] | undefined} */
|
||
let ignoreList;
|
||
if (options.ignoreList) {
|
||
const list = moduleFilenames.reduce(
|
||
/** @type {(acc: number[], sourceName: string, idx: number) => number[]} */ (
|
||
(acc, sourceName, idx) => {
|
||
const rule = /** @type {Rules} */ (
|
||
options.ignoreList
|
||
);
|
||
if (
|
||
ModuleFilenameHelpers.matchPart(sourceName, rule)
|
||
) {
|
||
acc.push(idx);
|
||
}
|
||
return acc;
|
||
}
|
||
),
|
||
[]
|
||
);
|
||
if (list.length > 0) ignoreList = list;
|
||
}
|
||
|
||
const usesContentHash =
|
||
sourceMapFilename &&
|
||
CONTENT_HASH_DETECT_REGEXP.test(sourceMapFilename);
|
||
|
||
resetRegexpState(CONTENT_HASH_DETECT_REGEXP);
|
||
|
||
let outputFile = file;
|
||
// If SourceMap and asset uses contenthash, avoid a circular dependency by hiding hash in `file`
|
||
if (usesContentHash && task.assetInfo.contenthash) {
|
||
const contenthash = task.assetInfo.contenthash;
|
||
const pattern = Array.isArray(contenthash)
|
||
? contenthash.map(quoteMeta).join("|")
|
||
: quoteMeta(contenthash);
|
||
outputFile = outputFile.replace(
|
||
new RegExp(pattern, "g"),
|
||
(m) => "x".repeat(m.length)
|
||
);
|
||
}
|
||
|
||
/** @type {false | SourceMappingURLComment} */
|
||
let currentSourceMappingURLComment = sourceMappingURLComment;
|
||
const cssExtensionDetected =
|
||
CSS_EXTENSION_DETECT_REGEXP.test(file);
|
||
resetRegexpState(CSS_EXTENSION_DETECT_REGEXP);
|
||
if (
|
||
currentSourceMappingURLComment !== false &&
|
||
typeof currentSourceMappingURLComment !== "function" &&
|
||
cssExtensionDetected
|
||
) {
|
||
currentSourceMappingURLComment =
|
||
currentSourceMappingURLComment.replace(
|
||
URL_FORMATTING_REGEXP,
|
||
"\n/*$1*/"
|
||
);
|
||
}
|
||
|
||
/** @type {string | undefined} */
|
||
let debugIdValue;
|
||
if (options.debugIds) {
|
||
const debugId = generateDebugId(source, outputFile);
|
||
debugIdValue = debugId;
|
||
|
||
const debugIdComment = `\n//# debugId=${debugId}`;
|
||
if (currentSourceMappingURLComment === false) {
|
||
currentSourceMappingURLComment = debugIdComment;
|
||
} else if (
|
||
typeof currentSourceMappingURLComment === "function"
|
||
) {
|
||
// Wrap the user's append function so the debug-id
|
||
// comment is prepended at call time. Template-string
|
||
// concatenation would coerce the function to a string
|
||
// and lose its dynamic behavior.
|
||
const wrappedFn = currentSourceMappingURLComment;
|
||
currentSourceMappingURLComment = (pathData, assetInfo) =>
|
||
`${debugIdComment}${wrappedFn(pathData, assetInfo)}`;
|
||
} else {
|
||
currentSourceMappingURLComment = `${debugIdComment}${currentSourceMappingURLComment}`;
|
||
}
|
||
}
|
||
|
||
/** @type {RawSourceMap} */
|
||
const outputSourceMap = {
|
||
...sourceMap,
|
||
sources: moduleFilenames,
|
||
sourceRoot: options.sourceRoot || "",
|
||
file: outputFile
|
||
};
|
||
if (ignoreList !== undefined) {
|
||
outputSourceMap.ignoreList = ignoreList;
|
||
}
|
||
if (options.noSources) {
|
||
outputSourceMap.sourcesContent = undefined;
|
||
}
|
||
if (debugIdValue !== undefined) {
|
||
outputSourceMap.debugId = debugIdValue;
|
||
}
|
||
|
||
if (sourceMapFilename) {
|
||
// External `.map` file: hold the serialized map as a
|
||
// `Buffer` instead of a V8 string. `RawSource` accepts
|
||
// a buffer directly, and the emitted asset stays in
|
||
// `compilation.assets` until the build finishes — so
|
||
// storing the bytes off the V8 heap (where Buffers
|
||
// live, accounted as `external` memory) avoids keeping
|
||
// a large V8 string alive for the rest of the build
|
||
// and reduces heap pressure on `--max-old-space-size`.
|
||
const sourceMapBuffer = Buffer.from(
|
||
JSON.stringify(outputSourceMap),
|
||
"utf8"
|
||
);
|
||
const filename = file;
|
||
const sourceMapContentHash = usesContentHash
|
||
? createHash(compilation.outputOptions.hashFunction)
|
||
.update(sourceMapBuffer)
|
||
.digest("hex")
|
||
: undefined;
|
||
|
||
const pathParams = {
|
||
chunk,
|
||
filename: options.fileContext
|
||
? relative(
|
||
outputFs,
|
||
`/${options.fileContext}`,
|
||
`/${filename}`
|
||
)
|
||
: filename,
|
||
contentHash: sourceMapContentHash
|
||
};
|
||
const { path: sourceMapFile, info: sourceMapInfo } =
|
||
compilation.getPathWithInfo(
|
||
sourceMapFilename,
|
||
pathParams
|
||
);
|
||
const sourceMapUrl = options.publicPath
|
||
? options.publicPath + sourceMapFile
|
||
: relative(
|
||
outputFs,
|
||
dirname(outputFs, `/${file}`),
|
||
`/${sourceMapFile}`
|
||
);
|
||
/** @type {Source} */
|
||
let asset = new RawSource(source);
|
||
if (currentSourceMappingURLComment !== false) {
|
||
// Add source map url to compilation asset, if currentSourceMappingURLComment is set
|
||
asset = new ConcatSource(
|
||
asset,
|
||
compilation.getPath(currentSourceMappingURLComment, {
|
||
url: sourceMapUrl,
|
||
...pathParams
|
||
})
|
||
);
|
||
}
|
||
// Preserve any existing related.sourceMap entries from
|
||
// earlier SourceMapDevToolPlugin runs on the same asset so
|
||
// that all generated maps remain discoverable via asset
|
||
// info (the schema allows string or string[]).
|
||
const existingSourceMap =
|
||
task.assetInfo.related &&
|
||
task.assetInfo.related.sourceMap;
|
||
/** @type {string | string[]} */
|
||
let relatedSourceMap;
|
||
if (
|
||
existingSourceMap === undefined ||
|
||
existingSourceMap === null
|
||
) {
|
||
relatedSourceMap = sourceMapFile;
|
||
} else if (Array.isArray(existingSourceMap)) {
|
||
relatedSourceMap = existingSourceMap.includes(
|
||
sourceMapFile
|
||
)
|
||
? existingSourceMap
|
||
: [...existingSourceMap, sourceMapFile];
|
||
} else {
|
||
relatedSourceMap =
|
||
existingSourceMap === sourceMapFile
|
||
? existingSourceMap
|
||
: [existingSourceMap, sourceMapFile];
|
||
}
|
||
const assetInfo = {
|
||
related: { sourceMap: relatedSourceMap }
|
||
};
|
||
assets[file] = asset;
|
||
assetsInfo[file] = assetInfo;
|
||
compilation.updateAsset(file, asset, assetInfo);
|
||
// Add source map file to compilation assets and chunk files
|
||
const sourceMapAsset = new RawSource(sourceMapBuffer);
|
||
const sourceMapAssetInfo = {
|
||
...sourceMapInfo,
|
||
development: true
|
||
};
|
||
assets[sourceMapFile] = sourceMapAsset;
|
||
assetsInfo[sourceMapFile] = sourceMapAssetInfo;
|
||
compilation.emitAsset(
|
||
sourceMapFile,
|
||
sourceMapAsset,
|
||
sourceMapAssetInfo
|
||
);
|
||
if (chunk !== undefined) {
|
||
chunk.auxiliaryFiles.add(sourceMapFile);
|
||
}
|
||
} else {
|
||
if (currentSourceMappingURLComment === false) {
|
||
throw new Error(
|
||
`${PLUGIN_NAME}: append can't be false when no filename is provided`
|
||
);
|
||
}
|
||
if (typeof currentSourceMappingURLComment === "function") {
|
||
throw new Error(
|
||
`${PLUGIN_NAME}: append can't be a function when no filename is provided`
|
||
);
|
||
}
|
||
// Inline data-URL form: `[map]` gets the raw JSON, `[url]`
|
||
// gets the same JSON base64-encoded. `URL_COMMENT_REGEXP`
|
||
// is a `/g` regex, so a user `append` template with more
|
||
// than one `[url]` placeholder would otherwise re-encode
|
||
// the same JSON per match. Pre-compute both once.
|
||
const sourceMapString = JSON.stringify(outputSourceMap);
|
||
const sourceMapBase64 = Buffer.from(
|
||
sourceMapString,
|
||
"utf8"
|
||
).toString("base64");
|
||
/**
|
||
* Add source map as data url to asset
|
||
*/
|
||
const asset = new ConcatSource(
|
||
new RawSource(source),
|
||
currentSourceMappingURLComment
|
||
.replace(MAP_URL_COMMENT_REGEXP, () => sourceMapString)
|
||
.replace(
|
||
URL_COMMENT_REGEXP,
|
||
() =>
|
||
`data:application/json;charset=utf-8;base64,${sourceMapBase64}`
|
||
)
|
||
);
|
||
assets[file] = asset;
|
||
assetsInfo[file] = undefined;
|
||
compilation.updateAsset(file, asset);
|
||
}
|
||
|
||
task.cacheItem.store({ assets, assetsInfo }, (err) => {
|
||
reportProgress(
|
||
0.5 + (0.5 * ++taskIndex) / tasks.length,
|
||
task.file,
|
||
"attached SourceMap"
|
||
);
|
||
|
||
if (err) {
|
||
return callback(err);
|
||
}
|
||
callback();
|
||
});
|
||
},
|
||
(err) => {
|
||
reportProgress(1);
|
||
callback(err);
|
||
}
|
||
);
|
||
}
|
||
);
|
||
}
|
||
);
|
||
});
|
||
}
|
||
}
|
||
|
||
module.exports = SourceMapDevToolPlugin;
|