UNPKG

9.38 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
18const basename = name => {
19 if (!name.includes("/")) return name;
20 return name.substr(name.lastIndexOf("/") + 1);
21};
22
23const assetsCache = new WeakMap();
24
25const getTaskForFile = (file, chunk, options, compilation) => {
26 const asset = compilation.assets[file];
27 const cache = assetsCache.get(asset);
28 if (cache && cache.file === file) {
29 for (const cachedFile in cache.assets) {
30 compilation.assets[cachedFile] = cache.assets[cachedFile];
31 if (cachedFile !== file) chunk.files.push(cachedFile);
32 }
33 return;
34 }
35 let source, sourceMap;
36 if (asset.sourceAndMap) {
37 const sourceAndMap = asset.sourceAndMap(options);
38 sourceMap = sourceAndMap.map;
39 source = sourceAndMap.source;
40 } else {
41 sourceMap = asset.map(options);
42 source = asset.source();
43 }
44 if (sourceMap) {
45 return {
46 chunk,
47 file,
48 asset,
49 source,
50 sourceMap,
51 modules: undefined
52 };
53 }
54};
55
56class SourceMapDevToolPlugin {
57 /**
58 * @param {SourceMapDevToolPluginOptions=} options options object
59 */
60 constructor(options) {
61 if (arguments.length > 1) {
62 throw new Error(
63 "SourceMapDevToolPlugin only takes one argument (pass an options object)"
64 );
65 }
66
67 if (!options) options = {};
68
69 validateOptions(schema, options, "SourceMap DevTool Plugin");
70
71 this.sourceMapFilename = options.filename;
72 /** @type {string | false} */
73 this.sourceMappingURLComment =
74 options.append === false
75 ? false
76 : options.append || "\n//# sourceMappingURL=[url]";
77 this.moduleFilenameTemplate =
78 options.moduleFilenameTemplate || "webpack://[namespace]/[resourcePath]";
79 this.fallbackModuleFilenameTemplate =
80 options.fallbackModuleFilenameTemplate ||
81 "webpack://[namespace]/[resourcePath]?[hash]";
82 this.namespace = options.namespace || "";
83 this.options = options;
84 }
85
86 apply(compiler) {
87 const sourceMapFilename = this.sourceMapFilename;
88 const sourceMappingURLComment = this.sourceMappingURLComment;
89 const moduleFilenameTemplate = this.moduleFilenameTemplate;
90 const namespace = this.namespace;
91 const fallbackModuleFilenameTemplate = this.fallbackModuleFilenameTemplate;
92 const requestShortener = compiler.requestShortener;
93 const options = this.options;
94 options.test = options.test || /\.(m?js|css)($|\?)/i;
95
96 const matchObject = ModuleFilenameHelpers.matchObject.bind(
97 undefined,
98 options
99 );
100
101 compiler.hooks.compilation.tap("SourceMapDevToolPlugin", compilation => {
102 new SourceMapDevToolModuleOptionsPlugin(options).apply(compilation);
103
104 compilation.hooks.afterOptimizeChunkAssets.tap(
105 {
106 name: "SourceMapDevToolPlugin",
107 context: true
108 },
109 (context, chunks) => {
110 const moduleToSourceNameMapping = new Map();
111 const reportProgress =
112 context && context.reportProgress
113 ? context.reportProgress
114 : () => {};
115
116 const files = [];
117 for (const chunk of chunks) {
118 for (const file of chunk.files) {
119 if (matchObject(file)) {
120 files.push({
121 file,
122 chunk
123 });
124 }
125 }
126 }
127
128 reportProgress(0.0);
129 const tasks = [];
130 files.forEach(({ file, chunk }, idx) => {
131 reportProgress(
132 (0.5 * idx) / files.length,
133 file,
134 "generate SourceMap"
135 );
136 const task = getTaskForFile(file, chunk, options, compilation);
137
138 if (task) {
139 const modules = task.sourceMap.sources.map(source => {
140 const module = compilation.findModule(source);
141 return module || source;
142 });
143
144 for (let idx = 0; idx < modules.length; idx++) {
145 const module = modules[idx];
146 if (!moduleToSourceNameMapping.get(module)) {
147 moduleToSourceNameMapping.set(
148 module,
149 ModuleFilenameHelpers.createFilename(
150 module,
151 {
152 moduleFilenameTemplate: moduleFilenameTemplate,
153 namespace: namespace
154 },
155 requestShortener
156 )
157 );
158 }
159 }
160
161 task.modules = modules;
162
163 tasks.push(task);
164 }
165 });
166
167 reportProgress(0.5, "resolve sources");
168 const usedNamesSet = new Set(moduleToSourceNameMapping.values());
169 const conflictDetectionSet = new Set();
170
171 // all modules in defined order (longest identifier first)
172 const allModules = Array.from(moduleToSourceNameMapping.keys()).sort(
173 (a, b) => {
174 const ai = typeof a === "string" ? a : a.identifier();
175 const bi = typeof b === "string" ? b : b.identifier();
176 return ai.length - bi.length;
177 }
178 );
179
180 // find modules with conflicting source names
181 for (let idx = 0; idx < allModules.length; idx++) {
182 const module = allModules[idx];
183 let sourceName = moduleToSourceNameMapping.get(module);
184 let hasName = conflictDetectionSet.has(sourceName);
185 if (!hasName) {
186 conflictDetectionSet.add(sourceName);
187 continue;
188 }
189
190 // try the fallback name first
191 sourceName = ModuleFilenameHelpers.createFilename(
192 module,
193 {
194 moduleFilenameTemplate: fallbackModuleFilenameTemplate,
195 namespace: namespace
196 },
197 requestShortener
198 );
199 hasName = usedNamesSet.has(sourceName);
200 if (!hasName) {
201 moduleToSourceNameMapping.set(module, sourceName);
202 usedNamesSet.add(sourceName);
203 continue;
204 }
205
206 // elsewise just append stars until we have a valid name
207 while (hasName) {
208 sourceName += "*";
209 hasName = usedNamesSet.has(sourceName);
210 }
211 moduleToSourceNameMapping.set(module, sourceName);
212 usedNamesSet.add(sourceName);
213 }
214 tasks.forEach((task, index) => {
215 reportProgress(
216 0.5 + (0.5 * index) / tasks.length,
217 task.file,
218 "attach SourceMap"
219 );
220 const assets = Object.create(null);
221 const chunk = task.chunk;
222 const file = task.file;
223 const asset = task.asset;
224 const sourceMap = task.sourceMap;
225 const source = task.source;
226 const modules = task.modules;
227 const moduleFilenames = modules.map(m =>
228 moduleToSourceNameMapping.get(m)
229 );
230 sourceMap.sources = moduleFilenames;
231 if (options.noSources) {
232 sourceMap.sourcesContent = undefined;
233 }
234 sourceMap.sourceRoot = options.sourceRoot || "";
235 sourceMap.file = file;
236 assetsCache.set(asset, { file, assets });
237 /** @type {string | false} */
238 let currentSourceMappingURLComment = sourceMappingURLComment;
239 if (
240 currentSourceMappingURLComment !== false &&
241 /\.css($|\?)/i.test(file)
242 ) {
243 currentSourceMappingURLComment = currentSourceMappingURLComment.replace(
244 /^\n\/\/(.*)$/,
245 "\n/*$1*/"
246 );
247 }
248 const sourceMapString = JSON.stringify(sourceMap);
249 if (sourceMapFilename) {
250 let filename = file;
251 let query = "";
252 const idx = filename.indexOf("?");
253 if (idx >= 0) {
254 query = filename.substr(idx);
255 filename = filename.substr(0, idx);
256 }
257 let sourceMapFile = compilation.getPath(sourceMapFilename, {
258 chunk,
259 filename: options.fileContext
260 ? path.relative(options.fileContext, filename)
261 : filename,
262 query,
263 basename: basename(filename),
264 contentHash: createHash("md4")
265 .update(sourceMapString)
266 .digest("hex")
267 });
268 const sourceMapUrl = options.publicPath
269 ? options.publicPath + sourceMapFile.replace(/\\/g, "/")
270 : path
271 .relative(path.dirname(file), sourceMapFile)
272 .replace(/\\/g, "/");
273 if (currentSourceMappingURLComment !== false) {
274 assets[file] = compilation.assets[file] = new ConcatSource(
275 new RawSource(source),
276 currentSourceMappingURLComment.replace(
277 /\[url\]/g,
278 sourceMapUrl
279 )
280 );
281 }
282 assets[sourceMapFile] = compilation.assets[
283 sourceMapFile
284 ] = new RawSource(sourceMapString);
285 chunk.files.push(sourceMapFile);
286 } else {
287 if (currentSourceMappingURLComment === false) {
288 throw new Error(
289 "SourceMapDevToolPlugin: append can't be false when no filename is provided"
290 );
291 }
292 assets[file] = compilation.assets[file] = new ConcatSource(
293 new RawSource(source),
294 currentSourceMappingURLComment
295 .replace(/\[map\]/g, () => sourceMapString)
296 .replace(
297 /\[url\]/g,
298 () =>
299 `data:application/json;charset=utf-8;base64,${Buffer.from(
300 sourceMapString,
301 "utf-8"
302 ).toString("base64")}`
303 )
304 );
305 }
306 });
307 reportProgress(1.0);
308 }
309 );
310 });
311 }
312}
313
314module.exports = SourceMapDevToolPlugin;