SourceMapDevToolPlugin.js 12 KB

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