UNPKG

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