webpack
Version:
Packs CommonJs/AMD modules for the browser. Allows to split your codebase into multiple bundles, which can be loaded on demand. Support loaders to preprocess files, i.e. json, jsx, es7, css, less, ... and your custom stuff.
415 lines (386 loc) • 12.5 kB
JavaScript
/*
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 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) {
return {
chunk,
file,
asset,
source,
sourceMap,
modules: undefined
};
}
};
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.sourceMap.sources.map(source => {
const module = compilation.findModule(source);
return module || source;
});
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
)
);
}
}
task.modules = modules;
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;