source: frontend/node_modules/webpack/lib/SourceMapDevToolPlugin.js

Last change on this file was 9af201e, checked in by MBK <marija.karapandzova@…>, 11 days ago

Fix frontend appearance

  • Property mode set to 100644
File size: 29.3 KB
Line 
1/*
2 MIT License http://www.opensource.org/licenses/mit-license.php
3 Author Tobias Koppers @sokra
4*/
5
6"use strict";
7
8const asyncLib = require("neo-async");
9const { ConcatSource, RawSource } = require("webpack-sources");
10const Compilation = require("./Compilation");
11const ModuleFilenameHelpers = require("./ModuleFilenameHelpers");
12const ProgressPlugin = require("./ProgressPlugin");
13const SourceMapDevToolModuleOptionsPlugin = require("./SourceMapDevToolModuleOptionsPlugin");
14const createHash = require("./util/createHash");
15const { dirname, relative } = require("./util/fs");
16const generateDebugId = require("./util/generateDebugId");
17const { 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
48const METACHARACTERS_REGEXP = /[-[\]\\/{}()*+?.^$|]/g;
49const CONTENT_HASH_DETECT_REGEXP = /\[contenthash(?::\w+)?\]/;
50const CSS_AND_JS_MODULE_EXTENSIONS_REGEXP = /\.((c|m)?js|css)($|\?)/i;
51const CSS_EXTENSION_DETECT_REGEXP = /\.css(?:$|\?)/i;
52const MAP_URL_COMMENT_REGEXP = /\[map\]/g;
53const URL_COMMENT_REGEXP = /\[url\]/g;
54const 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 */
62const 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 */
71const 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 */
89const 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 */
97const 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 */
121const 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 */
169const 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
202const 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 */
218const 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
227class 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
851module.exports = SourceMapDevToolPlugin;
Note: See TracBrowser for help on using the repository browser.