SourceMapDevToolPlugin.js 33 KB

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