UNPKG

12.5 kBJavaScriptView Raw
1/*
2 MIT License http://www.opensource.org/licenses/mit-license.php
3 Author Tobias Koppers @sokra
4*/
5"use strict";
6
7const path = require("path");
8const { ConcatSource, RawSource } = require("webpack-sources");
9const ModuleFilenameHelpers = require("./ModuleFilenameHelpers");
10const SourceMapDevToolModuleOptionsPlugin = require("./SourceMapDevToolModuleOptionsPlugin");
11const createHash = require("./util/createHash");
12
13const validateOptions = require("schema-utils");
14const schema = require("../schemas/plugins/SourceMapDevToolPlugin.json");
15
16/** @typedef {import("../declarations/plugins/SourceMapDevToolPlugin").SourceMapDevToolPluginOptions} SourceMapDevToolPluginOptions */
17/** @typedef {import("./Chunk")} Chunk */
18/** @typedef {import("webpack-sources").Source} Source */
19/** @typedef {import("source-map").RawSourceMap} SourceMap */
20/** @typedef {import("./Module")} Module */
21/** @typedef {import("./Compilation")} Compilation */
22/** @typedef {import("./Compiler")} Compiler */
23/** @typedef {import("./Compilation")} SourceMapDefinition */
24
25/**
26 * @typedef {object} SourceMapTask
27 * @property {Source} asset
28 * @property {Array<string | Module>} [modules]
29 * @property {string} source
30 * @property {string} file
31 * @property {SourceMap} sourceMap
32 * @property {Chunk} chunk
33 */
34
35/**
36 * @param {string} name file path
37 * @returns {string} file name
38 */
39const basename = name => {
40 if (!name.includes("/")) return name;
41 return name.substr(name.lastIndexOf("/") + 1);
42};
43
44/**
45 * @type {WeakMap<Source, {file: string, assets: {[k: string]: ConcatSource | RawSource}}>}
46 */
47const assetsCache = new WeakMap();
48
49/**
50 * Creating {@link SourceMapTask} for given file
51 * @param {string} file current compiled file
52 * @param {Source} asset the asset
53 * @param {Chunk} chunk related chunk
54 * @param {SourceMapDevToolPluginOptions} options source map options
55 * @param {Compilation} compilation compilation instance
56 * @returns {SourceMapTask | undefined} created task instance or `undefined`
57 */
58const getTaskForFile = (file, asset, chunk, options, compilation) => {
59 let source, sourceMap;
60 /**
61 * Check if asset can build source map
62 */
63 if (asset.sourceAndMap) {
64 const sourceAndMap = asset.sourceAndMap(options);
65 sourceMap = sourceAndMap.map;
66 source = sourceAndMap.source;
67 } else {
68 sourceMap = asset.map(options);
69 source = asset.source();
70 }
71 if (sourceMap) {
72 return {
73 chunk,
74 file,
75 asset,
76 source,
77 sourceMap,
78 modules: undefined
79 };
80 }
81};
82
83class SourceMapDevToolPlugin {
84 /**
85 * @param {SourceMapDevToolPluginOptions} [options] options object
86 * @throws {Error} throws error, if got more than 1 arguments
87 */
88 constructor(options) {
89 if (arguments.length > 1) {
90 throw new Error(
91 "SourceMapDevToolPlugin only takes one argument (pass an options object)"
92 );
93 }
94
95 if (!options) options = {};
96
97 validateOptions(schema, options, "SourceMap DevTool Plugin");
98
99 /** @type {string | false} */
100 this.sourceMapFilename = options.filename;
101 /** @type {string | false} */
102 this.sourceMappingURLComment =
103 options.append === false
104 ? false
105 : options.append || "\n//# sourceMappingURL=[url]";
106 /** @type {string | Function} */
107 this.moduleFilenameTemplate =
108 options.moduleFilenameTemplate || "webpack://[namespace]/[resourcePath]";
109 /** @type {string | Function} */
110 this.fallbackModuleFilenameTemplate =
111 options.fallbackModuleFilenameTemplate ||
112 "webpack://[namespace]/[resourcePath]?[hash]";
113 /** @type {string} */
114 this.namespace = options.namespace || "";
115 /** @type {SourceMapDevToolPluginOptions} */
116 this.options = options;
117 }
118
119 /**
120 * Apply compiler
121 * @param {Compiler} compiler compiler instance
122 * @returns {void}
123 */
124 apply(compiler) {
125 const sourceMapFilename = this.sourceMapFilename;
126 const sourceMappingURLComment = this.sourceMappingURLComment;
127 const moduleFilenameTemplate = this.moduleFilenameTemplate;
128 const namespace = this.namespace;
129 const fallbackModuleFilenameTemplate = this.fallbackModuleFilenameTemplate;
130 const requestShortener = compiler.requestShortener;
131 const options = this.options;
132 options.test = options.test || /\.(m?js|css)($|\?)/i;
133
134 const matchObject = ModuleFilenameHelpers.matchObject.bind(
135 undefined,
136 options
137 );
138
139 compiler.hooks.compilation.tap("SourceMapDevToolPlugin", compilation => {
140 new SourceMapDevToolModuleOptionsPlugin(options).apply(compilation);
141
142 compilation.hooks.afterOptimizeChunkAssets.tap(
143 /** @type {TODO} */
144 ({ name: "SourceMapDevToolPlugin", context: true }),
145 /**
146 * @param {object} context hook context
147 * @param {Array<Chunk>} chunks resulted chunks
148 * @throws {Error} throws error, if `sourceMapFilename === false && sourceMappingURLComment === false`
149 * @returns {void}
150 */
151 (context, chunks) => {
152 /** @type {Map<string | Module, string>} */
153 const moduleToSourceNameMapping = new Map();
154 /**
155 * @type {Function}
156 * @returns {void}
157 */
158 const reportProgress =
159 context && context.reportProgress
160 ? context.reportProgress
161 : () => {};
162
163 const files = [];
164 for (const chunk of chunks) {
165 for (const file of chunk.files) {
166 if (matchObject(file)) {
167 files.push({
168 file,
169 chunk
170 });
171 }
172 }
173 }
174
175 reportProgress(0.0);
176 const tasks = [];
177 files.forEach(({ file, chunk }, idx) => {
178 const asset = compilation.getAsset(file).source;
179 const cache = assetsCache.get(asset);
180 /**
181 * If presented in cache, reassigns assets. Cache assets already have source maps.
182 */
183 if (cache && cache.file === file) {
184 for (const cachedFile in cache.assets) {
185 if (cachedFile === file) {
186 compilation.updateAsset(cachedFile, cache.assets[cachedFile]);
187 } else {
188 compilation.emitAsset(cachedFile, cache.assets[cachedFile], {
189 development: true
190 });
191 }
192 /**
193 * Add file to chunk, if not presented there
194 */
195 if (cachedFile !== file) chunk.files.push(cachedFile);
196 }
197 return;
198 }
199
200 reportProgress(
201 (0.5 * idx) / files.length,
202 file,
203 "generate SourceMap"
204 );
205 /** @type {SourceMapTask | undefined} */
206 const task = getTaskForFile(
207 file,
208 asset,
209 chunk,
210 options,
211 compilation
212 );
213
214 if (task) {
215 const modules = task.sourceMap.sources.map(source => {
216 const module = compilation.findModule(source);
217 return module || source;
218 });
219
220 for (let idx = 0; idx < modules.length; idx++) {
221 const module = modules[idx];
222 if (!moduleToSourceNameMapping.get(module)) {
223 moduleToSourceNameMapping.set(
224 module,
225 ModuleFilenameHelpers.createFilename(
226 module,
227 {
228 moduleFilenameTemplate: moduleFilenameTemplate,
229 namespace: namespace
230 },
231 requestShortener
232 )
233 );
234 }
235 }
236
237 task.modules = modules;
238
239 tasks.push(task);
240 }
241 });
242
243 reportProgress(0.5, "resolve sources");
244 /** @type {Set<string>} */
245 const usedNamesSet = new Set(moduleToSourceNameMapping.values());
246 /** @type {Set<string>} */
247 const conflictDetectionSet = new Set();
248
249 /**
250 * all modules in defined order (longest identifier first)
251 * @type {Array<string | Module>}
252 */
253 const allModules = Array.from(moduleToSourceNameMapping.keys()).sort(
254 (a, b) => {
255 const ai = typeof a === "string" ? a : a.identifier();
256 const bi = typeof b === "string" ? b : b.identifier();
257 return ai.length - bi.length;
258 }
259 );
260
261 // find modules with conflicting source names
262 for (let idx = 0; idx < allModules.length; idx++) {
263 const module = allModules[idx];
264 let sourceName = moduleToSourceNameMapping.get(module);
265 let hasName = conflictDetectionSet.has(sourceName);
266 if (!hasName) {
267 conflictDetectionSet.add(sourceName);
268 continue;
269 }
270
271 // try the fallback name first
272 sourceName = ModuleFilenameHelpers.createFilename(
273 module,
274 {
275 moduleFilenameTemplate: fallbackModuleFilenameTemplate,
276 namespace: namespace
277 },
278 requestShortener
279 );
280 hasName = usedNamesSet.has(sourceName);
281 if (!hasName) {
282 moduleToSourceNameMapping.set(module, sourceName);
283 usedNamesSet.add(sourceName);
284 continue;
285 }
286
287 // elsewise just append stars until we have a valid name
288 while (hasName) {
289 sourceName += "*";
290 hasName = usedNamesSet.has(sourceName);
291 }
292 moduleToSourceNameMapping.set(module, sourceName);
293 usedNamesSet.add(sourceName);
294 }
295 tasks.forEach((task, index) => {
296 reportProgress(
297 0.5 + (0.5 * index) / tasks.length,
298 task.file,
299 "attach SourceMap"
300 );
301 const assets = Object.create(null);
302 const chunk = task.chunk;
303 const file = task.file;
304 const asset = task.asset;
305 const sourceMap = task.sourceMap;
306 const source = task.source;
307 const modules = task.modules;
308 const moduleFilenames = modules.map(m =>
309 moduleToSourceNameMapping.get(m)
310 );
311 sourceMap.sources = moduleFilenames;
312 if (options.noSources) {
313 sourceMap.sourcesContent = undefined;
314 }
315 sourceMap.sourceRoot = options.sourceRoot || "";
316 sourceMap.file = file;
317 assetsCache.set(asset, { file, assets });
318 /** @type {string | false} */
319 let currentSourceMappingURLComment = sourceMappingURLComment;
320 if (
321 currentSourceMappingURLComment !== false &&
322 /\.css($|\?)/i.test(file)
323 ) {
324 currentSourceMappingURLComment = currentSourceMappingURLComment.replace(
325 /^\n\/\/(.*)$/,
326 "\n/*$1*/"
327 );
328 }
329 const sourceMapString = JSON.stringify(sourceMap);
330 if (sourceMapFilename) {
331 let filename = file;
332 let query = "";
333 const idx = filename.indexOf("?");
334 if (idx >= 0) {
335 query = filename.substr(idx);
336 filename = filename.substr(0, idx);
337 }
338 const pathParams = {
339 chunk,
340 filename: options.fileContext
341 ? path.relative(options.fileContext, filename)
342 : filename,
343 query,
344 basename: basename(filename),
345 contentHash: createHash("md4")
346 .update(sourceMapString)
347 .digest("hex")
348 };
349 let sourceMapFile = compilation.getPath(
350 sourceMapFilename,
351 pathParams
352 );
353 const sourceMapUrl = options.publicPath
354 ? options.publicPath + sourceMapFile.replace(/\\/g, "/")
355 : path
356 .relative(path.dirname(file), sourceMapFile)
357 .replace(/\\/g, "/");
358 /**
359 * Add source map url to compilation asset, if {@link currentSourceMappingURLComment} presented
360 */
361 if (currentSourceMappingURLComment !== false) {
362 const asset = new ConcatSource(
363 new RawSource(source),
364 compilation.getPath(
365 currentSourceMappingURLComment,
366 Object.assign({ url: sourceMapUrl }, pathParams)
367 )
368 );
369 assets[file] = asset;
370 compilation.updateAsset(file, asset);
371 }
372 /**
373 * Add source map file to compilation assets and chunk files
374 */
375 const asset = new RawSource(sourceMapString);
376 assets[sourceMapFile] = asset;
377 compilation.emitAsset(sourceMapFile, asset, {
378 development: true
379 });
380 chunk.files.push(sourceMapFile);
381 } else {
382 if (currentSourceMappingURLComment === false) {
383 throw new Error(
384 "SourceMapDevToolPlugin: append can't be false when no filename is provided"
385 );
386 }
387 /**
388 * Add source map as data url to asset
389 */
390 const asset = new ConcatSource(
391 new RawSource(source),
392 currentSourceMappingURLComment
393 .replace(/\[map\]/g, () => sourceMapString)
394 .replace(
395 /\[url\]/g,
396 () =>
397 `data:application/json;charset=utf-8;base64,${Buffer.from(
398 sourceMapString,
399 "utf-8"
400 ).toString("base64")}`
401 )
402 );
403 assets[file] = asset;
404 compilation.updateAsset(file, asset);
405 }
406 });
407 reportProgress(1.0);
408 }
409 );
410 });
411 }
412}
413
414module.exports = SourceMapDevToolPlugin;