Projekt

Obecné

Profil

Stáhnout (12.4 KB) Statistiky
| Větev: | Revize:
1
/*
2
	MIT License http://www.opensource.org/licenses/mit-license.php
3
	Author Tobias Koppers @sokra
4
*/
5
"use strict";
6

    
7
const path = require("path");
8
const { ConcatSource, RawSource } = require("webpack-sources");
9
const ModuleFilenameHelpers = require("./ModuleFilenameHelpers");
10
const SourceMapDevToolModuleOptionsPlugin = require("./SourceMapDevToolModuleOptionsPlugin");
11
const createHash = require("./util/createHash");
12
const { absolutify } = require("./util/identifier");
13

    
14
const validateOptions = require("schema-utils");
15
const schema = require("../schemas/plugins/SourceMapDevToolPlugin.json");
16

    
17
/** @typedef {import("../declarations/plugins/SourceMapDevToolPlugin").SourceMapDevToolPluginOptions} SourceMapDevToolPluginOptions */
18
/** @typedef {import("./Chunk")} Chunk */
19
/** @typedef {import("webpack-sources").Source} Source */
20
/** @typedef {import("source-map").RawSourceMap} SourceMap */
21
/** @typedef {import("./Module")} Module */
22
/** @typedef {import("./Compilation")} Compilation */
23
/** @typedef {import("./Compiler")} Compiler */
24
/** @typedef {import("./Compilation")} SourceMapDefinition */
25

    
26
/**
27
 * @typedef {object} SourceMapTask
28
 * @property {Source} asset
29
 * @property {Array<string | Module>} [modules]
30
 * @property {string} source
31
 * @property {string} file
32
 * @property {SourceMap} sourceMap
33
 * @property {Chunk} chunk
34
 */
35

    
36
/**
37
 * @param {string} name file path
38
 * @returns {string} file name
39
 */
40
const basename = name => {
41
	if (!name.includes("/")) return name;
42
	return name.substr(name.lastIndexOf("/") + 1);
43
};
44

    
45
/**
46
 * @type {WeakMap<Source, {file: string, assets: {[k: string]: ConcatSource | RawSource}}>}
47
 */
48
const assetsCache = new WeakMap();
49

    
50
/**
51
 * Creating {@link SourceMapTask} for given file
52
 * @param {string} file current compiled file
53
 * @param {Source} asset the asset
54
 * @param {Chunk} chunk related chunk
55
 * @param {SourceMapDevToolPluginOptions} options source map options
56
 * @param {Compilation} compilation compilation instance
57
 * @returns {SourceMapTask | undefined} created task instance or `undefined`
58
 */
