SourceMapDevToolPlugin.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560
  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 createSchemaValidation = require("./util/create-schema-validation");
  13. const createHash = require("./util/createHash");
  14. const { relative, dirname } = require("./util/fs");
  15. const { makePathsAbsolute } = require("./util/identifier");
  16. /** @typedef {import("webpack-sources").MapOptions} MapOptions */
  17. /** @typedef {import("webpack-sources").Source} Source */
  18. /** @typedef {import("../declarations/plugins/SourceMapDevToolPlugin").SourceMapDevToolPluginOptions} SourceMapDevToolPluginOptions */
  19. /** @typedef {import("./Cache").Etag} Etag */
  20. /** @typedef {import("./CacheFacade").ItemCacheFacade} ItemCacheFacade */
  21. /** @typedef {import("./Chunk")} Chunk */
  22. /** @typedef {import("./Compilation").AssetInfo} AssetInfo */
  23. /** @typedef {import("./Compiler")} Compiler */
  24. /** @typedef {import("./Module")} Module */
  25. /** @typedef {import("./NormalModule").SourceMap} SourceMap */
  26. /** @typedef {import("./util/Hash")} Hash */
  27. const validate = createSchemaValidation(
  28. require("../schemas/plugins/SourceMapDevToolPlugin.check.js"),
  29. () => require("../schemas/plugins/SourceMapDevToolPlugin.json"),
  30. {
  31. name: "SourceMap DevTool Plugin",
  32. baseDataPath: "options"
  33. }
  34. );
  35. /**
  36. * @typedef {object} SourceMapTask
  37. * @property {Source} asset
  38. * @property {AssetInfo} assetInfo
  39. * @property {(string | Module)[]} modules
  40. * @property {string} source
  41. * @property {string} file
  42. * @property {SourceMap} sourceMap
  43. * @property {ItemCacheFacade} cacheItem cache item
  44. */
  45. /**
  46. * Escapes regular expression metacharacters
  47. * @param {string} str String to quote
  48. * @returns {string} Escaped string
  49. */
  50. const quoteMeta = str => {
  51. return str.replace(/[-[\]\\/{}()*+?.^$|]/g, "\\$&");
  52. };
  53. /**
  54. * Creating {@link SourceMapTask} for given file
  55. * @param {string} file current compiled file
  56. * @param {Source} asset the asset
  57. * @param {AssetInfo} assetInfo the asset info
  58. * @param {MapOptions} options source map options
  59. * @param {Compilation} compilation compilation instance
  60. * @param {ItemCacheFacade} cacheItem cache item
  61. * @returns {SourceMapTask | undefined} created task instance or `undefined`
  62. */
  63. const getTaskForFile = (
  64. file,
  65. asset,
  66. assetInfo,
  67. options,
  68. compilation,
  69. cacheItem
  70. ) => {
  71. let source;
  72. /** @type {SourceMap} */
  73. let sourceMap;
  74. /**
  75. * Check if asset can build source map
  76. */
  77. if (asset.sourceAndMap) {
  78. const sourceAndMap = asset.sourceAndMap(options);
  79. sourceMap = /** @type {SourceMap} */ (sourceAndMap.map);
  80. source = sourceAndMap.source;
  81. } else {
  82. sourceMap = /** @type {SourceMap} */ (asset.map(options));
  83. source = asset.source();
  84. }
  85. if (!sourceMap || typeof source !== "string") return;
  86. const context = compilation.options.context;
  87. const root = compilation.compiler.root;
  88. const cachedAbsolutify = makePathsAbsolute.bindContextCache(context, root);
  89. const modules = sourceMap.sources.map(source => {
  90. if (!source.startsWith("webpack://")) return source;
  91. source = cachedAbsolutify(source.slice(10));
  92. const module = compilation.findModule(source);
  93. return module || source;
  94. });
  95. return {
  96. file,
  97. asset,
  98. source,
  99. assetInfo,
  100. sourceMap,
  101. modules,
  102. cacheItem
  103. };
  104. };
  105. class SourceMapDevToolPlugin {
  106. /**
  107. * @param {SourceMapDevToolPluginOptions} [options] options object
  108. * @throws {Error} throws error, if got more than 1 arguments
  109. */
  110. constructor(options = {}) {
  111. validate(options);
  112. /** @type {string | false} */
  113. this.sourceMapFilename = options.filename;
  114. /** @type {string | false} */
  115. this.sourceMappingURLComment =
  116. options.append === false
  117. ? false
  118. : options.append || "\n//# source" + "MappingURL=[url]";
  119. /** @type {string | Function} */
  120. this.moduleFilenameTemplate =
  121. options.moduleFilenameTemplate || "webpack://[namespace]/[resourcePath]";
  122. /** @type {string | Function} */
  123. this.fallbackModuleFilenameTemplate =
  124. options.fallbackModuleFilenameTemplate ||
  125. "webpack://[namespace]/[resourcePath]?[hash]";
  126. /** @type {string} */
  127. this.namespace = options.namespace || "";
  128. /** @type {SourceMapDevToolPluginOptions} */
  129. this.options = options;
  130. }
  131. /**
  132. * Apply the plugin
  133. * @param {Compiler} compiler compiler instance
  134. * @returns {void}
  135. */
  136. apply(compiler) {
  137. const outputFs = compiler.outputFileSystem;
  138. const sourceMapFilename = this.sourceMapFilename;
  139. const sourceMappingURLComment = this.sourceMappingURLComment;
  140. const moduleFilenameTemplate = this.moduleFilenameTemplate;
  141. const namespace = this.namespace;
  142. const fallbackModuleFilenameTemplate = this.fallbackModuleFilenameTemplate;
  143. const requestShortener = compiler.requestShortener;
  144. const options = this.options;
  145. options.test = options.test || /\.((c|m)?js|css)($|\?)/i;
  146. const matchObject = ModuleFilenameHelpers.matchObject.bind(
  147. undefined,
  148. options
  149. );
  150. compiler.hooks.compilation.tap("SourceMapDevToolPlugin", compilation => {
  151. new SourceMapDevToolModuleOptionsPlugin(options).apply(compilation);
  152. compilation.hooks.processAssets.tapAsync(
  153. {
  154. name: "SourceMapDevToolPlugin",
  155. stage: Compilation.PROCESS_ASSETS_STAGE_DEV_TOOLING,
  156. additionalAssets: true
  157. },
  158. (assets, callback) => {
  159. const chunkGraph = compilation.chunkGraph;
  160. const cache = compilation.getCache("SourceMapDevToolPlugin");
  161. /** @type {Map<string | Module, string>} */
  162. const moduleToSourceNameMapping = new Map();
  163. /**
  164. * @type {Function}
  165. * @returns {void}
  166. */
  167. const reportProgress =
  168. ProgressPlugin.getReporter(compilation.compiler) || (() => {});
  169. /** @type {Map<string, Chunk>} */
  170. const fileToChunk = new Map();
  171. for (const chunk of compilation.chunks) {
  172. for (const file of chunk.files) {
  173. fileToChunk.set(file, chunk);
  174. }
  175. for (const file of chunk.auxiliaryFiles) {
  176. fileToChunk.set(file, chunk);
  177. }
  178. }
  179. /** @type {string[]} */
  180. const files = [];
  181. for (const file of Object.keys(assets)) {
  182. if (matchObject(file)) {
  183. files.push(file);
  184. }
  185. }
  186. reportProgress(0.0);
  187. /** @type {SourceMapTask[]} */
  188. const tasks = [];
  189. let fileIndex = 0;
  190. asyncLib.each(
  191. files,
  192. (file, callback) => {
  193. const asset = compilation.getAsset(file);
  194. if (asset.info.related && asset.info.related.sourceMap) {
  195. fileIndex++;
  196. return callback();
  197. }
  198. const cacheItem = cache.getItemCache(
  199. file,
  200. cache.mergeEtags(
  201. cache.getLazyHashedEtag(asset.source),
  202. namespace
  203. )
  204. );
  205. cacheItem.get((err, cacheEntry) => {
  206. if (err) {
  207. return callback(err);
  208. }
  209. /**
  210. * If presented in cache, reassigns assets. Cache assets already have source maps.
  211. */
  212. if (cacheEntry) {
  213. const { assets, assetsInfo } = cacheEntry;
  214. for (const cachedFile of Object.keys(assets)) {
  215. if (cachedFile === file) {
  216. compilation.updateAsset(
  217. cachedFile,
  218. assets[cachedFile],
  219. assetsInfo[cachedFile]
  220. );
  221. } else {
  222. compilation.emitAsset(
  223. cachedFile,
  224. assets[cachedFile],
  225. assetsInfo[cachedFile]
  226. );
  227. }
  228. /**
  229. * Add file to chunk, if not presented there
  230. */
  231. if (cachedFile !== file) {
  232. const chunk = fileToChunk.get(file);
  233. if (chunk !== undefined)
  234. chunk.auxiliaryFiles.add(cachedFile);
  235. }
  236. }
  237. reportProgress(
  238. (0.5 * ++fileIndex) / files.length,
  239. file,
  240. "restored cached SourceMap"
  241. );
  242. return callback();
  243. }
  244. reportProgress(
  245. (0.5 * fileIndex) / files.length,
  246. file,
  247. "generate SourceMap"
  248. );
  249. /** @type {SourceMapTask | undefined} */
  250. const task = getTaskForFile(
  251. file,
  252. asset.source,
  253. asset.info,
  254. {
  255. module: options.module,
  256. columns: options.columns
  257. },
  258. compilation,
  259. cacheItem
  260. );
  261. if (task) {
  262. const modules = task.modules;
  263. for (let idx = 0; idx < modules.length; idx++) {
  264. const module = modules[idx];
  265. if (!moduleToSourceNameMapping.get(module)) {
  266. moduleToSourceNameMapping.set(
  267. module,
  268. ModuleFilenameHelpers.createFilename(
  269. module,
  270. {
  271. moduleFilenameTemplate: moduleFilenameTemplate,
  272. namespace: namespace
  273. },
  274. {
  275. requestShortener,
  276. chunkGraph,
  277. hashFunction: compilation.outputOptions.hashFunction
  278. }
  279. )
  280. );
  281. }
  282. }
  283. tasks.push(task);
  284. }
  285. reportProgress(
  286. (0.5 * ++fileIndex) / files.length,
  287. file,
  288. "generated SourceMap"
  289. );
  290. callback();
  291. });
  292. },
  293. err => {
  294. if (err) {
  295. return callback(err);
  296. }
  297. reportProgress(0.5, "resolve sources");
  298. /** @type {Set<string>} */
  299. const usedNamesSet = new Set(moduleToSourceNameMapping.values());
  300. /** @type {Set<string>} */
  301. const conflictDetectionSet = new Set();
  302. /**
  303. * all modules in defined order (longest identifier first)
  304. * @type {Array<string | Module>}
  305. */
  306. const allModules = Array.from(
  307. moduleToSourceNameMapping.keys()
  308. ).sort((a, b) => {
  309. const ai = typeof a === "string" ? a : a.identifier();
  310. const bi = typeof b === "string" ? b : b.identifier();
  311. return ai.length - bi.length;
  312. });
  313. // find modules with conflicting source names
  314. for (let idx = 0; idx < allModules.length; idx++) {
  315. const module = allModules[idx];
  316. let sourceName = moduleToSourceNameMapping.get(module);
  317. let hasName = conflictDetectionSet.has(sourceName);
  318. if (!hasName) {
  319. conflictDetectionSet.add(sourceName);
  320. continue;
  321. }
  322. // try the fallback name first
  323. sourceName = ModuleFilenameHelpers.createFilename(
  324. module,
  325. {
  326. moduleFilenameTemplate: fallbackModuleFilenameTemplate,
  327. namespace: namespace
  328. },
  329. {
  330. requestShortener,
  331. chunkGraph,
  332. hashFunction: compilation.outputOptions.hashFunction
  333. }
  334. );
  335. hasName = usedNamesSet.has(sourceName);
  336. if (!hasName) {
  337. moduleToSourceNameMapping.set(module, sourceName);
  338. usedNamesSet.add(sourceName);
  339. continue;
  340. }
  341. // otherwise just append stars until we have a valid name
  342. while (hasName) {
  343. sourceName += "*";
  344. hasName = usedNamesSet.has(sourceName);
  345. }
  346. moduleToSourceNameMapping.set(module, sourceName);
  347. usedNamesSet.add(sourceName);
  348. }
  349. let taskIndex = 0;
  350. asyncLib.each(
  351. tasks,
  352. (task, callback) => {
  353. const assets = Object.create(null);
  354. const assetsInfo = Object.create(null);
  355. const file = task.file;
  356. const chunk = fileToChunk.get(file);
  357. const sourceMap = task.sourceMap;
  358. const source = task.source;
  359. const modules = task.modules;
  360. reportProgress(
  361. 0.5 + (0.5 * taskIndex) / tasks.length,
  362. file,
  363. "attach SourceMap"
  364. );
  365. const moduleFilenames = modules.map(m =>
  366. moduleToSourceNameMapping.get(m)
  367. );
  368. sourceMap.sources = moduleFilenames;
  369. if (options.noSources) {
  370. sourceMap.sourcesContent = undefined;
  371. }
  372. sourceMap.sourceRoot = options.sourceRoot || "";
  373. sourceMap.file = file;
  374. const usesContentHash =
  375. sourceMapFilename &&
  376. /\[contenthash(:\w+)?\]/.test(sourceMapFilename);
  377. // If SourceMap and asset uses contenthash, avoid a circular dependency by hiding hash in `file`
  378. if (usesContentHash && task.assetInfo.contenthash) {
  379. const contenthash = task.assetInfo.contenthash;
  380. let pattern;
  381. if (Array.isArray(contenthash)) {
  382. pattern = contenthash.map(quoteMeta).join("|");
  383. } else {
  384. pattern = quoteMeta(contenthash);
  385. }
  386. sourceMap.file = sourceMap.file.replace(
  387. new RegExp(pattern, "g"),
  388. m => "x".repeat(m.length)
  389. );
  390. }
  391. /** @type {string | false} */
  392. let currentSourceMappingURLComment = sourceMappingURLComment;
  393. if (
  394. currentSourceMappingURLComment !== false &&
  395. /\.css($|\?)/i.test(file)
  396. ) {
  397. currentSourceMappingURLComment =
  398. currentSourceMappingURLComment.replace(
  399. /^\n\/\/(.*)$/,
  400. "\n/*$1*/"
  401. );
  402. }
  403. const sourceMapString = JSON.stringify(sourceMap);
  404. if (sourceMapFilename) {
  405. let filename = file;
  406. const sourceMapContentHash =
  407. usesContentHash &&
  408. /** @type {string} */ (
  409. createHash(compilation.outputOptions.hashFunction)
  410. .update(sourceMapString)
  411. .digest("hex")
  412. );
  413. const pathParams = {
  414. chunk,
  415. filename: options.fileContext
  416. ? relative(
  417. outputFs,
  418. `/${options.fileContext}`,
  419. `/${filename}`
  420. )
  421. : filename,
  422. contentHash: sourceMapContentHash
  423. };
  424. const { path: sourceMapFile, info: sourceMapInfo } =
  425. compilation.getPathWithInfo(
  426. sourceMapFilename,
  427. pathParams
  428. );
  429. const sourceMapUrl = options.publicPath
  430. ? options.publicPath + sourceMapFile
  431. : relative(
  432. outputFs,
  433. dirname(outputFs, `/${file}`),
  434. `/${sourceMapFile}`
  435. );
  436. /** @type {Source} */
  437. let asset = new RawSource(source);
  438. if (currentSourceMappingURLComment !== false) {
  439. // Add source map url to compilation asset, if currentSourceMappingURLComment is set
  440. asset = new ConcatSource(
  441. asset,
  442. compilation.getPath(
  443. currentSourceMappingURLComment,
  444. Object.assign({ url: sourceMapUrl }, pathParams)
  445. )
  446. );
  447. }
  448. const assetInfo = {
  449. related: { sourceMap: sourceMapFile }
  450. };
  451. assets[file] = asset;
  452. assetsInfo[file] = assetInfo;
  453. compilation.updateAsset(file, asset, assetInfo);
  454. // Add source map file to compilation assets and chunk files
  455. const sourceMapAsset = new RawSource(sourceMapString);
  456. const sourceMapAssetInfo = {
  457. ...sourceMapInfo,
  458. development: true
  459. };
  460. assets[sourceMapFile] = sourceMapAsset;
  461. assetsInfo[sourceMapFile] = sourceMapAssetInfo;
  462. compilation.emitAsset(
  463. sourceMapFile,
  464. sourceMapAsset,
  465. sourceMapAssetInfo
  466. );
  467. if (chunk !== undefined)
  468. chunk.auxiliaryFiles.add(sourceMapFile);
  469. } else {
  470. if (currentSourceMappingURLComment === false) {
  471. throw new Error(
  472. "SourceMapDevToolPlugin: append can't be false when no filename is provided"
  473. );
  474. }
  475. /**
  476. * Add source map as data url to asset
  477. */
  478. const asset = new ConcatSource(
  479. new RawSource(source),
  480. currentSourceMappingURLComment
  481. .replace(/\[map\]/g, () => sourceMapString)
  482. .replace(
  483. /\[url\]/g,
  484. () =>
  485. `data:application/json;charset=utf-8;base64,${Buffer.from(
  486. sourceMapString,
  487. "utf-8"
  488. ).toString("base64")}`
  489. )
  490. );
  491. assets[file] = asset;
  492. assetsInfo[file] = undefined;
  493. compilation.updateAsset(file, asset);
  494. }
  495. task.cacheItem.store({ assets, assetsInfo }, err => {
  496. reportProgress(
  497. 0.5 + (0.5 * ++taskIndex) / tasks.length,
  498. task.file,
  499. "attached SourceMap"
  500. );
  501. if (err) {
  502. return callback(err);
  503. }
  504. callback();
  505. });
  506. },
  507. err => {
  508. reportProgress(1.0);
  509. callback(err);
  510. }
  511. );
  512. }
  513. );
  514. }
  515. );
  516. });
  517. }
  518. }
  519. module.exports = SourceMapDevToolPlugin;