SourceMapDevToolPlugin.js 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315
  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. const basename = name => {
  15. if (!name.includes("/")) return name;
  16. return name.substr(name.lastIndexOf("/") + 1);
  17. };
  18. const assetsCache = new WeakMap();
  19. const getTaskForFile = (file, chunk, options, compilation) => {
  20. const asset = compilation.assets[file];
  21. const cache = assetsCache.get(asset);
  22. if (cache && cache.file === file) {
  23. for (const cachedFile in cache.assets) {
  24. compilation.assets[cachedFile] = cache.assets[cachedFile];
  25. if (cachedFile !== file) chunk.files.push(cachedFile);
  26. }
  27. return;
  28. }
  29. let source, sourceMap;
  30. if (asset.sourceAndMap) {
  31. const sourceAndMap = asset.sourceAndMap(options);
  32. sourceMap = sourceAndMap.map;
  33. source = sourceAndMap.source;
  34. } else {
  35. sourceMap = asset.map(options);
  36. source = asset.source();
  37. }
  38. if (sourceMap) {
  39. return {
  40. chunk,
  41. file,
  42. asset,
  43. source,
  44. sourceMap,
  45. modules: undefined
  46. };
  47. }
  48. };
  49. class SourceMapDevToolPlugin {
  50. /**
  51. * @param {SourceMapDevToolPluginOptions=} options options object
  52. */
  53. constructor(options) {
  54. if (arguments.length > 1) {
  55. throw new Error(
  56. "SourceMapDevToolPlugin only takes one argument (pass an options object)"
  57. );
  58. }
  59. if (!options) options = {};
  60. validateOptions(schema, options, "SourceMap DevTool Plugin");
  61. this.sourceMapFilename = options.filename;
  62. /** @type {string | false} */
  63. this.sourceMappingURLComment =
  64. options.append === false
  65. ? false
  66. : options.append || "\n//# sourceMappingURL=[url]";
  67. this.moduleFilenameTemplate =
  68. options.moduleFilenameTemplate || "webpack://[namespace]/[resourcePath]";
  69. this.fallbackModuleFilenameTemplate =
  70. options.fallbackModuleFilenameTemplate ||
  71. "webpack://[namespace]/[resourcePath]?[hash]";
  72. this.namespace = options.namespace || "";
  73. this.options = options;
  74. }
  75. apply(compiler) {
  76. const sourceMapFilename = this.sourceMapFilename;
  77. const sourceMappingURLComment = this.sourceMappingURLComment;
  78. const moduleFilenameTemplate = this.moduleFilenameTemplate;
  79. const namespace = this.namespace;
  80. const fallbackModuleFilenameTemplate = this.fallbackModuleFilenameTemplate;
  81. const requestShortener = compiler.requestShortener;
  82. const options = this.options;
  83. options.test = options.test || /\.(m?js|css)($|\?)/i;
  84. const matchObject = ModuleFilenameHelpers.matchObject.bind(
  85. undefined,
  86. options
  87. );
  88. compiler.hooks.compilation.tap("SourceMapDevToolPlugin", compilation => {
  89. new SourceMapDevToolModuleOptionsPlugin(options).apply(compilation);
  90. compilation.hooks.afterOptimizeChunkAssets.tap(
  91. {
  92. name: "SourceMapDevToolPlugin",
  93. context: true
  94. },
  95. (context, chunks) => {
  96. const moduleToSourceNameMapping = new Map();
  97. const reportProgress =
  98. context && context.reportProgress
  99. ? context.reportProgress
  100. : () => {};
  101. const files = [];
  102. for (const chunk of chunks) {
  103. for (const file of chunk.files) {
  104. if (matchObject(file)) {
  105. files.push({
  106. file,
  107. chunk
  108. });
  109. }
  110. }
  111. }
  112. reportProgress(0.0);
  113. const tasks = [];
  114. files.forEach(({ file, chunk }, idx) => {
  115. reportProgress(
  116. (0.5 * idx) / files.length,
  117. file,
  118. "generate SourceMap"
  119. );
  120. const task = getTaskForFile(file, chunk, options, compilation);
  121. if (task) {
  122. const modules = task.sourceMap.sources.map(source => {
  123. const module = compilation.findModule(source);
  124. return module || source;
  125. });
  126. for (let idx = 0; idx < modules.length; idx++) {
  127. const module = modules[idx];
  128. if (!moduleToSourceNameMapping.get(module)) {
  129. moduleToSourceNameMapping.set(
  130. module,
  131. ModuleFilenameHelpers.createFilename(
  132. module,
  133. {
  134. moduleFilenameTemplate: moduleFilenameTemplate,
  135. namespace: namespace
  136. },
  137. requestShortener
  138. )
  139. );
  140. }
  141. }
  142. task.modules = modules;
  143. tasks.push(task);
  144. }
  145. });
  146. reportProgress(0.5, "resolve sources");
  147. const usedNamesSet = new Set(moduleToSourceNameMapping.values());
  148. const conflictDetectionSet = new Set();
  149. // all modules in defined order (longest identifier first)
  150. const allModules = Array.from(moduleToSourceNameMapping.keys()).sort(
  151. (a, b) => {
  152. const ai = typeof a === "string" ? a : a.identifier();
  153. const bi = typeof b === "string" ? b : b.identifier();
  154. return ai.length - bi.length;
  155. }
  156. );
  157. // find modules with conflicting source names
  158. for (let idx = 0; idx < allModules.length; idx++) {
  159. const module = allModules[idx];
  160. let sourceName = moduleToSourceNameMapping.get(module);
  161. let hasName = conflictDetectionSet.has(sourceName);
  162. if (!hasName) {
  163. conflictDetectionSet.add(sourceName);
  164. continue;
  165. }
  166. // try the fallback name first
  167. sourceName = ModuleFilenameHelpers.createFilename(
  168. module,
  169. {
  170. moduleFilenameTemplate: fallbackModuleFilenameTemplate,
  171. namespace: namespace
  172. },
  173. requestShortener
  174. );
  175. hasName = usedNamesSet.has(sourceName);
  176. if (!hasName) {
  177. moduleToSourceNameMapping.set(module, sourceName);
  178. usedNamesSet.add(sourceName);
  179. continue;
  180. }
  181. // elsewise just append stars until we have a valid name
  182. while (hasName) {
  183. sourceName += "*";
  184. hasName = usedNamesSet.has(sourceName);
  185. }
  186. moduleToSourceNameMapping.set(module, sourceName);
  187. usedNamesSet.add(sourceName);
  188. }
  189. tasks.forEach((task, index) => {
  190. reportProgress(
  191. 0.5 + (0.5 * index) / tasks.length,
  192. task.file,
  193. "attach SourceMap"
  194. );
  195. const assets = Object.create(null);
  196. const chunk = task.chunk;
  197. const file = task.file;
  198. const asset = task.asset;
  199. const sourceMap = task.sourceMap;
  200. const source = task.source;
  201. const modules = task.modules;
  202. const moduleFilenames = modules.map(m =>
  203. moduleToSourceNameMapping.get(m)
  204. );
  205. sourceMap.sources = moduleFilenames;
  206. if (options.noSources) {
  207. sourceMap.sourcesContent = undefined;
  208. }
  209. sourceMap.sourceRoot = options.sourceRoot || "";
  210. sourceMap.file = file;
  211. assetsCache.set(asset, { file, assets });
  212. /** @type {string | false} */
  213. let currentSourceMappingURLComment = sourceMappingURLComment;
  214. if (
  215. currentSourceMappingURLComment !== false &&
  216. /\.css($|\?)/i.test(file)
  217. ) {
  218. currentSourceMappingURLComment = currentSourceMappingURLComment.replace(
  219. /^\n\/\/(.*)$/,
  220. "\n/*$1*/"
  221. );
  222. }
  223. const sourceMapString = JSON.stringify(sourceMap);
  224. if (sourceMapFilename) {
  225. let filename = file;
  226. let query = "";
  227. const idx = filename.indexOf("?");
  228. if (idx >= 0) {
  229. query = filename.substr(idx);
  230. filename = filename.substr(0, idx);
  231. }
  232. let sourceMapFile = compilation.getPath(sourceMapFilename, {
  233. chunk,
  234. filename: options.fileContext
  235. ? path.relative(options.fileContext, filename)
  236. : filename,
  237. query,
  238. basename: basename(filename),
  239. contentHash: createHash("md4")
  240. .update(sourceMapString)
  241. .digest("hex")
  242. });
  243. const sourceMapUrl = options.publicPath
  244. ? options.publicPath + sourceMapFile.replace(/\\/g, "/")
  245. : path
  246. .relative(path.dirname(file), sourceMapFile)
  247. .replace(/\\/g, "/");
  248. if (currentSourceMappingURLComment !== false) {
  249. assets[file] = compilation.assets[file] = new ConcatSource(
  250. new RawSource(source),
  251. currentSourceMappingURLComment.replace(
  252. /\[url\]/g,
  253. sourceMapUrl
  254. )
  255. );
  256. }
  257. assets[sourceMapFile] = compilation.assets[
  258. sourceMapFile
  259. ] = new RawSource(sourceMapString);
  260. chunk.files.push(sourceMapFile);
  261. } else {
  262. if (currentSourceMappingURLComment === false) {
  263. throw new Error(
  264. "SourceMapDevToolPlugin: append can't be false when no filename is provided"
  265. );
  266. }
  267. assets[file] = compilation.assets[file] = new ConcatSource(
  268. new RawSource(source),
  269. currentSourceMappingURLComment
  270. .replace(/\[map\]/g, () => sourceMapString)
  271. .replace(
  272. /\[url\]/g,
  273. () =>
  274. `data:application/json;charset=utf-8;base64,${Buffer.from(
  275. sourceMapString,
  276. "utf-8"
  277. ).toString("base64")}`
  278. )
  279. );
  280. }
  281. });
  282. reportProgress(1.0);
  283. }
  284. );
  285. });
  286. }
  287. }
  288. module.exports = SourceMapDevToolPlugin;