| 1 | /*
|
|---|
| 2 | MIT License http://www.opensource.org/licenses/mit-license.php
|
|---|
| 3 | Author Tobias Koppers @sokra
|
|---|
| 4 | */
|
|---|
| 5 |
|
|---|
| 6 | "use strict";
|
|---|
| 7 |
|
|---|
| 8 | const asyncLib = require("neo-async");
|
|---|
| 9 | const { ConcatSource, RawSource } = require("webpack-sources");
|
|---|
| 10 | const Compilation = require("./Compilation");
|
|---|
| 11 | const ModuleFilenameHelpers = require("./ModuleFilenameHelpers");
|
|---|
| 12 | const ProgressPlugin = require("./ProgressPlugin");
|
|---|
| 13 | const SourceMapDevToolModuleOptionsPlugin = require("./SourceMapDevToolModuleOptionsPlugin");
|
|---|
| 14 | const createHash = require("./util/createHash");
|
|---|
| 15 | const { dirname, relative } = require("./util/fs");
|
|---|
| 16 | const generateDebugId = require("./util/generateDebugId");
|
|---|
| 17 | const { makePathsAbsolute } = require("./util/identifier");
|
|---|
| 18 |
|
|---|
| 19 | /** @typedef {import("webpack-sources").MapOptions} MapOptions */
|
|---|
| 20 | /** @typedef {import("webpack-sources").Source} Source */
|
|---|
| 21 | /** @typedef {import("../declarations/WebpackOptions").DevtoolNamespace} DevtoolNamespace */
|
|---|
| 22 | /** @typedef {import("../declarations/WebpackOptions").DevtoolModuleFilenameTemplate} DevtoolModuleFilenameTemplate */
|
|---|
| 23 | /** @typedef {import("../declarations/WebpackOptions").DevtoolFallbackModuleFilenameTemplate} DevtoolFallbackModuleFilenameTemplate */
|
|---|
| 24 | /** @typedef {import("../declarations/plugins/SourceMapDevToolPlugin").SourceMapDevToolPluginOptions} SourceMapDevToolPluginOptions */
|
|---|
| 25 | /** @typedef {import("../declarations/plugins/SourceMapDevToolPlugin").Rules} Rules */
|
|---|
| 26 | /** @typedef {import("./CacheFacade").ItemCacheFacade} ItemCacheFacade */
|
|---|
| 27 | /** @typedef {import("./Chunk")} Chunk */
|
|---|
| 28 | /** @typedef {import("./Compilation").Asset} Asset */
|
|---|
| 29 | /** @typedef {import("./Compilation").AssetInfo} AssetInfo */
|
|---|
| 30 | /** @typedef {import("./Compiler")} Compiler */
|
|---|
| 31 | /** @typedef {import("./Module")} Module */
|
|---|
| 32 | /** @typedef {import("./NormalModule").RawSourceMap} RawSourceMap */
|
|---|
| 33 | /** @typedef {import("./TemplatedPathPlugin").TemplatePath} SourceMappingURLComment */
|
|---|
| 34 | /** @typedef {import("./util/fs").OutputFileSystem} OutputFileSystem */
|
|---|
| 35 |
|
|---|
| 36 | /**
|
|---|
| 37 | * Defines the source map task type used by this module.
|
|---|
| 38 | * @typedef {object} SourceMapTask
|
|---|
| 39 | * @property {Source} asset
|
|---|
| 40 | * @property {AssetInfo} assetInfo
|
|---|
| 41 | * @property {(string | Module)[]} modules
|
|---|
| 42 | * @property {string} source
|
|---|
| 43 | * @property {string} file
|
|---|
| 44 | * @property {RawSourceMap} sourceMap
|
|---|
| 45 | * @property {ItemCacheFacade} cacheItem cache item
|
|---|
| 46 | */
|
|---|
| 47 |
|
|---|
| 48 | const METACHARACTERS_REGEXP = /[-[\]\\/{}()*+?.^$|]/g;
|
|---|
| 49 | const CONTENT_HASH_DETECT_REGEXP = /\[contenthash(?::\w+)?\]/;
|
|---|
| 50 | const CSS_AND_JS_MODULE_EXTENSIONS_REGEXP = /\.((c|m)?js|css)($|\?)/i;
|
|---|
| 51 | const CSS_EXTENSION_DETECT_REGEXP = /\.css(?:$|\?)/i;
|
|---|
| 52 | const MAP_URL_COMMENT_REGEXP = /\[map\]/g;
|
|---|
| 53 | const URL_COMMENT_REGEXP = /\[url\]/g;
|
|---|
| 54 | const URL_FORMATTING_REGEXP = /^\n\/\/(.*)$/;
|
|---|
| 55 |
|
|---|
| 56 | /**
|
|---|
| 57 | * Reset's .lastIndex of stateful Regular Expressions
|
|---|
| 58 | * For when `test` or `exec` is called on them
|
|---|
| 59 | * @param {RegExp} regexp Stateful Regular Expression to be reset
|
|---|
| 60 | * @returns {void}
|
|---|
| 61 | */
|
|---|
| 62 | const resetRegexpState = (regexp) => {
|
|---|
| 63 | regexp.lastIndex = -1;
|
|---|
| 64 | };
|
|---|
| 65 |
|
|---|
| 66 | /**
|
|---|
| 67 | * Escapes regular expression metacharacters
|
|---|
| 68 | * @param {string} str String to quote
|
|---|
| 69 | * @returns {string} Escaped string
|
|---|
| 70 | */
|
|---|
| 71 | const quoteMeta = (str) => str.replace(METACHARACTERS_REGEXP, "\\$&");
|
|---|
| 72 |
|
|---|
| 73 | /**
|
|---|
| 74 | * Compilation-scoped registry of original asset sources for multi-plugin
|
|---|
| 75 | * cooperation. The first SourceMapDevToolPlugin instance to see a file pins a
|
|---|
| 76 | * reference to the asset's still-unwrapped {@link Source} object; later
|
|---|
| 77 | * instances whose `asset.source.sourceAndMap()` would now return `null` (the
|
|---|
| 78 | * earlier instance replaced the asset with a `RawSource`) can re-extract the
|
|---|
| 79 | * map from this pinned reference. We keep the registry on a module-scoped
|
|---|
| 80 | * `WeakMap` so the entries are reclaimed automatically when the compilation
|
|---|
| 81 | * itself becomes unreachable; we never store anything on the compilation
|
|---|
| 82 | * object directly.
|
|---|
| 83 | *
|
|---|
| 84 | * Stashing the `Source` object itself rather than an extracted map keeps the
|
|---|
| 85 | * fast path free of cloning and source-map serialization work — the
|
|---|
| 86 | * extraction only happens if a subsequent plugin actually needs the map.
|
|---|
| 87 | * @type {WeakMap<Compilation, Map<string, Source>>}
|
|---|
| 88 | */
|
|---|
| 89 | const originalSourceRegistry = new WeakMap();
|
|---|
| 90 |
|
|---|
| 91 | /**
|
|---|
| 92 | * Returns (creating if necessary) the per-compilation registry of original
|
|---|
| 93 | * asset {@link Source} objects.
|
|---|
| 94 | * @param {Compilation} compilation compilation
|
|---|
| 95 | * @returns {Map<string, Source>} registry
|
|---|
| 96 | */
|
|---|
| 97 | const getOriginalSourceRegistry = (compilation) => {
|
|---|
| 98 | let registry = originalSourceRegistry.get(compilation);
|
|---|
| 99 | if (registry === undefined) {
|
|---|
| 100 | registry = new Map();
|
|---|
| 101 | originalSourceRegistry.set(compilation, registry);
|
|---|
| 102 | }
|
|---|
| 103 | return registry;
|
|---|
| 104 | };
|
|---|
| 105 |
|
|---|
| 106 | /**
|
|---|
| 107 | * Extracts source and source map from a Source object, falling back to a
|
|---|
| 108 | * registered original source for assets that another SourceMapDevToolPlugin
|
|---|
| 109 | * instance has already wrapped (whose internal map is now `null`).
|
|---|
| 110 | *
|
|---|
| 111 | * The returned source is read from the asset as it currently stands — that way
|
|---|
| 112 | * any `sourceMappingURL` comments appended by earlier plugin instances survive
|
|---|
| 113 | * — while the map is taken from the pinned original Source when the current
|
|---|
| 114 | * one no longer carries it.
|
|---|
| 115 | * @param {string} file file name
|
|---|
| 116 | * @param {Source} asset source object as currently held by the compilation
|
|---|
| 117 | * @param {MapOptions} options map extraction options
|
|---|
| 118 | * @param {Map<string, Source>} registry compilation-scoped original-source registry
|
|---|
| 119 | * @returns {{ source: string, sourceMap: RawSourceMap } | undefined} extracted pair or `undefined` when no map is recoverable
|
|---|
| 120 | */
|
|---|
| 121 | const extractSourceAndMap = (file, asset, options, registry) => {
|
|---|
| 122 | /** @type {string | Buffer} */
|
|---|
| 123 | let source;
|
|---|
| 124 | /** @type {null | RawSourceMap} */
|
|---|
| 125 | let sourceMap;
|
|---|
| 126 | if (asset.sourceAndMap) {
|
|---|
| 127 | const sourceAndMap = asset.sourceAndMap(options);
|
|---|
| 128 | source = sourceAndMap.source;
|
|---|
| 129 | sourceMap = sourceAndMap.map;
|
|---|
| 130 | } else {
|
|---|
| 131 | source = asset.source();
|
|---|
| 132 | sourceMap = asset.map(options);
|
|---|
| 133 | }
|
|---|
| 134 | // Bail before touching the registry if we can't return a usable string
|
|---|
| 135 | // source — pinning a non-string-producing asset would only waste the slot.
|
|---|
| 136 | if (typeof source !== "string") return;
|
|---|
| 137 | if (sourceMap) {
|
|---|
| 138 | // The current asset still owns the original map — pin a reference so
|
|---|
| 139 | // that a later plugin instance (which will see a rewrapped asset
|
|---|
| 140 | // without a map) can recover it on demand.
|
|---|
| 141 | if (!registry.has(file)) registry.set(file, asset);
|
|---|
| 142 | } else {
|
|---|
| 143 | // The current asset (typically a `RawSource` left by an earlier
|
|---|
| 144 | // SourceMapDevToolPlugin instance) has no internal map. Re-extract
|
|---|
| 145 | // the map from the original Source we pinned earlier. We keep using
|
|---|
| 146 | // `source` from the current asset so that any prior wrappers (e.g.
|
|---|
| 147 | // appended sourceMappingURL comments) are preserved.
|
|---|
| 148 | const original = registry.get(file);
|
|---|
| 149 | if (!original) return;
|
|---|
| 150 | sourceMap = original.sourceAndMap
|
|---|
| 151 | ? original.sourceAndMap(options).map
|
|---|
| 152 | : original.map(options);
|
|---|
| 153 | if (!sourceMap) return;
|
|---|
| 154 | }
|
|---|
| 155 | return { source, sourceMap };
|
|---|
| 156 | };
|
|---|
| 157 |
|
|---|
| 158 | /**
|
|---|
| 159 | * Creating {@link SourceMapTask} for given file
|
|---|
| 160 | * @param {string} file current compiled file
|
|---|
| 161 | * @param {Source} asset the asset
|
|---|
| 162 | * @param {AssetInfo} assetInfo the asset info
|
|---|
| 163 | * @param {MapOptions} options source map options
|
|---|
| 164 | * @param {Compilation} compilation compilation instance
|
|---|
| 165 | * @param {ItemCacheFacade} cacheItem cache item
|
|---|
| 166 | * @param {Map<string, Source>} registry compilation-scoped original-source registry
|
|---|
| 167 | * @returns {SourceMapTask | undefined} created task instance or `undefined`
|
|---|
| 168 | */
|
|---|
| 169 | const getTaskForFile = (
|
|---|
| 170 | file,
|
|---|
| 171 | asset,
|
|---|
| 172 | assetInfo,
|
|---|
| 173 | options,
|
|---|
| 174 | compilation,
|
|---|
| 175 | cacheItem,
|
|---|
| 176 | registry
|
|---|
| 177 | ) => {
|
|---|
| 178 | const extracted = extractSourceAndMap(file, asset, options, registry);
|
|---|
| 179 | if (!extracted) return;
|
|---|
| 180 | const { source, sourceMap } = extracted;
|
|---|
| 181 | const context = compilation.options.context;
|
|---|
| 182 | const root = compilation.compiler.root;
|
|---|
| 183 | const cachedAbsolutify = makePathsAbsolute.bindContextCache(context, root);
|
|---|
| 184 | const modules = sourceMap.sources.map((source) => {
|
|---|
| 185 | if (!source.startsWith("webpack://")) return source;
|
|---|
| 186 | source = cachedAbsolutify(source.slice(10));
|
|---|
| 187 | const module = compilation.findModule(source);
|
|---|
| 188 | return module || source;
|
|---|
| 189 | });
|
|---|
| 190 |
|
|---|
| 191 | return {
|
|---|
| 192 | file,
|
|---|
| 193 | asset,
|
|---|
| 194 | source: /** @type {string} */ (source),
|
|---|
| 195 | assetInfo,
|
|---|
| 196 | sourceMap,
|
|---|
| 197 | modules,
|
|---|
| 198 | cacheItem
|
|---|
| 199 | };
|
|---|
| 200 | };
|
|---|
| 201 |
|
|---|
| 202 | const PLUGIN_NAME = "SourceMapDevToolPlugin";
|
|---|
| 203 |
|
|---|
| 204 | /**
|
|---|
| 205 | * Maps a configuration value (string, RegExp, function, nullish, or array of
|
|---|
| 206 | * such) into a JSON-serializable form. Functions and RegExps are turned into
|
|---|
| 207 | * their `.toString()` representation so that changes to inline callbacks
|
|---|
| 208 | * invalidate caches; everything else is returned as-is so that the surrounding
|
|---|
| 209 | * `JSON.stringify` does the escaping.
|
|---|
| 210 | *
|
|---|
| 211 | * The result is used through `JSON.stringify` to build cache identifiers, so
|
|---|
| 212 | * we deliberately avoid any homemade `|` / `,` separators that could collide
|
|---|
| 213 | * with characters appearing inside user-provided values such as `publicPath`,
|
|---|
| 214 | * template strings, or `sourceRoot`.
|
|---|
| 215 | * @param {EXPECTED_ANY} value option value
|
|---|
| 216 | * @returns {EXPECTED_ANY} JSON-serializable representation
|
|---|
| 217 | */
|
|---|
| 218 | const toCacheKeyValue = (value) => {
|
|---|
| 219 | if (value === undefined || value === null) return value;
|
|---|
| 220 | if (Array.isArray(value)) return value.map(toCacheKeyValue);
|
|---|
| 221 | if (value instanceof RegExp || typeof value === "function") {
|
|---|
| 222 | return value.toString();
|
|---|
| 223 | }
|
|---|
| 224 | return value;
|
|---|
| 225 | };
|
|---|
| 226 |
|
|---|
| 227 | class SourceMapDevToolPlugin {
|
|---|
| 228 | /**
|
|---|
| 229 | * Creates an instance of SourceMapDevToolPlugin.
|
|---|
| 230 | * @param {SourceMapDevToolPluginOptions=} options options object
|
|---|
| 231 | * @throws {Error} throws error, if got more than 1 arguments
|
|---|
| 232 | */
|
|---|
| 233 | constructor(options = {}) {
|
|---|
| 234 | /** @type {undefined | null | false | string} */
|
|---|
| 235 | this.sourceMapFilename = options.filename;
|
|---|
| 236 | /** @type {false | SourceMappingURLComment} */
|
|---|
| 237 | this.sourceMappingURLComment =
|
|---|
| 238 | options.append === false
|
|---|
| 239 | ? false
|
|---|
| 240 | : // eslint-disable-next-line no-useless-concat
|
|---|
| 241 | options.append || "\n//# source" + "MappingURL=[url]";
|
|---|
| 242 | /** @type {DevtoolModuleFilenameTemplate} */
|
|---|
| 243 | this.moduleFilenameTemplate =
|
|---|
| 244 | options.moduleFilenameTemplate || "webpack://[namespace]/[resourcePath]";
|
|---|
| 245 | /** @type {DevtoolFallbackModuleFilenameTemplate} */
|
|---|
| 246 | this.fallbackModuleFilenameTemplate =
|
|---|
| 247 | options.fallbackModuleFilenameTemplate ||
|
|---|
| 248 | "webpack://[namespace]/[resourcePath]?[hash]";
|
|---|
| 249 | /** @type {DevtoolNamespace} */
|
|---|
| 250 | this.namespace = options.namespace || "";
|
|---|
| 251 | /** @type {SourceMapDevToolPluginOptions} */
|
|---|
| 252 | this.options = options;
|
|---|
| 253 | // Cache salt derived from output-affecting options, so that two
|
|---|
| 254 | // SourceMapDevToolPlugin instances (or `devtool` + a plugin) operating
|
|---|
| 255 | // on the same asset don't share a cache entry. We serialize via
|
|---|
| 256 | // `JSON.stringify` rather than a homemade separator so that any
|
|---|
| 257 | // special characters (e.g. `|` inside a publicPath or sourceRoot)
|
|---|
| 258 | // can't accidentally make two different option sets collide.
|
|---|
| 259 | /** @type {string} */
|
|---|
| 260 | this._cacheSalt = JSON.stringify([
|
|---|
| 261 | toCacheKeyValue(options.filename),
|
|---|
| 262 | toCacheKeyValue(options.append),
|
|---|
| 263 | toCacheKeyValue(this.moduleFilenameTemplate),
|
|---|
| 264 | toCacheKeyValue(this.fallbackModuleFilenameTemplate),
|
|---|
| 265 | toCacheKeyValue(this.namespace),
|
|---|
| 266 | options.module !== false,
|
|---|
| 267 | options.columns !== false,
|
|---|
| 268 | Boolean(options.noSources),
|
|---|
| 269 | Boolean(options.debugIds),
|
|---|
| 270 | options.sourceRoot || "",
|
|---|
| 271 | toCacheKeyValue(options.ignoreList),
|
|---|
| 272 | options.publicPath || "",
|
|---|
| 273 | options.fileContext || ""
|
|---|
| 274 | ]);
|
|---|
| 275 | }
|
|---|
| 276 |
|
|---|
| 277 | /**
|
|---|
| 278 | * Applies the plugin by registering its hooks on the compiler.
|
|---|
| 279 | * @param {Compiler} compiler compiler instance
|
|---|
| 280 | * @returns {void}
|
|---|
| 281 | */
|
|---|
| 282 | apply(compiler) {
|
|---|
| 283 | compiler.hooks.validate.tap(PLUGIN_NAME, () => {
|
|---|
| 284 | compiler.validate(
|
|---|
| 285 | () => require("../schemas/plugins/SourceMapDevToolPlugin.json"),
|
|---|
| 286 | this.options,
|
|---|
| 287 | {
|
|---|
| 288 | name: "SourceMap DevTool Plugin",
|
|---|
| 289 | baseDataPath: "options"
|
|---|
| 290 | },
|
|---|
| 291 | (options) =>
|
|---|
| 292 | require("../schemas/plugins/SourceMapDevToolPlugin.check")(options)
|
|---|
| 293 | );
|
|---|
| 294 | });
|
|---|
| 295 |
|
|---|
| 296 | const outputFs =
|
|---|
| 297 | /** @type {OutputFileSystem} */
|
|---|
| 298 | (compiler.outputFileSystem);
|
|---|
| 299 | const sourceMapFilename = this.sourceMapFilename;
|
|---|
| 300 | const sourceMappingURLComment = this.sourceMappingURLComment;
|
|---|
| 301 | const moduleFilenameTemplate = this.moduleFilenameTemplate;
|
|---|
| 302 | const namespace = this.namespace;
|
|---|
| 303 | const fallbackModuleFilenameTemplate = this.fallbackModuleFilenameTemplate;
|
|---|
| 304 | const requestShortener = compiler.requestShortener;
|
|---|
| 305 | const options = this.options;
|
|---|
| 306 | options.test = options.test || CSS_AND_JS_MODULE_EXTENSIONS_REGEXP;
|
|---|
| 307 |
|
|---|
| 308 | /** @type {(filename: string) => boolean} */
|
|---|
| 309 | const matchObject = ModuleFilenameHelpers.matchObject.bind(
|
|---|
| 310 | undefined,
|
|---|
| 311 | options
|
|---|
| 312 | );
|
|---|
| 313 |
|
|---|
| 314 | compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => {
|
|---|
| 315 | new SourceMapDevToolModuleOptionsPlugin(options).apply(compilation);
|
|---|
| 316 |
|
|---|
| 317 | // All SourceMapDevToolPlugin instances on the same compilation share
|
|---|
| 318 | // a registry of pristine asset sources, so the second instance to
|
|---|
| 319 | // run can still recover the original map after the first instance
|
|---|
| 320 | // has replaced the asset with a `RawSource`. The registry lives on a
|
|---|
| 321 | // module-scoped `WeakMap` keyed by compilation so it is released
|
|---|
| 322 | // automatically and never pollutes the compilation object.
|
|---|
| 323 | const originalSources = getOriginalSourceRegistry(compilation);
|
|---|
| 324 |
|
|---|
| 325 | compilation.hooks.processAssets.tapAsync(
|
|---|
| 326 | {
|
|---|
| 327 | name: PLUGIN_NAME,
|
|---|
| 328 | stage: Compilation.PROCESS_ASSETS_STAGE_DEV_TOOLING,
|
|---|
| 329 | additionalAssets: true
|
|---|
| 330 | },
|
|---|
| 331 | (assets, callback) => {
|
|---|
| 332 | const chunkGraph = compilation.chunkGraph;
|
|---|
| 333 | const cache = compilation.getCache(PLUGIN_NAME);
|
|---|
| 334 | /** @type {Map<string | Module, string>} */
|
|---|
| 335 | const moduleToSourceNameMapping = new Map();
|
|---|
| 336 | const reportProgress =
|
|---|
| 337 | ProgressPlugin.getReporter(compilation.compiler) || (() => {});
|
|---|
| 338 |
|
|---|
| 339 | /** @type {Map<string, Chunk>} */
|
|---|
| 340 | const fileToChunk = new Map();
|
|---|
| 341 | for (const chunk of compilation.chunks) {
|
|---|
| 342 | for (const file of chunk.files) {
|
|---|
| 343 | fileToChunk.set(file, chunk);
|
|---|
| 344 | }
|
|---|
| 345 | for (const file of chunk.auxiliaryFiles) {
|
|---|
| 346 | fileToChunk.set(file, chunk);
|
|---|
| 347 | }
|
|---|
| 348 | }
|
|---|
| 349 |
|
|---|
| 350 | /** @type {string[]} */
|
|---|
| 351 | const files = [];
|
|---|
| 352 | for (const file of Object.keys(assets)) {
|
|---|
| 353 | if (matchObject(file)) {
|
|---|
| 354 | files.push(file);
|
|---|
| 355 | }
|
|---|
| 356 | }
|
|---|
| 357 |
|
|---|
| 358 | reportProgress(0);
|
|---|
| 359 | /** @type {SourceMapTask[]} */
|
|---|
| 360 | const tasks = [];
|
|---|
| 361 | let fileIndex = 0;
|
|---|
| 362 |
|
|---|
| 363 | asyncLib.each(
|
|---|
| 364 | files,
|
|---|
| 365 | (file, callback) => {
|
|---|
| 366 | const asset =
|
|---|
| 367 | /** @type {Readonly<Asset>} */
|
|---|
| 368 | (compilation.getAsset(file));
|
|---|
| 369 |
|
|---|
| 370 | const chunk = fileToChunk.get(file);
|
|---|
| 371 | const sourceMapNamespace = compilation.getPath(this.namespace, {
|
|---|
| 372 | chunk
|
|---|
| 373 | });
|
|---|
| 374 |
|
|---|
| 375 | // The cache item identifier must include the per-instance
|
|---|
| 376 | // salt so two SourceMapDevToolPlugin instances that target
|
|---|
| 377 | // the same `file` don't collide in the persistent cache —
|
|---|
| 378 | // they'd otherwise write different content to the same key
|
|---|
| 379 | // and invalidate every pack on each build. We encode via
|
|---|
| 380 | // `JSON.stringify` so that special characters (e.g. `|`)
|
|---|
| 381 | // in an asset filename can't be spoofed to collide with the
|
|---|
| 382 | // salt portion of the identifier.
|
|---|
| 383 | const cacheItem = cache.getItemCache(
|
|---|
| 384 | JSON.stringify([file, this._cacheSalt]),
|
|---|
| 385 | cache.mergeEtags(
|
|---|
| 386 | cache.getLazyHashedEtag(asset.source),
|
|---|
| 387 | sourceMapNamespace
|
|---|
| 388 | )
|
|---|
| 389 | );
|
|---|
| 390 |
|
|---|
| 391 | cacheItem.get((err, cacheEntry) => {
|
|---|
| 392 | if (err) {
|
|---|
| 393 | return callback(err);
|
|---|
| 394 | }
|
|---|
| 395 | /**
|
|---|
| 396 | * If presented in cache, reassigns assets. Cache assets already have source maps.
|
|---|
| 397 | */
|
|---|
| 398 | if (cacheEntry) {
|
|---|
| 399 | // Pin the still-unwrapped asset source in the registry
|
|---|
| 400 | // before `compilation.updateAsset` replaces it. This is a
|
|---|
| 401 | // pointer assignment — no source-map extraction work — and
|
|---|
| 402 | // it lets a subsequent SourceMapDevToolPlugin instance
|
|---|
| 403 | // extract the original map on demand even though the
|
|---|
| 404 | // persistent cache hit lets us skip processing here.
|
|---|
| 405 | if (!originalSources.has(file)) {
|
|---|
| 406 | originalSources.set(file, asset.source);
|
|---|
| 407 | }
|
|---|
| 408 |
|
|---|
| 409 | const { assets, assetsInfo } = cacheEntry;
|
|---|
| 410 | for (const cachedFile of Object.keys(assets)) {
|
|---|
| 411 | if (cachedFile === file) {
|
|---|
| 412 | compilation.updateAsset(
|
|---|
| 413 | cachedFile,
|
|---|
| 414 | assets[cachedFile],
|
|---|
| 415 | assetsInfo[cachedFile]
|
|---|
| 416 | );
|
|---|
| 417 | } else {
|
|---|
| 418 | compilation.emitAsset(
|
|---|
| 419 | cachedFile,
|
|---|
| 420 | assets[cachedFile],
|
|---|
| 421 | assetsInfo[cachedFile]
|
|---|
| 422 | );
|
|---|
| 423 | }
|
|---|
| 424 | /**
|
|---|
| 425 | * Add file to chunk, if not presented there
|
|---|
| 426 | */
|
|---|
| 427 | if (cachedFile !== file && chunk !== undefined) {
|
|---|
| 428 | chunk.auxiliaryFiles.add(cachedFile);
|
|---|
| 429 | }
|
|---|
| 430 | }
|
|---|
| 431 |
|
|---|
| 432 | reportProgress(
|
|---|
| 433 | (0.5 * ++fileIndex) / files.length,
|
|---|
| 434 | file,
|
|---|
| 435 | "restored cached SourceMap"
|
|---|
| 436 | );
|
|---|
| 437 |
|
|---|
| 438 | return callback();
|
|---|
| 439 | }
|
|---|
| 440 |
|
|---|
| 441 | reportProgress(
|
|---|
| 442 | (0.5 * fileIndex) / files.length,
|
|---|
| 443 | file,
|
|---|
| 444 | "generate SourceMap"
|
|---|
| 445 | );
|
|---|
| 446 |
|
|---|
| 447 | /** @type {SourceMapTask | undefined} */
|
|---|
| 448 | const task = getTaskForFile(
|
|---|
| 449 | file,
|
|---|
| 450 | asset.source,
|
|---|
| 451 | asset.info,
|
|---|
| 452 | {
|
|---|
| 453 | module: options.module,
|
|---|
| 454 | columns: options.columns
|
|---|
| 455 | },
|
|---|
| 456 | compilation,
|
|---|
| 457 | cacheItem,
|
|---|
| 458 | originalSources
|
|---|
| 459 | );
|
|---|
| 460 |
|
|---|
| 461 | if (task) {
|
|---|
| 462 | const modules = task.modules;
|
|---|
| 463 |
|
|---|
| 464 | for (let idx = 0; idx < modules.length; idx++) {
|
|---|
| 465 | const module = modules[idx];
|
|---|
| 466 |
|
|---|
| 467 | if (
|
|---|
| 468 | typeof module === "string" &&
|
|---|
| 469 | /^(?:data|https?):/.test(module)
|
|---|
| 470 | ) {
|
|---|
| 471 | moduleToSourceNameMapping.set(module, module);
|
|---|
| 472 | continue;
|
|---|
| 473 | }
|
|---|
| 474 |
|
|---|
| 475 | if (!moduleToSourceNameMapping.get(module)) {
|
|---|
| 476 | moduleToSourceNameMapping.set(
|
|---|
| 477 | module,
|
|---|
| 478 | ModuleFilenameHelpers.createFilename(
|
|---|
| 479 | module,
|
|---|
| 480 | {
|
|---|
| 481 | moduleFilenameTemplate,
|
|---|
| 482 | namespace: sourceMapNamespace
|
|---|
| 483 | },
|
|---|
| 484 | {
|
|---|
| 485 | requestShortener,
|
|---|
| 486 | chunkGraph,
|
|---|
| 487 | hashFunction: compilation.outputOptions.hashFunction
|
|---|
| 488 | }
|
|---|
| 489 | )
|
|---|
| 490 | );
|
|---|
| 491 | }
|
|---|
| 492 | }
|
|---|
| 493 |
|
|---|
| 494 | tasks.push(task);
|
|---|
| 495 | }
|
|---|
| 496 |
|
|---|
| 497 | reportProgress(
|
|---|
| 498 | (0.5 * ++fileIndex) / files.length,
|
|---|
| 499 | file,
|
|---|
| 500 | "generated SourceMap"
|
|---|
| 501 | );
|
|---|
| 502 |
|
|---|
| 503 | callback();
|
|---|
| 504 | });
|
|---|
| 505 | },
|
|---|
| 506 | (err) => {
|
|---|
| 507 | if (err) {
|
|---|
| 508 | return callback(err);
|
|---|
| 509 | }
|
|---|
| 510 |
|
|---|
| 511 | reportProgress(0.5, "resolve sources");
|
|---|
| 512 | /** @type {Set<string>} */
|
|---|
| 513 | const usedNamesSet = new Set(moduleToSourceNameMapping.values());
|
|---|
| 514 | /** @type {Set<string>} */
|
|---|
| 515 | const conflictDetectionSet = new Set();
|
|---|
| 516 |
|
|---|
| 517 | /**
|
|---|
| 518 | * all modules in defined order (longest identifier first)
|
|---|
| 519 | * @type {(string | Module)[]}
|
|---|
| 520 | */
|
|---|
| 521 | const allModules = [...moduleToSourceNameMapping.keys()].sort(
|
|---|
| 522 | (a, b) => {
|
|---|
| 523 | const ai = typeof a === "string" ? a : a.identifier();
|
|---|
| 524 | const bi = typeof b === "string" ? b : b.identifier();
|
|---|
| 525 | return ai.length - bi.length;
|
|---|
| 526 | }
|
|---|
| 527 | );
|
|---|
| 528 |
|
|---|
| 529 | // find modules with conflicting source names
|
|---|
| 530 | for (let idx = 0; idx < allModules.length; idx++) {
|
|---|
| 531 | const module = allModules[idx];
|
|---|
| 532 | let sourceName =
|
|---|
| 533 | /** @type {string} */
|
|---|
| 534 | (moduleToSourceNameMapping.get(module));
|
|---|
| 535 | let hasName = conflictDetectionSet.has(sourceName);
|
|---|
| 536 | if (!hasName) {
|
|---|
| 537 | conflictDetectionSet.add(sourceName);
|
|---|
| 538 | continue;
|
|---|
| 539 | }
|
|---|
| 540 |
|
|---|
| 541 | // try the fallback name first
|
|---|
| 542 | sourceName = ModuleFilenameHelpers.createFilename(
|
|---|
| 543 | module,
|
|---|
| 544 | {
|
|---|
| 545 | moduleFilenameTemplate: fallbackModuleFilenameTemplate,
|
|---|
| 546 | namespace
|
|---|
| 547 | },
|
|---|
| 548 | {
|
|---|
| 549 | requestShortener,
|
|---|
| 550 | chunkGraph,
|
|---|
| 551 | hashFunction: compilation.outputOptions.hashFunction
|
|---|
| 552 | }
|
|---|
| 553 | );
|
|---|
| 554 | hasName = usedNamesSet.has(sourceName);
|
|---|
| 555 | if (!hasName) {
|
|---|
| 556 | moduleToSourceNameMapping.set(module, sourceName);
|
|---|
| 557 | usedNamesSet.add(sourceName);
|
|---|
| 558 | continue;
|
|---|
| 559 | }
|
|---|
| 560 |
|
|---|
| 561 | // otherwise just append stars until we have a valid name
|
|---|
| 562 | while (hasName) {
|
|---|
| 563 | sourceName += "*";
|
|---|
| 564 | hasName = usedNamesSet.has(sourceName);
|
|---|
| 565 | }
|
|---|
| 566 | moduleToSourceNameMapping.set(module, sourceName);
|
|---|
| 567 | usedNamesSet.add(sourceName);
|
|---|
| 568 | }
|
|---|
| 569 |
|
|---|
| 570 | let taskIndex = 0;
|
|---|
| 571 |
|
|---|
| 572 | asyncLib.each(
|
|---|
| 573 | tasks,
|
|---|
| 574 | (task, callback) => {
|
|---|
| 575 | /** @type {Record<string, Source>} */
|
|---|
| 576 | const assets = Object.create(null);
|
|---|
| 577 | /** @type {Record<string, AssetInfo | undefined>} */
|
|---|
| 578 | const assetsInfo = Object.create(null);
|
|---|
| 579 | const file = task.file;
|
|---|
| 580 | const chunk = fileToChunk.get(file);
|
|---|
| 581 | const sourceMap = task.sourceMap;
|
|---|
| 582 | const source = task.source;
|
|---|
| 583 | const modules = task.modules;
|
|---|
| 584 |
|
|---|
| 585 | reportProgress(
|
|---|
| 586 | 0.5 + (0.5 * taskIndex) / tasks.length,
|
|---|
| 587 | file,
|
|---|
| 588 | "attach SourceMap"
|
|---|
| 589 | );
|
|---|
| 590 |
|
|---|
| 591 | const moduleFilenames =
|
|---|
| 592 | /** @type {string[]} */
|
|---|
| 593 | (modules.map((m) => moduleToSourceNameMapping.get(m)));
|
|---|
| 594 | // We deliberately do NOT mutate `sourceMap` in place: the
|
|---|
| 595 | // task's `sourceMap` reference may be shared with a
|
|---|
| 596 | // `SourceMapSource` whose internal map cache is the same
|
|---|
| 597 | // object (webpack-sources keeps it cached). A second
|
|---|
| 598 | // `SourceMapDevToolPlugin` instance that reads the original
|
|---|
| 599 | // source through the registry would otherwise see our
|
|---|
| 600 | // rewrites. Instead we build a fresh `outputSourceMap` for
|
|---|
| 601 | // the .map file and leave the original alone.
|
|---|
| 602 | /** @type {number[] | undefined} */
|
|---|
| 603 | let ignoreList;
|
|---|
| 604 | if (options.ignoreList) {
|
|---|
| 605 | const list = moduleFilenames.reduce(
|
|---|
| 606 | /** @type {(acc: number[], sourceName: string, idx: number) => number[]} */ (
|
|---|
| 607 | (acc, sourceName, idx) => {
|
|---|
| 608 | const rule = /** @type {Rules} */ (
|
|---|
| 609 | options.ignoreList
|
|---|
| 610 | );
|
|---|
| 611 | if (
|
|---|
| 612 | ModuleFilenameHelpers.matchPart(sourceName, rule)
|
|---|
| 613 | ) {
|
|---|
| 614 | acc.push(idx);
|
|---|
| 615 | }
|
|---|
| 616 | return acc;
|
|---|
| 617 | }
|
|---|
| 618 | ),
|
|---|
| 619 | []
|
|---|
| 620 | );
|
|---|
| 621 | if (list.length > 0) ignoreList = list;
|
|---|
| 622 | }
|
|---|
| 623 |
|
|---|
| 624 | const usesContentHash =
|
|---|
| 625 | sourceMapFilename &&
|
|---|
| 626 | CONTENT_HASH_DETECT_REGEXP.test(sourceMapFilename);
|
|---|
| 627 |
|
|---|
| 628 | resetRegexpState(CONTENT_HASH_DETECT_REGEXP);
|
|---|
| 629 |
|
|---|
| 630 | let outputFile = file;
|
|---|
| 631 | // If SourceMap and asset uses contenthash, avoid a circular dependency by hiding hash in `file`
|
|---|
| 632 | if (usesContentHash && task.assetInfo.contenthash) {
|
|---|
| 633 | const contenthash = task.assetInfo.contenthash;
|
|---|
| 634 | const pattern = Array.isArray(contenthash)
|
|---|
| 635 | ? contenthash.map(quoteMeta).join("|")
|
|---|
| 636 | : quoteMeta(contenthash);
|
|---|
| 637 | outputFile = outputFile.replace(
|
|---|
| 638 | new RegExp(pattern, "g"),
|
|---|
| 639 | (m) => "x".repeat(m.length)
|
|---|
| 640 | );
|
|---|
| 641 | }
|
|---|
| 642 |
|
|---|
| 643 | /** @type {false | SourceMappingURLComment} */
|
|---|
| 644 | let currentSourceMappingURLComment = sourceMappingURLComment;
|
|---|
| 645 | const cssExtensionDetected =
|
|---|
| 646 | CSS_EXTENSION_DETECT_REGEXP.test(file);
|
|---|
| 647 | resetRegexpState(CSS_EXTENSION_DETECT_REGEXP);
|
|---|
| 648 | if (
|
|---|
| 649 | currentSourceMappingURLComment !== false &&
|
|---|
| 650 | typeof currentSourceMappingURLComment !== "function" &&
|
|---|
| 651 | cssExtensionDetected
|
|---|
| 652 | ) {
|
|---|
| 653 | currentSourceMappingURLComment =
|
|---|
| 654 | currentSourceMappingURLComment.replace(
|
|---|
| 655 | URL_FORMATTING_REGEXP,
|
|---|
| 656 | "\n/*$1*/"
|
|---|
| 657 | );
|
|---|
| 658 | }
|
|---|
| 659 |
|
|---|
| 660 | /** @type {string | undefined} */
|
|---|
| 661 | let debugIdValue;
|
|---|
| 662 | if (options.debugIds) {
|
|---|
| 663 | const debugId = generateDebugId(source, outputFile);
|
|---|
| 664 | debugIdValue = debugId;
|
|---|
| 665 |
|
|---|
| 666 | const debugIdComment = `\n//# debugId=${debugId}`;
|
|---|
| 667 | if (currentSourceMappingURLComment === false) {
|
|---|
| 668 | currentSourceMappingURLComment = debugIdComment;
|
|---|
| 669 | } else if (
|
|---|
| 670 | typeof currentSourceMappingURLComment === "function"
|
|---|
| 671 | ) {
|
|---|
| 672 | // Wrap the user's append function so the debug-id
|
|---|
| 673 | // comment is prepended at call time. Template-string
|
|---|
| 674 | // concatenation would coerce the function to a string
|
|---|
| 675 | // and lose its dynamic behavior.
|
|---|
| 676 | const wrappedFn = currentSourceMappingURLComment;
|
|---|
| 677 | currentSourceMappingURLComment = (pathData, assetInfo) =>
|
|---|
| 678 | `${debugIdComment}${wrappedFn(pathData, assetInfo)}`;
|
|---|
| 679 | } else {
|
|---|
| 680 | currentSourceMappingURLComment = `${debugIdComment}${currentSourceMappingURLComment}`;
|
|---|
| 681 | }
|
|---|
| 682 | }
|
|---|
| 683 |
|
|---|
| 684 | /** @type {RawSourceMap} */
|
|---|
| 685 | const outputSourceMap = {
|
|---|
| 686 | ...sourceMap,
|
|---|
| 687 | sources: moduleFilenames,
|
|---|
| 688 | sourceRoot: options.sourceRoot || "",
|
|---|
| 689 | file: outputFile
|
|---|
| 690 | };
|
|---|
| 691 | if (ignoreList !== undefined) {
|
|---|
| 692 | outputSourceMap.ignoreList = ignoreList;
|
|---|
| 693 | }
|
|---|
| 694 | if (options.noSources) {
|
|---|
| 695 | outputSourceMap.sourcesContent = undefined;
|
|---|
| 696 | }
|
|---|
| 697 | if (debugIdValue !== undefined) {
|
|---|
| 698 | outputSourceMap.debugId = debugIdValue;
|
|---|
| 699 | }
|
|---|
| 700 |
|
|---|
| 701 | const sourceMapString = JSON.stringify(outputSourceMap);
|
|---|
| 702 | if (sourceMapFilename) {
|
|---|
| 703 | const filename = file;
|
|---|
| 704 | const sourceMapContentHash = usesContentHash
|
|---|
| 705 | ? createHash(compilation.outputOptions.hashFunction)
|
|---|
| 706 | .update(sourceMapString)
|
|---|
| 707 | .digest("hex")
|
|---|
| 708 | : undefined;
|
|---|
| 709 |
|
|---|
| 710 | const pathParams = {
|
|---|
| 711 | chunk,
|
|---|
| 712 | filename: options.fileContext
|
|---|
| 713 | ? relative(
|
|---|
| 714 | outputFs,
|
|---|
| 715 | `/${options.fileContext}`,
|
|---|
| 716 | `/${filename}`
|
|---|
| 717 | )
|
|---|
| 718 | : filename,
|
|---|
| 719 | contentHash: sourceMapContentHash
|
|---|
| 720 | };
|
|---|
| 721 | const { path: sourceMapFile, info: sourceMapInfo } =
|
|---|
| 722 | compilation.getPathWithInfo(
|
|---|
| 723 | sourceMapFilename,
|
|---|
| 724 | pathParams
|
|---|
| 725 | );
|
|---|
| 726 | const sourceMapUrl = options.publicPath
|
|---|
| 727 | ? options.publicPath + sourceMapFile
|
|---|
| 728 | : relative(
|
|---|
| 729 | outputFs,
|
|---|
| 730 | dirname(outputFs, `/${file}`),
|
|---|
| 731 | `/${sourceMapFile}`
|
|---|
| 732 | );
|
|---|
| 733 | /** @type {Source} */
|
|---|
| 734 | let asset = new RawSource(source);
|
|---|
| 735 | if (currentSourceMappingURLComment !== false) {
|
|---|
| 736 | // Add source map url to compilation asset, if currentSourceMappingURLComment is set
|
|---|
| 737 | asset = new ConcatSource(
|
|---|
| 738 | asset,
|
|---|
| 739 | compilation.getPath(currentSourceMappingURLComment, {
|
|---|
| 740 | url: sourceMapUrl,
|
|---|
| 741 | ...pathParams
|
|---|
| 742 | })
|
|---|
| 743 | );
|
|---|
| 744 | }
|
|---|
| 745 | // Preserve any existing related.sourceMap entries from
|
|---|
| 746 | // earlier SourceMapDevToolPlugin runs on the same asset so
|
|---|
| 747 | // that all generated maps remain discoverable via asset
|
|---|
| 748 | // info (the schema allows string or string[]).
|
|---|
| 749 | const existingSourceMap =
|
|---|
| 750 | task.assetInfo.related &&
|
|---|
| 751 | task.assetInfo.related.sourceMap;
|
|---|
| 752 | /** @type {string | string[]} */
|
|---|
| 753 | let relatedSourceMap;
|
|---|
| 754 | if (
|
|---|
| 755 | existingSourceMap === undefined ||
|
|---|
| 756 | existingSourceMap === null
|
|---|
| 757 | ) {
|
|---|
| 758 | relatedSourceMap = sourceMapFile;
|
|---|
| 759 | } else if (Array.isArray(existingSourceMap)) {
|
|---|
| 760 | relatedSourceMap = existingSourceMap.includes(
|
|---|
| 761 | sourceMapFile
|
|---|
| 762 | )
|
|---|
| 763 | ? existingSourceMap
|
|---|
| 764 | : [...existingSourceMap, sourceMapFile];
|
|---|
| 765 | } else {
|
|---|
| 766 | relatedSourceMap =
|
|---|
| 767 | existingSourceMap === sourceMapFile
|
|---|
| 768 | ? existingSourceMap
|
|---|
| 769 | : [existingSourceMap, sourceMapFile];
|
|---|
| 770 | }
|
|---|
| 771 | const assetInfo = {
|
|---|
| 772 | related: { sourceMap: relatedSourceMap }
|
|---|
| 773 | };
|
|---|
| 774 | assets[file] = asset;
|
|---|
| 775 | assetsInfo[file] = assetInfo;
|
|---|
| 776 | compilation.updateAsset(file, asset, assetInfo);
|
|---|
| 777 | // Add source map file to compilation assets and chunk files
|
|---|
| 778 | const sourceMapAsset = new RawSource(sourceMapString);
|
|---|
| 779 | const sourceMapAssetInfo = {
|
|---|
| 780 | ...sourceMapInfo,
|
|---|
| 781 | development: true
|
|---|
| 782 | };
|
|---|
| 783 | assets[sourceMapFile] = sourceMapAsset;
|
|---|
| 784 | assetsInfo[sourceMapFile] = sourceMapAssetInfo;
|
|---|
| 785 | compilation.emitAsset(
|
|---|
| 786 | sourceMapFile,
|
|---|
| 787 | sourceMapAsset,
|
|---|
| 788 | sourceMapAssetInfo
|
|---|
| 789 | );
|
|---|
| 790 | if (chunk !== undefined) {
|
|---|
| 791 | chunk.auxiliaryFiles.add(sourceMapFile);
|
|---|
| 792 | }
|
|---|
| 793 | } else {
|
|---|
| 794 | if (currentSourceMappingURLComment === false) {
|
|---|
| 795 | throw new Error(
|
|---|
| 796 | `${PLUGIN_NAME}: append can't be false when no filename is provided`
|
|---|
| 797 | );
|
|---|
| 798 | }
|
|---|
| 799 | if (typeof currentSourceMappingURLComment === "function") {
|
|---|
| 800 | throw new Error(
|
|---|
| 801 | `${PLUGIN_NAME}: append can't be a function when no filename is provided`
|
|---|
| 802 | );
|
|---|
| 803 | }
|
|---|
| 804 | /**
|
|---|
| 805 | * Add source map as data url to asset
|
|---|
| 806 | */
|
|---|
| 807 | const asset = new ConcatSource(
|
|---|
| 808 | new RawSource(source),
|
|---|
| 809 | currentSourceMappingURLComment
|
|---|
| 810 | .replace(MAP_URL_COMMENT_REGEXP, () => sourceMapString)
|
|---|
| 811 | .replace(
|
|---|
| 812 | URL_COMMENT_REGEXP,
|
|---|
| 813 | () =>
|
|---|
| 814 | `data:application/json;charset=utf-8;base64,${Buffer.from(
|
|---|
| 815 | sourceMapString,
|
|---|
| 816 | "utf8"
|
|---|
| 817 | ).toString("base64")}`
|
|---|
| 818 | )
|
|---|
| 819 | );
|
|---|
| 820 | assets[file] = asset;
|
|---|
| 821 | assetsInfo[file] = undefined;
|
|---|
| 822 | compilation.updateAsset(file, asset);
|
|---|
| 823 | }
|
|---|
| 824 |
|
|---|
| 825 | task.cacheItem.store({ assets, assetsInfo }, (err) => {
|
|---|
| 826 | reportProgress(
|
|---|
| 827 | 0.5 + (0.5 * ++taskIndex) / tasks.length,
|
|---|
| 828 | task.file,
|
|---|
| 829 | "attached SourceMap"
|
|---|
| 830 | );
|
|---|
| 831 |
|
|---|
| 832 | if (err) {
|
|---|
| 833 | return callback(err);
|
|---|
| 834 | }
|
|---|
| 835 | callback();
|
|---|
| 836 | });
|
|---|
| 837 | },
|
|---|
| 838 | (err) => {
|
|---|
| 839 | reportProgress(1);
|
|---|
| 840 | callback(err);
|
|---|
| 841 | }
|
|---|
| 842 | );
|
|---|
| 843 | }
|
|---|
| 844 | );
|
|---|
| 845 | }
|
|---|
| 846 | );
|
|---|
| 847 | });
|
|---|
| 848 | }
|
|---|
| 849 | }
|
|---|
| 850 |
|
|---|
| 851 | module.exports = SourceMapDevToolPlugin;
|
|---|