59
const getTaskForFile = (file, asset, chunk, options, compilation) => {
60
	let source, sourceMap;
61
	/**
62
	 * Check if asset can build source map
63
	 */
64
	if (asset.sourceAndMap) {
65
		const sourceAndMap = asset.sourceAndMap(options);
66
		sourceMap = sourceAndMap.map;
67
		source = sourceAndMap.source;
68
	} else {
69
		sourceMap = asset.map(options);
70
		source = asset.source();
71
	}
72
	if (!sourceMap || typeof source !== "string") return;
73
	const context = compilation.options.context;
74
	const modules = sourceMap.sources.map(source => {
75
		if (source.startsWith("webpack://")) {
76
			source = absolutify(context, source.slice(10));
77
		}
78
		const module = compilation.findModule(source);
79
		return module || source;
80
	});
81

    
82
	return {
83
		chunk,
84
		file,
85
		asset,
86
		source,
87
		sourceMap,
88
		modules
89
	};
90
};
91

    
92
class SourceMapDevToolPlugin {
93
	/**
94
	 * @param {SourceMapDevToolPluginOptions} [options] options object
95
	 * @throws {Error} throws error, if got more than 1 arguments
96
	 */
97
	constructor(options) {
98
		if (arguments.length > 1) {
99
			throw new Error(
100
				"SourceMapDevToolPlugin only takes one argument (pass an options object)"
101
			);
102
		}
103

    
104
		if (!options) options = {};
105

    
106
		validateOptions(schema, options, "SourceMap DevTool Plugin");
107

    
108
		/** @type {string | false} */
109
		this.sourceMapFilename = options.filename;
110
		/** @type {string | false} */
111
		this.sourceMappingURLComment =
112
			options.append === false
113
				? false
114
				: options.append || "\n//# sourceMappingURL=[url]";
115
		/** @type {string | Function} */
116
		this.moduleFilenameTemplate =
117
			options.moduleFilenameTemplate || "webpack://[namespace]/[resourcePath]";
118
		/** @type {string | Function} */
119
		this.fallbackModuleFilenameTemplate =
120
			options.fallbackModuleFilenameTemplate ||
121
			"webpack://[namespace]/[resourcePath]?[hash]";
122
		/** @type {string} */
123
		this.namespace = options.namespace || "";
124
		/** @type {SourceMapDevToolPluginOptions} */
125
		this.options = options;
126
	}
127

    
128
	/**
129
	 * Apply compiler
130
	 * @param {Compiler} compiler compiler instance
131
	 * @returns {void}
132
	 */
133
	apply(compiler) {
134
		const sourceMapFilename = this.sourceMapFilename;
135
		const sourceMappingURLComment = this.sourceMappingURLComment;
136
		const moduleFilenameTemplate = this.moduleFilenameTemplate;
137
		const namespace = this.namespace;
138
		const fallbackModuleFilenameTemplate = this.fallbackModuleFilenameTemplate;
139
		const requestShortener = compiler.requestShortener;
140
		const options = this.options;
141
		options.test = options.test || /\.(m?js|css)($|\?)/i;
142

    
143
		const matchObject = ModuleFilenameHelpers.matchObject.bind(
144
			undefined,
145
			options
146
		);
147

    
148
		compiler.hooks.compilation.tap("SourceMapDevToolPlugin", compilation => {
149
			new SourceMapDevToolModuleOptionsPlugin(options).apply(compilation);
150

    
151
			compilation.hooks.afterOptimizeChunkAssets.tap(
152
				/** @type {TODO} */
153
				({ name: "SourceMapDevToolPlugin", context: true }),
154
				/**
155
				 * @param {object} context hook context
156
				 * @param {Array<Chunk>} chunks resulted chunks
157
				 * @throws {Error} throws error, if `sourceMapFilename === false && sourceMappingURLComment === false`
158
				 * @returns {void}
159
				 */
160
				(context, chunks) => {
161
					/** @type {Map<string | Module, string>} */
162
					const moduleToSourceNameMapping = new Map();
163
					/**
164
					 * @type {Function}
165
					 * @returns {void}
166
					 */
167
					const reportProgress =
168
						context && context.reportProgress
169
							? context.reportProgress
170
							: () => {};
171

    
172
					const files = [];
173
					for (const chunk of chunks) {
174
						for (const file of chunk.files) {
175
							if (matchObject(file)) {
176
								files.push({
177
									file,
178
									chunk
179
								});
180
							}
181
						}
182
					}
183

    
184
					reportProgress(0.0);
185
					const tasks = [];
186
					files.forEach(({ file, chunk }, idx) => {
187
						const asset = compilation.getAsset(file).source;
188
						const cache = assetsCache.get(asset);
189
						/**
190
						 * If presented in cache, reassigns assets. Cache assets already have source maps.
191
						 */
192
						if (cache && cache.file === file) {
193
							for (const cachedFile in cache.assets) {
194
								if (cachedFile === file) {
195
									compilation.updateAsset(cachedFile, cache.assets[cachedFile]);
196
								} else {
197
									compilation.emitAsset(cachedFile, cache.assets[cachedFile], {
198
										development: true
199
									});
200
								}
201
								/**
202
								 * Add file to chunk, if not presented there
203
								 */
204
								if (cachedFile !== file) chunk.files.push(cachedFile);
205
							}
206
							return;
207
						}
208

    
209
						reportProgress(
210
							(0.5 * idx) / files.length,
211
							file,
212
							"generate SourceMap"
213
						);
214
						/** @type {SourceMapTask | undefined} */
215
						const task = getTaskForFile(
216
							file,
217
							asset,
218
							chunk,
219
							options,
220
							compilation
221
						);
222

    
223
						if (task) {
224
							const modules = task.modules;
225

    
226
							for (let idx = 0; idx < modules.length; idx++) {
227
								const module = modules[idx];
228
								if (!moduleToSourceNameMapping.get(module)) {
229
									moduleToSourceNameMapping.set(
230
										module,
231
										ModuleFilenameHelpers.createFilename(
232
											module,
233
											{
234
												moduleFilenameTemplate: moduleFilenameTemplate,
235
												namespace: namespace
236
											},
237
											requestShortener
238
										)
239
									);
240
								}
241
							}
242

    
243
							tasks.push(task);
244
						}
245
					});
246

    
247
					reportProgress(0.5, "resolve sources");
248
					/** @type {Set<string>} */
249
					const usedNamesSet = new Set(moduleToSourceNameMapping.values());
250
					/** @type {Set<string>} */
251
					const conflictDetectionSet = new Set();
252

    
253
					/**
254
					 * all modules in defined order (longest identifier first)
255
					 * @type {Array<string | Module>}
256
					 */
257
					const allModules = Array.from(moduleToSourceNameMapping.keys()).sort(
258
						(a, b) => {
259
							const ai = typeof a === "string" ? a : a.identifier();
260
							const bi = typeof b === "string" ? b : b.identifier();
261
							return ai.length - bi.length;
262
						}
263
					);
264

    
265
					// find modules with conflicting source names
266
					for (let idx = 0; idx < allModules.length; idx++) {
267
						const module = allModules[idx];
268
						let sourceName = moduleToSourceNameMapping.get(module);
269
						let hasName = conflictDetectionSet.has(sourceName);
270
						if (!hasName) {
271
							conflictDetectionSet.add(sourceName);
272
							continue;
273
						}
274

    
275
						// try the fallback name first
276
						sourceName = ModuleFilenameHelpers.createFilename(
277
							module,
278
							{
279
								moduleFilenameTemplate: fallbackModuleFilenameTemplate,
280
								namespace: namespace
281
							},
282
							requestShortener
283
						);
284
						hasName = usedNamesSet.has(sourceName);
285
						if (!hasName) {
286
							moduleToSourceNameMapping.set(module, sourceName);
287
							usedNamesSet.add(sourceName);
288
							continue;
289
						}
290

    
291
						// elsewise just append stars until we have a valid name
292
						while (hasName) {
293
							sourceName += "*";
294
							hasName = usedNamesSet.has(sourceName);
295
						}
296
						moduleToSourceNameMapping.set(module, sourceName);
297
						usedNamesSet.add(sourceName);
298
					}
299
					tasks.forEach((task, index) => {
300
						reportProgress(
301
							0.5 + (0.5 * index) / tasks.length,
302
							task.file,
303
							"attach SourceMap"
304
						);
305
						const assets = Object.create(null);
306
						const chunk = task.chunk;
307
						const file = task.file;
308
						const asset = task.asset;
309
						const sourceMap = task.sourceMap;
310
						const source = task.source;
311
						const modules = task.modules;
312
						const moduleFilenames = modules.map(m =>
313
							moduleToSourceNameMapping.get(m)
314
						);
315
						sourceMap.sources = moduleFilenames;
316
						if (options.noSources) {
317
							sourceMap.sourcesContent = undefined;
318
						}
319
						sourceMap.sourceRoot = options.sourceRoot || "";
320
						sourceMap.file = file;
321
						assetsCache.set(asset, { file, assets });
322
						/** @type {string | false} */
323
						let currentSourceMappingURLComment = sourceMappingURLComment;
324
						if (
325
							currentSourceMappingURLComment !== false &&
326
							/\.css($|\?)/i.test(file)
327
						) {
328
							currentSourceMappingURLComment = currentSourceMappingURLComment.replace(
329
								/^\n\/\/(.*)$/,
330
								"\n/*$1*/"
331
							);
332
						}
333
						const sourceMapString = JSON.stringify(sourceMap);
334
						if (sourceMapFilename) {
335
							let filename = file;
336
							let query = "";
337
							const idx = filename.indexOf("?");
338
							if (idx >= 0) {
339
								query = filename.substr(idx);
340
								filename = filename.substr(0, idx);
341
							}
342
							const pathParams = {
343
								chunk,
344
								filename: options.fileContext
345
									? path.relative(options.fileContext, filename)
346
									: filename,
347
								query,
348
								basename: basename(filename),
349
								contentHash: createHash("md4")
350
									.update(sourceMapString)
351
									.digest("hex")
352
							};
353
							let sourceMapFile = compilation.getPath(
354
								sourceMapFilename,
355
								pathParams
356
							);
357
							const sourceMapUrl = options.publicPath
358
								? options.publicPath + sourceMapFile.replace(/\\/g, "/")
359
								: path
360
										.relative(path.dirname(file), sourceMapFile)
361
										.replace(/\\/g, "/");
362
							/**
363
							 * Add source map url to compilation asset, if {@link currentSourceMappingURLComment} presented
364
							 */
365
							if (currentSourceMappingURLComment !== false) {
366
								const asset = new ConcatSource(
367
									new RawSource(source),
368
									compilation.getPath(
369
										currentSourceMappingURLComment,
370
										Object.assign({ url: sourceMapUrl }, pathParams)
371
									)
372
								);
373
								assets[file] = asset;
374
								compilation.updateAsset(file, asset);
375
							}
376
							/**
377
							 * Add source map file to compilation assets and chunk files
378
							 */
379
							const asset = new RawSource(sourceMapString);
380
							assets[sourceMapFile] = asset;
381
							compilation.emitAsset(sourceMapFile, asset, {
382
								development: true
383
							});
384
							chunk.files.push(sourceMapFile);
385
						} else {
386
							if (currentSourceMappingURLComment === false) {
387
								throw new Error(
388
									"SourceMapDevToolPlugin: append can't be false when no filename is provided"
389
								);
390
							}
391
							/**
392
							 * Add source map as data url to asset
393
							 */
394
							const asset = new ConcatSource(
395
								new RawSource(source),
396
								currentSourceMappingURLComment
397
									.replace(/\[map\]/g, () => sourceMapString)
398
									.replace(
399
										/\[url\]/g,
400
										() =>
401
											`data:application/json;charset=utf-8;base64,${Buffer.from(
402
												sourceMapString,
403
												"utf-8"
404
											).toString("base64")}`
405
									)
406
							);
407
							assets[file] = asset;
408
							compilation.updateAsset(file, asset);
409
						}
410
					});
411
					reportProgress(1.0);
412
				}
413
			);
414
		});
415
	}
416
}
417

    
418
module.exports = SourceMapDevToolPlugin;
(123-123/144)