SourceMapDevToolPlugin.js 8.7 KB

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