| /* |
| MIT License http://www.opensource.org/licenses/mit-license.php |
| Author Tobias Koppers @sokra |
| */ |
| "use strict"; |
| |
| const path = require("path"); |
| const { ConcatSource, RawSource } = require("webpack-sources"); |
| const ModuleFilenameHelpers = require("./ModuleFilenameHelpers"); |
| const SourceMapDevToolModuleOptionsPlugin = require("./SourceMapDevToolModuleOptionsPlugin"); |
| const createHash = require("./util/createHash"); |
| const { absolutify } = require("./util/identifier"); |
| |
| const validateOptions = require("schema-utils"); |
| const schema = require("../schemas/plugins/SourceMapDevToolPlugin.json"); |
| |
| /** @typedef {import("../declarations/plugins/SourceMapDevToolPlugin").SourceMapDevToolPluginOptions} SourceMapDevToolPluginOptions */ |
| /** @typedef {import("./Chunk")} Chunk */ |
| /** @typedef {import("webpack-sources").Source} Source */ |
| /** @typedef {import("source-map").RawSourceMap} SourceMap */ |
| /** @typedef {import("./Module")} Module */ |
| /** @typedef {import("./Compilation")} Compilation */ |
| /** @typedef {import("./Compiler")} Compiler */ |
| /** @typedef {import("./Compilation")} SourceMapDefinition */ |
| |
| /** |
| * @typedef {object} SourceMapTask |
| * @property {Source} asset |
| * @property {Array<string | Module>} [modules] |
| * @property {string} source |
| * @property {string} file |
| * @property {SourceMap} sourceMap |
| * @property {Chunk} chunk |
| */ |
| |
| /** |
| * @param {string} name file path |
| * @returns {string} file name |
| */ |
| const basename = name => { |
| if (!name.includes("/")) return name; |
| return name.substr(name.lastIndexOf("/") + 1); |
| }; |
| |
| /** |
| * @type {WeakMap<Source, {file: string, assets: {[k: string]: ConcatSource | RawSource}}>} |
| */ |
| const assetsCache = new WeakMap(); |
| |
| /** |
| * Creating {@link SourceMapTask} for given file |
| * @param {string} file current compiled file |
| * @param {Source} asset the asset |
| * @param {Chunk} chunk related chunk |
| * @param {SourceMapDevToolPluginOptions} options source map options |
| * @param {Compilation} compilation compilation instance |
| * @returns {SourceMapTask | undefined} created task instance or `undefined` |
| */ |
| const getTaskForFile = (file, asset, chunk, options, compilation) => { |
| let source, sourceMap; |
| /** |
| * Check if asset can build source map |
| */ |
| if (asset.sourceAndMap) { |
| const sourceAndMap = asset.sourceAndMap(options); |
| sourceMap = sourceAndMap.map; |
| source = sourceAndMap.source; |
| } else { |
| sourceMap = asset.map(options); |
| source = asset.source(); |
| } |
| if (!sourceMap || typeof source !== "string") return; |
| const context = compilation.options.context; |
| const modules = sourceMap.sources.map(source => { |
| if (source.startsWith("webpack://")) { |
| source = absolutify(context, source.slice(10)); |
| } |
| const module = compilation.findModule(source); |
| return module || source; |
| }); |
| |
| return { |
| chunk, |
| file, |
| asset, |
| source, |
| sourceMap, |
| modules |
| }; |
| }; |
| |
| class SourceMapDevToolPlugin { |
| /** |
| * @param {SourceMapDevToolPluginOptions} [options] options object |
| * @throws {Error} throws error, if got more than 1 arguments |
| */ |
| constructor(options) { |
| if (arguments.length > 1) { |
| throw new Error( |
| "SourceMapDevToolPlugin only takes one argument (pass an options object)" |
| ); |
| } |
| |
| if (!options) options = {}; |
| |
| validateOptions(schema, options, "SourceMap DevTool Plugin"); |
| |
| /** @type {string | false} */ |
| this.sourceMapFilename = options.filename; |
| /** @type {string | false} */ |
| this.sourceMappingURLComment = |
| options.append === false |
| ? false |
| : options.append || "\n//# sourceMappingURL=[url]"; |
| /** @type {string | Function} */ |
| this.moduleFilenameTemplate = |
| options.moduleFilenameTemplate || "webpack://[namespace]/[resourcePath]"; |
| /** @type {string | Function} */ |
| this.fallbackModuleFilenameTemplate = |
| options.fallbackModuleFilenameTemplate || |
| "webpack://[namespace]/[resourcePath]?[hash]"; |
| /** @type {string} */ |
| this.namespace = options.namespace || ""; |
| /** @type {SourceMapDevToolPluginOptions} */ |
| this.options = options; |
| } |
| |
| /** |
| * Apply compiler |
| * @param {Compiler} compiler compiler instance |
| * @returns {void} |
| */ |
| apply(compiler) { |
| const sourceMapFilename = this.sourceMapFilename; |
| const sourceMappingURLComment = this.sourceMappingURLComment; |
| const moduleFilenameTemplate = this.moduleFilenameTemplate; |
| const namespace = this.namespace; |
| const fallbackModuleFilenameTemplate = this.fallbackModuleFilenameTemplate; |
| const requestShortener = compiler.requestShortener; |
| const options = this.options; |
| options.test = options.test || /\.(m?js|css)($|\?)/i; |
| |
| const matchObject = ModuleFilenameHelpers.matchObject.bind( |
| undefined, |
| options |
| ); |
| |
| compiler.hooks.compilation.tap("SourceMapDevToolPlugin", compilation => { |
| new SourceMapDevToolModuleOptionsPlugin(options).apply(compilation); |
| |
| compilation.hooks.afterOptimizeChunkAssets.tap( |
| /** @type {TODO} */ |
| ({ name: "SourceMapDevToolPlugin", context: true }), |
| /** |
| * @param {object} context hook context |
| * @param {Array<Chunk>} chunks resulted chunks |
| * @throws {Error} throws error, if `sourceMapFilename === false && sourceMappingURLComment === false` |
| * @returns {void} |
| */ |
| (context, chunks) => { |
| /** @type {Map<string | Module, string>} */ |
| const moduleToSourceNameMapping = new Map(); |
| /** |
| * @type {Function} |
| * @returns {void} |
| */ |
| const reportProgress = |
| context && context.reportProgress |
| ? context.reportProgress |
| : () => {}; |
| |
| const files = []; |
| for (const chunk of chunks) { |
| for (const file of chunk.files) { |
| if (matchObject(file)) { |
| files.push({ |
| file, |
| chunk |
| }); |
| } |
| } |
| } |
| |
| reportProgress(0.0); |
| const tasks = []; |
| files.forEach(({ file, chunk }, idx) => { |
| const asset = compilation.getAsset(file).source; |
| const cache = assetsCache.get(asset); |
| /** |
| * If presented in cache, reassigns assets. Cache assets already have source maps. |
| */ |
| if (cache && cache.file === file) { |
| for (const cachedFile in cache.assets) { |
| if (cachedFile === file) { |
| compilation.updateAsset(cachedFile, cache.assets[cachedFile]); |
| } else { |
| compilation.emitAsset(cachedFile, cache.assets[cachedFile], { |
| development: true |
| }); |
| } |
| /** |
| * Add file to chunk, if not presented there |
| */ |
| if (cachedFile !== file) chunk.files.push(cachedFile); |
| } |
| return; |
| } |
| |
| reportProgress( |
| (0.5 * idx) / files.length, |
| file, |
| "generate SourceMap" |
| ); |
| /** @type {SourceMapTask | undefined} */ |
| const task = getTaskForFile( |
| file, |
| asset, |
| chunk, |
| options, |
| compilation |
| ); |
| |
| if (task) { |
| const modules = task.modules; |
| |
| for (let idx = 0; idx < modules.length; idx++) { |
| const module = modules[idx]; |
| if (!moduleToSourceNameMapping.get(module)) { |
| moduleToSourceNameMapping.set( |
| module, |
| ModuleFilenameHelpers.createFilename( |
| module, |
| { |
| moduleFilenameTemplate: moduleFilenameTemplate, |
| namespace: namespace |
| }, |
| requestShortener |
| ) |
| ); |
| } |
| } |
| |
| tasks.push(task); |
| } |
| }); |
| |
| reportProgress(0.5, "resolve sources"); |
| /** @type {Set<string>} */ |
| const usedNamesSet = new Set(moduleToSourceNameMapping.values()); |
| /** @type {Set<string>} */ |
| const conflictDetectionSet = new Set(); |
| |
| /** |
| * all modules in defined order (longest identifier first) |
| * @type {Array<string | Module>} |
| */ |
| const allModules = Array.from(moduleToSourceNameMapping.keys()).sort( |
| (a, b) => { |
| const ai = typeof a === "string" ? a : a.identifier(); |
| const bi = typeof b === "string" ? b : b.identifier(); |
| return ai.length - bi.length; |
| } |
| ); |
| |
| // find modules with conflicting source names |
| for (let idx = 0; idx < allModules.length; idx++) { |
| const module = allModules[idx]; |
| let sourceName = moduleToSourceNameMapping.get(module); |
| let hasName = conflictDetectionSet.has(sourceName); |
| if (!hasName) { |
| conflictDetectionSet.add(sourceName); |
| continue; |
| } |
| |
| // try the fallback name first |
| sourceName = ModuleFilenameHelpers.createFilename( |
| module, |
| { |
| moduleFilenameTemplate: fallbackModuleFilenameTemplate, |
| namespace: namespace |
| }, |
| requestShortener |
| ); |
| hasName = usedNamesSet.has(sourceName); |
| if (!hasName) { |
| moduleToSourceNameMapping.set(module, sourceName); |
| usedNamesSet.add(sourceName); |
| continue; |
| } |
| |
| // elsewise just append stars until we have a valid name |
| while (hasName) { |
| sourceName += "*"; |
| hasName = usedNamesSet.has(sourceName); |
| } |
| moduleToSourceNameMapping.set(module, sourceName); |
| usedNamesSet.add(sourceName); |
| } |
| tasks.forEach((task, index) => { |
| reportProgress( |
| 0.5 + (0.5 * index) / tasks.length, |
| task.file, |
| "attach SourceMap" |
| ); |
| const assets = Object.create(null); |
| const chunk = task.chunk; |
| const file = task.file; |
| const asset = task.asset; |
| const sourceMap = task.sourceMap; |
| const source = task.source; |
| const modules = task.modules; |
| const moduleFilenames = modules.map(m => |
| moduleToSourceNameMapping.get(m) |
| ); |
| sourceMap.sources = moduleFilenames; |
| if (options.noSources) { |
| sourceMap.sourcesContent = undefined; |
| } |
| sourceMap.sourceRoot = options.sourceRoot || ""; |
| sourceMap.file = file; |
| assetsCache.set(asset, { file, assets }); |
| /** @type {string | false} */ |
| let currentSourceMappingURLComment = sourceMappingURLComment; |
| if ( |
| currentSourceMappingURLComment !== false && |
| /\.css($|\?)/i.test(file) |
| ) { |
| currentSourceMappingURLComment = currentSourceMappingURLComment.replace( |
| /^\n\/\/(.*)$/, |
| "\n/*$1*/" |
| ); |
| } |
| const sourceMapString = JSON.stringify(sourceMap); |
| if (sourceMapFilename) { |
| let filename = file; |
| let query = ""; |
| const idx = filename.indexOf("?"); |
| if (idx >= 0) { |
| query = filename.substr(idx); |
| filename = filename.substr(0, idx); |
| } |
| const pathParams = { |
| chunk, |
| filename: options.fileContext |
| ? path.relative(options.fileContext, filename) |
| : filename, |
| query, |
| basename: basename(filename), |
| contentHash: createHash("md4") |
| .update(sourceMapString) |
| .digest("hex") |
| }; |
| let sourceMapFile = compilation.getPath( |
| sourceMapFilename, |
| pathParams |
| ); |
| const sourceMapUrl = options.publicPath |
| ? options.publicPath + sourceMapFile.replace(/\\/g, "/") |
| : path |
| .relative(path.dirname(file), sourceMapFile) |
| .replace(/\\/g, "/"); |
| /** |
| * Add source map url to compilation asset, if {@link currentSourceMappingURLComment} presented |
| */ |
| if (currentSourceMappingURLComment !== false) { |
| const asset = new ConcatSource( |
| new RawSource(source), |
| compilation.getPath( |
| currentSourceMappingURLComment, |
| Object.assign({ url: sourceMapUrl }, pathParams) |
| ) |
| ); |
| assets[file] = asset; |
| compilation.updateAsset(file, asset); |
| } |
| /** |
| * Add source map file to compilation assets and chunk files |
| */ |
| const asset = new RawSource(sourceMapString); |
| assets[sourceMapFile] = asset; |
| compilation.emitAsset(sourceMapFile, asset, { |
| development: true |
| }); |
| chunk.files.push(sourceMapFile); |
| } else { |
| if (currentSourceMappingURLComment === false) { |
| throw new Error( |
| "SourceMapDevToolPlugin: append can't be false when no filename is provided" |
| ); |
| } |
| /** |
| * Add source map as data url to asset |
| */ |
| const asset = new ConcatSource( |
| new RawSource(source), |
| currentSourceMappingURLComment |
| .replace(/\[map\]/g, () => sourceMapString) |
| .replace( |
| /\[url\]/g, |
| () => |
| `data:application/json;charset=utf-8;base64,${Buffer.from( |
| sourceMapString, |
| "utf-8" |
| ).toString("base64")}` |
| ) |
| ); |
| assets[file] = asset; |
| compilation.updateAsset(file, asset); |
| } |
| }); |
| reportProgress(1.0); |
| } |
| ); |
| }); |
| } |
| } |
| |
| module.exports = SourceMapDevToolPlugin; |