您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

SourceMapDevToolPlugin.js 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414
  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.getAsset(file).source;
  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. if (cachedFile === file) {
  169. compilation.updateAsset(cachedFile, cache.assets[cachedFile]);
  170. } else {
  171. compilation.emitAsset(cachedFile, cache.assets[cachedFile], {
  172. development: true
  173. });
  174. }
  175. /**
  176. * Add file to chunk, if not presented there
  177. */
  178. if (cachedFile !== file) chunk.files.push(cachedFile);
  179. }
  180. return;
  181. }
  182. reportProgress(
  183. (0.5 * idx) / files.length,
  184. file,
  185. "generate SourceMap"
  186. );
  187. /** @type {SourceMapTask | undefined} */
  188. const task = getTaskForFile(
  189. file,
  190. asset,
  191. chunk,
  192. options,
  193. compilation
  194. );
  195. if (task) {
  196. const modules = task.sourceMap.sources.map(source => {
  197. const module = compilation.findModule(source);
  198. return module || source;
  199. });
  200. for (let idx = 0; idx < modules.length; idx++) {
  201. const module = modules[idx];
  202. if (!moduleToSourceNameMapping.get(module)) {
  203. moduleToSourceNameMapping.set(
  204. module,
  205. ModuleFilenameHelpers.createFilename(
  206. module,
  207. {
  208. moduleFilenameTemplate: moduleFilenameTemplate,
  209. namespace: namespace
  210. },
  211. requestShortener
  212. )
  213. );
  214. }
  215. }
  216. task.modules = modules;
  217. tasks.push(task);
  218. }
  219. });
  220. reportProgress(0.5, "resolve sources");
  221. /** @type {Set<string>} */
  222. const usedNamesSet = new Set(moduleToSourceNameMapping.values());
  223. /** @type {Set<string>} */
  224. const conflictDetectionSet = new Set();
  225. /**
  226. * all modules in defined order (longest identifier first)
  227. * @type {Array<string | Module>}
  228. */
  229. const allModules = Array.from(moduleToSourceNameMapping.keys()).sort(
  230. (a, b) => {
  231. const ai = typeof a === "string" ? a : a.identifier();
  232. const bi = typeof b === "string" ? b : b.identifier();
  233. return ai.length - bi.length;
  234. }
  235. );
  236. // find modules with conflicting source names
  237. for (let idx = 0; idx < allModules.length; idx++) {
  238. const module = allModules[idx];
  239. let sourceName = moduleToSourceNameMapping.get(module);
  240. let hasName = conflictDetectionSet.has(sourceName);
  241. if (!hasName) {
  242. conflictDetectionSet.add(sourceName);
  243. continue;
  244. }
  245. // try the fallback name first
  246. sourceName = ModuleFilenameHelpers.createFilename(
  247. module,
  248. {
  249. moduleFilenameTemplate: fallbackModuleFilenameTemplate,
  250. namespace: namespace
  251. },
  252. requestShortener
  253. );
  254. hasName = usedNamesSet.has(sourceName);
  255. if (!hasName) {
  256. moduleToSourceNameMapping.set(module, sourceName);
  257. usedNamesSet.add(sourceName);
  258. continue;
  259. }
  260. // elsewise just append stars until we have a valid name
  261. while (hasName) {
  262. sourceName += "*";
  263. hasName = usedNamesSet.has(sourceName);
  264. }
  265. moduleToSourceNameMapping.set(module, sourceName);
  266. usedNamesSet.add(sourceName);
  267. }
  268. tasks.forEach((task, index) => {
  269. reportProgress(
  270. 0.5 + (0.5 * index) / tasks.length,
  271. task.file,
  272. "attach SourceMap"
  273. );
  274. const assets = Object.create(null);
  275. const chunk = task.chunk;
  276. const file = task.file;
  277. const asset = task.asset;
  278. const sourceMap = task.sourceMap;
  279. const source = task.source;
  280. const modules = task.modules;
  281. const moduleFilenames = modules.map(m =>
  282. moduleToSourceNameMapping.get(m)
  283. );
  284. sourceMap.sources = moduleFilenames;
  285. if (options.noSources) {
  286. sourceMap.sourcesContent = undefined;
  287. }
  288. sourceMap.sourceRoot = options.sourceRoot || "";
  289. sourceMap.file = file;
  290. assetsCache.set(asset, { file, assets });
  291. /** @type {string | false} */
  292. let currentSourceMappingURLComment = sourceMappingURLComment;
  293. if (
  294. currentSourceMappingURLComment !== false &&
  295. /\.css($|\?)/i.test(file)
  296. ) {
  297. currentSourceMappingURLComment = currentSourceMappingURLComment.replace(
  298. /^\n\/\/(.*)$/,
  299. "\n/*$1*/"
  300. );
  301. }
  302. const sourceMapString = JSON.stringify(sourceMap);
  303. if (sourceMapFilename) {
  304. let filename = file;
  305. let query = "";
  306. const idx = filename.indexOf("?");
  307. if (idx >= 0) {
  308. query = filename.substr(idx);
  309. filename = filename.substr(0, idx);
  310. }
  311. const pathParams = {
  312. chunk,
  313. filename: options.fileContext
  314. ? path.relative(options.fileContext, filename)
  315. : filename,
  316. query,
  317. basename: basename(filename),
  318. contentHash: createHash("md4")
  319. .update(sourceMapString)
  320. .digest("hex")
  321. };
  322. let sourceMapFile = compilation.getPath(
  323. sourceMapFilename,
  324. pathParams
  325. );
  326. const sourceMapUrl = options.publicPath
  327. ? options.publicPath + sourceMapFile.replace(/\\/g, "/")
  328. : path
  329. .relative(path.dirname(file), sourceMapFile)
  330. .replace(/\\/g, "/");
  331. /**
  332. * Add source map url to compilation asset, if {@link currentSourceMappingURLComment} presented
  333. */
  334. if (currentSourceMappingURLComment !== false) {
  335. const asset = new ConcatSource(
  336. new RawSource(source),
  337. compilation.getPath(
  338. currentSourceMappingURLComment,
  339. Object.assign({ url: sourceMapUrl }, pathParams)
  340. )
  341. );
  342. assets[file] = asset;
  343. compilation.updateAsset(file, asset);
  344. }
  345. /**
  346. * Add source map file to compilation assets and chunk files
  347. */
  348. const asset = new RawSource(sourceMapString);
  349. assets[sourceMapFile] = asset;
  350. compilation.emitAsset(sourceMapFile, asset, {
  351. development: true
  352. });
  353. chunk.files.push(sourceMapFile);
  354. } else {
  355. if (currentSourceMappingURLComment === false) {
  356. throw new Error(
  357. "SourceMapDevToolPlugin: append can't be false when no filename is provided"
  358. );
  359. }
  360. /**
  361. * Add source map as data url to asset
  362. */
  363. const asset = new ConcatSource(
  364. new RawSource(source),
  365. currentSourceMappingURLComment
  366. .replace(/\[map\]/g, () => sourceMapString)
  367. .replace(
  368. /\[url\]/g,
  369. () =>
  370. `data:application/json;charset=utf-8;base64,${Buffer.from(
  371. sourceMapString,
  372. "utf-8"
  373. ).toString("base64")}`
  374. )
  375. );
  376. assets[file] = asset;
  377. compilation.updateAsset(file, asset);
  378. }
  379. });
  380. reportProgress(1.0);
  381. }
  382. );
  383. });
  384. }
  385. }
  386. module.exports = SourceMapDevToolPlugin;