SourceMapDevToolPlugin.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Tobias Koppers @sokra
  4. */
  5. "use strict";
  6. const path = require("path");
  7. const { ConcatSource, RawSource } = require("webpack-sources");
  8. const ModuleFilenameHelpers = require("./ModuleFilenameHelpers");
  9. const SourceMapDevToolModuleOptionsPlugin = require("./SourceMapDevToolModuleOptionsPlugin");
  10. const createHash = require("./util/createHash");
  11. const { absolutify } = require("./util/identifier");
  12. const validateOptions = require("schema-utils");
  13. const schema = require("../schemas/plugins/SourceMapDevToolPlugin.json");
  14. /** @typedef {import("../declarations/plugins/SourceMapDevToolPlugin").SourceMapDevToolPluginOptions} SourceMapDevToolPluginOptions */
  15. /** @typedef {import("./Chunk")} Chunk */
  16. /** @typedef {import("webpack-sources").Source} Source */
  17. /** @typedef {import("source-map").RawSourceMap} SourceMap */
  18. /** @typedef {import("./Module")} Module */
  19. /** @typedef {import("./Compilation")} Compilation */
  20. /** @typedef {import("./Compiler")} Compiler */
  21. /** @typedef {import("./Compilation")} SourceMapDefinition */
  22. /**
  23. * @typedef {object} SourceMapTask
  24. * @property {Source} asset
  25. * @property {Array<string | Module>} [modules]
  26. * @property {string} source
  27. * @property {string} file
  28. * @property {SourceMap} sourceMap
  29. * @property {Chunk} chunk
  30. */
  31. /**
  32. * @param {string} name file path
  33. * @returns {string} file name
  34. */
  35. const basename = name => {
  36. if (!name.includes("/")) return name;
  37. return name.substr(name.lastIndexOf("/") + 1);
  38. };
  39. /**
  40. * @type {WeakMap<Source, {file: string, assets: {[k: string]: ConcatSource | RawSource}}>}
  41. */
  42. const assetsCache = new WeakMap();
  43. /**
  44. * Creating {@link SourceMapTask} for given file
  45. * @param {string} file current compiled file
  46. * @param {Source} asset the asset
  47. * @param {Chunk} chunk related chunk
  48. * @param {SourceMapDevToolPluginOptions} options source map options
  49. * @param {Compilation} compilation compilation instance
  50. * @returns {SourceMapTask | undefined} created task instance or `undefined`
  51. */
  52. const getTaskForFile = (file, asset, chunk, options, compilation) => {
  53. let source, sourceMap;
  54. /**
  55. * Check if asset can build source map
  56. */
  57. if (asset.sourceAndMap) {
  58. const sourceAndMap = asset.sourceAndMap(options);
  59. sourceMap = sourceAndMap.map;
  60. source = sourceAndMap.source;
  61. } else {
  62. sourceMap = asset.map(options);
  63. source = asset.source();
  64. }
  65. if (!sourceMap || typeof source !== "string") return;
  66. const context = compilation.options.context;
  67. const modules = sourceMap.sources.map(source => {
  68. if (source.startsWith("webpack://")) {
  69. source = absolutify(context, source.slice(10));
  70. }
  71. const module = compilation.findModule(source);
  72. return module || source;
  73. });
  74. return {
  75. chunk,
  76. file,
  77. asset,
  78. source,
  79. sourceMap,
  80. modules
  81. };
  82. };
  83. class SourceMapDevToolPlugin {
  84. /**
  85. * @param {SourceMapDevToolPluginOptions} [options] options object
  86. * @throws {Error} throws error, if got more than 1 arguments
  87. */
  88. constructor(options) {
  89. if (arguments.length > 1) {
  90. throw new Error(
  91. "SourceMapDevToolPlugin only takes one argument (pass an options object)"
  92. );
  93. }
  94. if (!options) options = {};
  95. validateOptions(schema, options, "SourceMap DevTool Plugin");
  96. /** @type {string | false} */
  97. this.sourceMapFilename = options.filename;
  98. /** @type {string | false} */
  99. this.sourceMappingURLComment =
  100. options.append === false
  101. ? false
  102. : options.append || "\n//# sourceMappingURL=[url]";
  103. /** @type {string | Function} */
  104. this.moduleFilenameTemplate =
  105. options.moduleFilenameTemplate || "webpack://[namespace]/[resourcePath]";
  106. /** @type {string | Function} */
  107. this.fallbackModuleFilenameTemplate =
  108. options.fallbackModuleFilenameTemplate ||
  109. "webpack://[namespace]/[resourcePath]?[hash]";
  110. /** @type {string} */
  111. this.namespace = options.namespace || "";
  112. /** @type {SourceMapDevToolPluginOptions} */
  113. this.options = options;
  114. }
  115. /**
  116. * Apply compiler
  117. * @param {Compiler} compiler compiler instance
  118. * @returns {void}
  119. */
  120. apply(compiler) {
  121. const sourceMapFilename = this.sourceMapFilename;
  122. const sourceMappingURLComment = this.sourceMappingURLComment;
  123. const moduleFilenameTemplate = this.moduleFilenameTemplate;
  124. const namespace = this.namespace;
  125. const fallbackModuleFilenameTemplate = this.fallbackModuleFilenameTemplate;
  126. const requestShortener = compiler.requestShortener;
  127. const options = this.options;
  128. options.test = options.test || /\.(m?js|css)($|\?)/i;
  129. const matchObject = ModuleFilenameHelpers.matchObject.bind(
  130. undefined,
  131. options
  132. );
  133. compiler.hooks.compilation.tap("SourceMapDevToolPlugin", compilation => {
  134. new SourceMapDevToolModuleOptionsPlugin(options).apply(compilation);
  135. compilation.hooks.afterOptimizeChunkAssets.tap(
  136. /** @type {TODO} */
  137. ({ name: "SourceMapDevToolPlugin", context: true }),
  138. /**
  139. * @param {object} context hook context
  140. * @param {Array<Chunk>} chunks resulted chunks
  141. * @throws {Error} throws error, if `sourceMapFilename === false && sourceMappingURLComment === false`
  142. * @returns {void}
  143. */
  144. (context, chunks) => {
  145. /** @type {Map<string | Module, string>} */
  146. const moduleToSourceNameMapping = new Map();
  147. /**
  148. * @type {Function}
  149. * @returns {void}
  150. */
  151. const reportProgress =
  152. context && context.reportProgress
  153. ? context.reportProgress
  154. : () => {};
  155. const files = [];
  156. for (const chunk of chunks) {
  157. for (const file of chunk.files) {
  158. if (matchObject(file)) {
  159. files.push({
  160. file,
  161. chunk
  162. });
  163. }
  164. }
  165. }
  166. reportProgress(0.0);
  167. const tasks = [];
  168. files.forEach(({ file, chunk }, idx) => {
  169. const asset = compilation.getAsset(file).source;
  170. const cache = assetsCache.get(asset);
  171. /**
  172. * If presented in cache, reassigns assets. Cache assets already have source maps.
  173. */
  174. if (cache && cache.file === file) {
  175. for (const cachedFile in cache.assets) {
  176. if (cachedFile === file) {
  177. compilation.updateAsset(cachedFile, cache.assets[cachedFile]);
  178. } else {
  179. compilation.emitAsset(cachedFile, cache.assets[cachedFile], {
  180. development: true
  181. });
  182. }
  183. /**
  184. * Add file to chunk, if not presented there
  185. */
  186. if (cachedFile !== file) chunk.files.push(cachedFile);
  187. }
  188. return;
  189. }
  190. reportProgress(
  191. (0.5 * idx) / files.length,
  192. file,
  193. "generate SourceMap"
  194. );
  195. /** @type {SourceMapTask | undefined} */
  196. const task = getTaskForFile(
  197. file,
  198. asset,
  199. chunk,
  200. options,
  201. compilation
  202. );
  203. if (task) {
  204. const modules = task.modules;
  205. for (let idx = 0; idx < modules.length; idx++) {
  206. const module = modules[idx];
  207. if (!moduleToSourceNameMapping.get(module)) {
  208. moduleToSourceNameMapping.set(
  209. module,
  210. ModuleFilenameHelpers.createFilename(
  211. module,
  212. {
  213. moduleFilenameTemplate: moduleFilenameTemplate,
  214. namespace: namespace
  215. },
  216. requestShortener
  217. )
  218. );
  219. }
  220. }
  221. tasks.push(task);
  222. }
  223. });
  224. reportProgress(0.5, "resolve sources");
  225. /** @type {Set<string>} */
  226. const usedNamesSet = new Set(moduleToSourceNameMapping.values());
  227. /** @type {Set<string>} */
  228. const conflictDetectionSet = new Set();
  229. /**
  230. * all modules in defined order (longest identifier first)
  231. * @type {Array<string | Module>}
  232. */
  233. const allModules = Array.from(moduleToSourceNameMapping.keys()).sort(
  234. (a, b) => {
  235. const ai = typeof a === "string" ? a : a.identifier();
  236. const bi = typeof b === "string" ? b : b.identifier();
  237. return ai.length - bi.length;
  238. }
  239. );
  240. // find modules with conflicting source names
  241. for (let idx = 0; idx < allModules.length; idx++) {
  242. const module = allModules[idx];
  243. let sourceName = moduleToSourceNameMapping.get(module);
  244. let hasName = conflictDetectionSet.has(sourceName);
  245. if (!hasName) {
  246. conflictDetectionSet.add(sourceName);
  247. continue;
  248. }
  249. // try the fallback name first
  250. sourceName = ModuleFilenameHelpers.createFilename(
  251. module,
  252. {
  253. moduleFilenameTemplate: fallbackModuleFilenameTemplate,
  254. namespace: namespace
  255. },
  256. requestShortener
  257. );
  258. hasName = usedNamesSet.has(sourceName);
  259. if (!hasName) {
  260. moduleToSourceNameMapping.set(module, sourceName);
  261. usedNamesSet.add(sourceName);
  262. continue;
  263. }
  264. // elsewise just append stars until we have a valid name
  265. while (hasName) {
  266. sourceName += "*";
  267. hasName = usedNamesSet.has(sourceName);
  268. }
  269. moduleToSourceNameMapping.set(module, sourceName);
  270. usedNamesSet.add(sourceName);
  271. }
  272. tasks.forEach((task, index) => {
  273. reportProgress(
  274. 0.5 + (0.5 * index) / tasks.length,
  275. task.file,
  276. "attach SourceMap"
  277. );
  278. const assets = Object.create(null);
  279. const chunk = task.chunk;
  280. const file = task.file;
  281. const asset = task.asset;
  282. const sourceMap = task.sourceMap;
  283. const source = task.source;
  284. const modules = task.modules;
  285. const moduleFilenames = modules.map(m =>
  286. moduleToSourceNameMapping.get(m)
  287. );
  288. sourceMap.sources = moduleFilenames;
  289. if (options.noSources) {
  290. sourceMap.sourcesContent = undefined;
  291. }
  292. sourceMap.sourceRoot = options.sourceRoot || "";
  293. sourceMap.file = file;
  294. assetsCache.set(asset, { file, assets });
  295. /** @type {string | false} */
  296. let currentSourceMappingURLComment = sourceMappingURLComment;
  297. if (
  298. currentSourceMappingURLComment !== false &&
  299. /\.css($|\?)/i.test(file)
  300. ) {
  301. currentSourceMappingURLComment = currentSourceMappingURLComment.replace(
  302. /^\n\/\/(.*)$/,
  303. "\n/*$1*/"
  304. );
  305. }
  306. const sourceMapString = JSON.stringify(sourceMap);
  307. if (sourceMapFilename) {
  308. let filename = file;
  309. let query = "";
  310. const idx = filename.indexOf("?");
  311. if (idx >= 0) {
  312. query = filename.substr(idx);
  313. filename = filename.substr(0, idx);
  314. }
  315. const pathParams = {
  316. chunk,
  317. filename: options.fileContext
  318. ? path.relative(options.fileContext, filename)
  319. : filename,
  320. query,
  321. basename: basename(filename),
  322. contentHash: createHash("md4")
  323. .update(sourceMapString)
  324. .digest("hex")
  325. };
  326. let sourceMapFile = compilation.getPath(
  327. sourceMapFilename,
  328. pathParams
  329. );
  330. const sourceMapUrl = options.publicPath
  331. ? options.publicPath + sourceMapFile.replace(/\\/g, "/")
  332. : path
  333. .relative(path.dirname(file), sourceMapFile)
  334. .replace(/\\/g, "/");
  335. /**
  336. * Add source map url to compilation asset, if {@link currentSourceMappingURLComment} presented
  337. */
  338. if (currentSourceMappingURLComment !== false) {
  339. const asset = new ConcatSource(
  340. new RawSource(source),
  341. compilation.getPath(
  342. currentSourceMappingURLComment,
  343. Object.assign({ url: sourceMapUrl }, pathParams)
  344. )
  345. );
  346. assets[file] = asset;
  347. compilation.updateAsset(file, asset);
  348. }
  349. /**
  350. * Add source map file to compilation assets and chunk files
  351. */
  352. const asset = new RawSource(sourceMapString);
  353. assets[sourceMapFile] = asset;
  354. compilation.emitAsset(sourceMapFile, asset, {
  355. development: true
  356. });
  357. chunk.files.push(sourceMapFile);
  358. } else {
  359. if (currentSourceMappingURLComment === false) {
  360. throw new Error(
  361. "SourceMapDevToolPlugin: append can't be false when no filename is provided"
  362. );
  363. }
  364. /**
  365. * Add source map as data url to asset
  366. */
  367. const asset = new ConcatSource(
  368. new RawSource(source),
  369. currentSourceMappingURLComment
  370. .replace(/\[map\]/g, () => sourceMapString)
  371. .replace(
  372. /\[url\]/g,
  373. () =>
  374. `data:application/json;charset=utf-8;base64,${Buffer.from(
  375. sourceMapString,
  376. "utf-8"
  377. ).toString("base64")}`
  378. )
  379. );
  380. assets[file] = asset;
  381. compilation.updateAsset(file, asset);
  382. }
  383. });
  384. reportProgress(1.0);
  385. }
  386. );
  387. });
  388. }
  389. }
  390. module.exports = SourceMapDevToolPlugin;