UNPKG

16.5 kBJavaScriptView Raw
1/*
2 MIT License http://www.opensource.org/licenses/mit-license.php
3 Author Tobias Koppers @sokra
4*/
5
6"use strict";
7
8const asyncLib = require("neo-async");
9const { validate } = require("schema-utils");
10const { ConcatSource, RawSource } = require("webpack-sources");
11const Compilation = require("./Compilation");
12const ModuleFilenameHelpers = require("./ModuleFilenameHelpers");
13const ProgressPlugin = require("./ProgressPlugin");
14const SourceMapDevToolModuleOptionsPlugin = require("./SourceMapDevToolModuleOptionsPlugin");
15const createHash = require("./util/createHash");
16const { relative, dirname } = require("./util/fs");
17const { absolutify } = require("./util/identifier");
18
19const schema = require("../schemas/plugins/SourceMapDevToolPlugin.json");
20
21/** @typedef {import("source-map").RawSourceMap} SourceMap */
22/** @typedef {import("webpack-sources").Source} Source */
23/** @typedef {import("../declarations/plugins/SourceMapDevToolPlugin").SourceMapDevToolPluginOptions} SourceMapDevToolPluginOptions */
24/** @typedef {import("./Cache").Etag} Etag */
25/** @typedef {import("./CacheFacade").ItemCacheFacade} ItemCacheFacade */
26/** @typedef {import("./Chunk")} Chunk */
27/** @typedef {import("./Compilation").AssetInfo} AssetInfo */
28/** @typedef {import("./Compiler")} Compiler */
29/** @typedef {import("./Module")} Module */
30/** @typedef {import("./util/Hash")} Hash */
31
32/**
33 * @typedef {object} SourceMapTask
34 * @property {Source} asset
35 * @property {AssetInfo} assetInfo
36 * @property {(string | Module)[]} modules
37 * @property {string} source
38 * @property {string} file
39 * @property {SourceMap} sourceMap
40 * @property {ItemCacheFacade} cacheItem cache item
41 */
42
43/**
44 * Escapes regular expression metacharacters
45 * @param {string} str String to quote
46 * @returns {string} Escaped string
47 */
48const quoteMeta = str => {
49 return str.replace(/[-[\]\\/{}()*+?.^$|]/g, "\\$&");
50};
51
52/**
53 * Creating {@link SourceMapTask} for given file
54 * @param {string} file current compiled file
55 * @param {Source} asset the asset
56 * @param {AssetInfo} assetInfo the asset info
57 * @param {SourceMapDevToolPluginOptions} options source map options
58 * @param {Compilation} compilation compilation instance
59 * @param {ItemCacheFacade} cacheItem cache item
60 * @returns {SourceMapTask | undefined} created task instance or `undefined`
61 */
62const getTaskForFile = (
63 file,
64 asset,
65 assetInfo,
66 options,
67 compilation,
68 cacheItem
69) => {
70 let source;
71 /** @type {SourceMap} */
72 let sourceMap;
73 /**
74 * Check if asset can build source map
75 */
76 if (asset.sourceAndMap) {
77 const sourceAndMap = asset.sourceAndMap(options);
78 sourceMap = /** @type {SourceMap} */ (sourceAndMap.map);
79 source = sourceAndMap.source;
80 } else {
81 sourceMap = /** @type {SourceMap} */ (asset.map(options));
82 source = asset.source();
83 }
84 if (!sourceMap || typeof source !== "string") return;
85 const context = compilation.options.context;
86 const root = compilation.compiler.root;
87 const cachedAbsolutify = absolutify.bindContextCache(context, root);
88 const modules = sourceMap.sources.map(source => {
89 if (!source.startsWith("webpack://")) return source;
90 source = cachedAbsolutify(source.slice(10));
91 const module = compilation.findModule(source);
92 return module || source;
93 });
94
95 return {
96 file,
97 asset,
98 source,
99 assetInfo,
100 sourceMap,
101 modules,
102 cacheItem
103 };
104};
105
106class SourceMapDevToolPlugin {
107 /**
108 * @param {SourceMapDevToolPluginOptions} [options] options object
109 * @throws {Error} throws error, if got more than 1 arguments
110 */
111 constructor(options = {}) {
112 validate(schema, options, {
113 name: "SourceMap DevTool Plugin",
114 baseDataPath: "options"
115 });
116
117 /** @type {string | false} */
118 this.sourceMapFilename = options.filename;
119 /** @type {string | false} */
120 this.sourceMappingURLComment =
121 options.append === false
122 ? false
123 : options.append || "\n//# source" + "MappingURL=[url]";
124 /** @type {string | Function} */
125 this.moduleFilenameTemplate =
126 options.moduleFilenameTemplate || "webpack://[namespace]/[resourcePath]";
127 /** @type {string | Function} */
128 this.fallbackModuleFilenameTemplate =
129 options.fallbackModuleFilenameTemplate ||
130 "webpack://[namespace]/[resourcePath]?[hash]";
131 /** @type {string} */
132 this.namespace = options.namespace || "";
133 /** @type {SourceMapDevToolPluginOptions} */
134 this.options = options;
135 }
136
137 /**
138 * Apply the plugin
139 * @param {Compiler} compiler compiler instance
140 * @returns {void}
141 */
142 apply(compiler) {
143 const outputFs = compiler.outputFileSystem;
144 const sourceMapFilename = this.sourceMapFilename;
145 const sourceMappingURLComment = this.sourceMappingURLComment;
146 const moduleFilenameTemplate = this.moduleFilenameTemplate;
147 const namespace = this.namespace;
148 const fallbackModuleFilenameTemplate = this.fallbackModuleFilenameTemplate;
149 const requestShortener = compiler.requestShortener;
150 const options = this.options;
151 options.test = options.test || /\.(m?js|css)($|\?)/i;
152
153 const matchObject = ModuleFilenameHelpers.matchObject.bind(
154 undefined,
155 options
156 );
157
158 compiler.hooks.compilation.tap("SourceMapDevToolPlugin", compilation => {
159 new SourceMapDevToolModuleOptionsPlugin(options).apply(compilation);
160
161 compilation.hooks.processAssets.tapAsync(
162 {
163 name: "SourceMapDevToolPlugin",
164 stage: Compilation.PROCESS_ASSETS_STAGE_DEV_TOOLING
165 },
166 (assets, callback) => {
167 const chunkGraph = compilation.chunkGraph;
168 const cache = compilation.getCache("SourceMapDevToolPlugin");
169 /** @type {Map<string | Module, string>} */
170 const moduleToSourceNameMapping = new Map();
171 /**
172 * @type {Function}
173 * @returns {void}
174 */
175 const reportProgress =
176 ProgressPlugin.getReporter(compilation.compiler) || (() => {});
177
178 /** @type {Map<string, Chunk>} */
179 const fileToChunk = new Map();
180 for (const chunk of compilation.chunks) {
181 for (const file of chunk.files) {
182 fileToChunk.set(file, chunk);
183 }
184 for (const file of chunk.auxiliaryFiles) {
185 fileToChunk.set(file, chunk);
186 }
187 }
188
189 /** @type {string[]} */
190 const files = [];
191 for (const file of Object.keys(assets)) {
192 if (matchObject(file)) {
193 files.push(file);
194 }
195 }
196
197 reportProgress(0.0);
198 /** @type {SourceMapTask[]} */
199 const tasks = [];
200 let fileIndex = 0;
201
202 asyncLib.each(
203 files,
204 (file, callback) => {
205 const asset = compilation.getAsset(file);
206 if (asset.info.related && asset.info.related.sourceMap) {
207 fileIndex++;
208 return callback();
209 }
210 const cacheItem = cache.getItemCache(
211 file,
212 cache.getLazyHashedEtag(asset.source)
213 );
214
215 cacheItem.get((err, cacheEntry) => {
216 if (err) {
217 return callback(err);
218 }
219 /**
220 * If presented in cache, reassigns assets. Cache assets already have source maps.
221 */
222 if (cacheEntry) {
223 const { assets, assetsInfo } = cacheEntry;
224 for (const cachedFile of Object.keys(assets)) {
225 if (cachedFile === file) {
226 compilation.updateAsset(
227 cachedFile,
228 assets[cachedFile],
229 assetsInfo[cachedFile]
230 );
231 } else {
232 compilation.emitAsset(
233 cachedFile,
234 assets[cachedFile],
235 assetsInfo[cachedFile]
236 );
237 }
238 /**
239 * Add file to chunk, if not presented there
240 */
241 if (cachedFile !== file) {
242 const chunk = fileToChunk.get(file);
243 if (chunk !== undefined)
244 chunk.auxiliaryFiles.add(cachedFile);
245 }
246 }
247
248 reportProgress(
249 (0.5 * ++fileIndex) / files.length,
250 file,
251 "restored cached SourceMap"
252 );
253
254 return callback();
255 }
256
257 reportProgress(
258 (0.5 * fileIndex) / files.length,
259 file,
260 "generate SourceMap"
261 );
262
263 /** @type {SourceMapTask | undefined} */
264 const task = getTaskForFile(
265 file,
266 asset.source,
267 asset.info,
268 options,
269 compilation,
270 cacheItem
271 );
272
273 if (task) {
274 const modules = task.modules;
275
276 for (let idx = 0; idx < modules.length; idx++) {
277 const module = modules[idx];
278 if (!moduleToSourceNameMapping.get(module)) {
279 moduleToSourceNameMapping.set(
280 module,
281 ModuleFilenameHelpers.createFilename(
282 module,
283 {
284 moduleFilenameTemplate: moduleFilenameTemplate,
285 namespace: namespace
286 },
287 {
288 requestShortener,
289 chunkGraph
290 }
291 )
292 );
293 }
294 }
295
296 tasks.push(task);
297 }
298
299 reportProgress(
300 (0.5 * ++fileIndex) / files.length,
301 file,
302 "generated SourceMap"
303 );
304
305 callback();
306 });
307 },
308 err => {
309 if (err) {
310 return callback(err);
311 }
312
313 reportProgress(0.5, "resolve sources");
314 /** @type {Set<string>} */
315 const usedNamesSet = new Set(moduleToSourceNameMapping.values());
316 /** @type {Set<string>} */
317 const conflictDetectionSet = new Set();
318
319 /**
320 * all modules in defined order (longest identifier first)
321 * @type {Array<string | Module>}
322 */
323 const allModules = Array.from(
324 moduleToSourceNameMapping.keys()
325 ).sort((a, b) => {
326 const ai = typeof a === "string" ? a : a.identifier();
327 const bi = typeof b === "string" ? b : b.identifier();
328 return ai.length - bi.length;
329 });
330
331 // find modules with conflicting source names
332 for (let idx = 0; idx < allModules.length; idx++) {
333 const module = allModules[idx];
334 let sourceName = moduleToSourceNameMapping.get(module);
335 let hasName = conflictDetectionSet.has(sourceName);
336 if (!hasName) {
337 conflictDetectionSet.add(sourceName);
338 continue;
339 }
340
341 // try the fallback name first
342 sourceName = ModuleFilenameHelpers.createFilename(
343 module,
344 {
345 moduleFilenameTemplate: fallbackModuleFilenameTemplate,
346 namespace: namespace
347 },
348 {
349 requestShortener,
350 chunkGraph
351 }
352 );
353 hasName = usedNamesSet.has(sourceName);
354 if (!hasName) {
355 moduleToSourceNameMapping.set(module, sourceName);
356 usedNamesSet.add(sourceName);
357 continue;
358 }
359
360 // otherwise just append stars until we have a valid name
361 while (hasName) {
362 sourceName += "*";
363 hasName = usedNamesSet.has(sourceName);
364 }
365 moduleToSourceNameMapping.set(module, sourceName);
366 usedNamesSet.add(sourceName);
367 }
368
369 let taskIndex = 0;
370
371 asyncLib.each(
372 tasks,
373 (task, callback) => {
374 const assets = Object.create(null);
375 const assetsInfo = Object.create(null);
376 const file = task.file;
377 const chunk = fileToChunk.get(file);
378 const sourceMap = task.sourceMap;
379 const source = task.source;
380 const modules = task.modules;
381
382 reportProgress(
383 0.5 + (0.5 * taskIndex) / tasks.length,
384 file,
385 "attach SourceMap"
386 );
387
388 const moduleFilenames = modules.map(m =>
389 moduleToSourceNameMapping.get(m)
390 );
391 sourceMap.sources = moduleFilenames;
392 if (options.noSources) {
393 sourceMap.sourcesContent = undefined;
394 }
395 sourceMap.sourceRoot = options.sourceRoot || "";
396 sourceMap.file = file;
397 const usesContentHash =
398 sourceMapFilename &&
399 /\[contenthash(:\w+)?\]/.test(sourceMapFilename);
400
401 // If SourceMap and asset uses contenthash, avoid a circular dependency by hiding hash in `file`
402 if (usesContentHash && task.assetInfo.contenthash) {
403 const contenthash = task.assetInfo.contenthash;
404 let pattern;
405 if (Array.isArray(contenthash)) {
406 pattern = contenthash.map(quoteMeta).join("|");
407 } else {
408 pattern = quoteMeta(contenthash);
409 }
410 sourceMap.file = sourceMap.file.replace(
411 new RegExp(pattern, "g"),
412 m => "x".repeat(m.length)
413 );
414 }
415
416 /** @type {string | false} */
417 let currentSourceMappingURLComment = sourceMappingURLComment;
418 if (
419 currentSourceMappingURLComment !== false &&
420 /\.css($|\?)/i.test(file)
421 ) {
422 currentSourceMappingURLComment = currentSourceMappingURLComment.replace(
423 /^\n\/\/(.*)$/,
424 "\n/*$1*/"
425 );
426 }
427 const sourceMapString = JSON.stringify(sourceMap);
428 if (sourceMapFilename) {
429 let filename = file;
430 const sourceMapContentHash =
431 usesContentHash &&
432 /** @type {string} */ (createHash("md4")
433 .update(sourceMapString)
434 .digest("hex"));
435 const pathParams = {
436 chunk,
437 filename: options.fileContext
438 ? relative(
439 outputFs,
440 `/${options.fileContext}`,
441 `/${filename}`
442 )
443 : filename,
444 contentHash: sourceMapContentHash
445 };
446 const {
447 path: sourceMapFile,
448 info: sourceMapInfo
449 } = compilation.getPathWithInfo(
450 sourceMapFilename,
451 pathParams
452 );
453 const sourceMapUrl = options.publicPath
454 ? options.publicPath + sourceMapFile
455 : relative(
456 outputFs,
457 dirname(outputFs, `/${file}`),
458 `/${sourceMapFile}`
459 );
460 /** @type {Source} */
461 let asset = new RawSource(source);
462 if (currentSourceMappingURLComment !== false) {
463 // Add source map url to compilation asset, if currentSourceMappingURLComment is set
464 asset = new ConcatSource(
465 asset,
466 compilation.getPath(
467 currentSourceMappingURLComment,
468 Object.assign({ url: sourceMapUrl }, pathParams)
469 )
470 );
471 }
472 const assetInfo = {
473 related: { sourceMap: sourceMapFile }
474 };
475 assets[file] = asset;
476 assetsInfo[file] = assetInfo;
477 compilation.updateAsset(file, asset, assetInfo);
478 // Add source map file to compilation assets and chunk files
479 const sourceMapAsset = new RawSource(sourceMapString);
480 const sourceMapAssetInfo = {
481 ...sourceMapInfo,
482 development: true
483 };
484 assets[sourceMapFile] = sourceMapAsset;
485 assetsInfo[sourceMapFile] = sourceMapAssetInfo;
486 compilation.emitAsset(
487 sourceMapFile,
488 sourceMapAsset,
489 sourceMapAssetInfo
490 );
491 if (chunk !== undefined)
492 chunk.auxiliaryFiles.add(sourceMapFile);
493 } else {
494 if (currentSourceMappingURLComment === false) {
495 throw new Error(
496 "SourceMapDevToolPlugin: append can't be false when no filename is provided"
497 );
498 }
499 /**
500 * Add source map as data url to asset
501 */
502 const asset = new ConcatSource(
503 new RawSource(source),
504 currentSourceMappingURLComment
505 .replace(/\[map\]/g, () => sourceMapString)
506 .replace(
507 /\[url\]/g,
508 () =>
509 `data:application/json;charset=utf-8;base64,${Buffer.from(
510 sourceMapString,
511 "utf-8"
512 ).toString("base64")}`
513 )
514 );
515 assets[file] = asset;
516 assetsInfo[file] = undefined;
517 compilation.updateAsset(file, asset);
518 }
519
520 task.cacheItem.store({ assets, assetsInfo }, err => {
521 reportProgress(
522 0.5 + (0.5 * ++taskIndex) / tasks.length,
523 task.file,
524 "attached SourceMap"
525 );
526
527 if (err) {
528 return callback(err);
529 }
530 callback();
531 });
532 },
533 err => {
534 reportProgress(1.0);
535 callback(err);
536 }
537 );
538 }
539 );
540 }
541 );
542 });
543 }
544}
545
546module.exports = SourceMapDevToolPlugin;