| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936 |
- /*
- 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 { getPresentKinds } = require("./TemplatedPathPlugin");
- const createHash = require("./util/createHash");
- const { dirname, relative } = require("./util/fs");
- const generateDebugId = require("./util/generateDebugId");
- const { makePathsAbsolute } = require("./util/identifier");
- /** @import { MapOptions, Source, RawSourceMap } from "webpack-sources" */
- /**
- * @import {
- * DevtoolNamespace,
- * DevtoolModuleFilenameTemplate,
- * DevtoolFallbackModuleFilenameTemplate
- * } from "../declarations/WebpackOptions"
- */
- /**
- * @import {
- * SourceMapDevToolPluginOptions,
- * Rules
- * } from "../declarations/plugins/SourceMapDevToolPlugin"
- */
- /** @import { ItemCacheFacade } from "./CacheFacade" */
- /** @import Chunk from "./Chunk" */
- /** @import { Asset, AssetInfo } from "./Compilation" */
- /** @import Compiler from "./Compiler" */
- /** @import Module from "./Module" */
- /**
- * @import {
- * TemplatePath as SourceMappingURLComment
- * } from "./TemplatedPathPlugin"
- */
- /** @import { OutputFileSystem } from "./util/fs" */
- /**
- * 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 {InstanceType<ItemCacheFacade>} cacheItem cache item
- */
- const METACHARACTERS_REGEXP = /[-[\]\\/{}()*+?.^$|]/g;
- 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 {InstanceType<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 ||
- ModuleFilenameHelpers.DEFAULT_MODULE_FILENAME_TEMPLATE;
- /** @type {DevtoolFallbackModuleFilenameTemplate} */
- this.fallbackModuleFilenameTemplate =
- options.fallbackModuleFilenameTemplate ||
- ModuleFilenameHelpers.DEFAULT_FALLBACK_MODULE_FILENAME_TEMPLATE;
- /** @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 =
- typeof sourceMapFilename === "string" &&
- getPresentKinds(sourceMapFilename).has("contenthash");
- 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;
|