feat: add `extractSourceMap` option to implement the capabilities of loading source maps by comment

This commit is contained in:
Xiao 2025-08-27 23:53:50 +08:00 committed by GitHub
parent bb98bb1470
commit 64ce283d76
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
41 changed files with 959 additions and 36 deletions

View File

@ -316,7 +316,8 @@
"Nitin",
"Kumar",
"spacek",
"thelarkinn"
"thelarkinn",
"behaviour"
],
"ignoreRegExpList": [
"/Author.+/",

View File

@ -1467,6 +1467,10 @@ export interface RuleSetRule {
* Shortcut for resource.exclude.
*/
exclude?: RuleSetConditionOrConditionsAbsolute;
/**
* Enable/Disable extracting source map.
*/
extractSourceMap?: boolean;
/**
* The options for the module generator.
*/

View File

@ -101,6 +101,7 @@ const memoize = require("./util/memoize");
/** @typedef {import("./util/identifier").AssociatedObjectForCache} AssociatedObjectForCache */
/** @typedef {import("./dependencies/HarmonyImportSideEffectDependency")} HarmonyImportSideEffectDependency */
/** @typedef {import("./dependencies/HarmonyImportSpecifierDependency")} HarmonyImportSpecifierDependency */
/** @typedef {import("../declarations/WebpackOptions").RuleSetRule["extractSourceMap"]} ExtractSourceMapOptions */
/**
* @template T
* @typedef {import("./util/deprecation").FakeHook<T>} FakeHook
@ -122,6 +123,9 @@ const memoize = require("./util/memoize");
const getInvalidDependenciesModuleWarning = memoize(() =>
require("./InvalidDependenciesModuleWarning")
);
const getExtractSourceMap = memoize(() => require("./util/extractSourceMap"));
const getValidate = memoize(() => require("schema-utils").validate);
const ABSOLUTE_PATH_REGEX = /^([a-zA-Z]:\\|\\\\|\/)/;
@ -254,6 +258,7 @@ makeSerializable(
* @property {Generator} generator the generator used
* @property {GeneratorOptions=} generatorOptions the options of the generator used
* @property {ResolveOptions=} resolveOptions options used for resolving requests from this module
* @property {boolean=} extractSourceMap enable/disable extracting source map
*/
/** @type {WeakMap<Compilation, NormalModuleCompilationHooks>} */
@ -341,7 +346,8 @@ class NormalModule extends Module {
parserOptions,
generator,
generatorOptions,
resolveOptions
resolveOptions,
extractSourceMap
}) {
super(type, context || getContext(resource), layer);
@ -373,6 +379,8 @@ class NormalModule extends Module {
// already declared in super class
this.resolveOptions = resolveOptions;
}
/** @type {ExtractSourceMapOptions} */
this.extractSourceMap = extractSourceMap;
// Info from Build
/** @type {WebpackError | null} */
@ -1004,14 +1012,14 @@ class NormalModule extends Module {
/**
* @param {LoaderContext<EXPECTED_ANY>} loaderContext the loader context
* @param {string} resourcePath the resource Path
* @param {(err: Error | null, result?: string | Buffer) => void} callback callback
* @param {(err: Error | null, result?: string | Buffer, sourceMap?: Result[1]) => void} callback callback
*/
processResource: (loaderContext, resourcePath, callback) => {
const resource = loaderContext.resource;
const scheme = getScheme(resource);
hooks.readResource
.for(scheme)
.callAsync(loaderContext, (err, result) => {
.callAsync(loaderContext, async (err, result) => {
if (err) return callback(err);
if (typeof result !== "string" && !result) {
return callback(
@ -1022,6 +1030,24 @@ class NormalModule extends Module {
)
);
}
if (
this.extractSourceMap &&
(this.useSourceMap || this.useSimpleSourceMap)
) {
try {
const { source, sourceMap, fileDependencies } =
await getExtractSourceMap()(result, resourcePath, fs);
if (this.buildInfo && this.buildInfo.fileDependencies) {
this.buildInfo.fileDependencies.addAll(fileDependencies);
}
return callback(null, source, sourceMap);
} catch (err) {
this.addWarning(
new ModuleWarning(/** @type {Error} */ (err))
);
return callback(null, result);
}
}
return callback(null, result);
});
}
@ -1654,6 +1680,7 @@ class NormalModule extends Module {
write(this._lastSuccessfulBuildMeta);
write(this._forceBuild);
write(this._codeGeneratorData);
write(this.extractSourceMap);
super.serialize(context);
}
@ -1694,6 +1721,7 @@ class NormalModule extends Module {
this._lastSuccessfulBuildMeta = read();
this._forceBuild = read();
this._codeGeneratorData = read();
this.extractSourceMap = read();
super.deserialize(context);
}
}

View File

@ -61,7 +61,7 @@ const {
* @typedef {import("./Compiler").Callback<T>} Callback
*/
/** @typedef {Pick<RuleSetRule, 'type' | 'sideEffects' | 'parser' | 'generator' | 'resolve' | 'layer'>} ModuleSettings */
/** @typedef {Pick<RuleSetRule, 'type' | 'sideEffects' | 'parser' | 'generator' | 'resolve' | 'layer' | 'extractSourceMap'>} ModuleSettings */
/** @typedef {Partial<NormalModuleCreateData & { settings: ModuleSettings }>} CreateData */
/**
@ -246,6 +246,7 @@ const ruleSetCompiler = new RuleSetCompiler([
new BasicEffectRulePlugin("resolve"),
new BasicEffectRulePlugin("generator"),
new BasicEffectRulePlugin("layer"),
new BasicEffectRulePlugin("extractSourceMap"),
new UseEffectRulePlugin()
]);
@ -714,7 +715,8 @@ class NormalModuleFactory extends ModuleFactory {
parserOptions: settings.parser,
generator: this.getGenerator(type, settings.generator),
generatorOptions: settings.generator,
resolveOptions
resolveOptions,
extractSourceMap: settings.extractSourceMap || false
});
} catch (createDataErr) {
return callback(/** @type {Error} */ (createDataErr));

View File

@ -6,37 +6,10 @@
"use strict";
const NormalModule = require("../NormalModule");
const { URIRegEx, decodeDataURI } = require("../util/dataURL");
/** @typedef {import("../Compiler")} Compiler */
// data URL scheme: "data:text/javascript;charset=utf-8;base64,some-string"
// http://www.ietf.org/rfc/rfc2397.txt
const URIRegEx = /^data:([^;,]+)?((?:;[^;,]+)*?)(?:;(base64)?)?,(.*)$/i;
/**
* @param {string} uri data URI
* @returns {Buffer | null} decoded data
*/
const decodeDataURI = (uri) => {
const match = URIRegEx.exec(uri);
if (!match) return null;
const isBase64 = match[3];
const body = match[4];
if (isBase64) {
return Buffer.from(body, "base64");
}
// CSS allows to use `data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg"><rect width="100%" height="100%" style="stroke: rgb(223,224,225); stroke-width: 2px; fill: none; stroke-dasharray: 6px 3px" /></svg>`
// so we return original body if we can't `decodeURIComponent`
try {
return Buffer.from(decodeURIComponent(body), "ascii");
} catch (_) {
return Buffer.from(body, "ascii");
}
};
const PLUGIN_NAME = "DataUriPlugin";
class DataUriPlugin {

39
lib/util/dataURL.js Normal file
View File

@ -0,0 +1,39 @@
/*
MIT License http://www.opensource.org/licenses/mit-license.php
Author Natsu @xiaoxiaojx
*/
"use strict";
// data URL scheme: "data:text/javascript;charset=utf-8;base64,some-string"
// http://www.ietf.org/rfc/rfc2397.txt
const URIRegEx = /^data:([^;,]+)?((?:;[^;,]+)*?)(?:;(base64)?)?,(.*)$/i;
/**
* @param {string} uri data URI
* @returns {Buffer | null} decoded data
*/
const decodeDataURI = (uri) => {
const match = URIRegEx.exec(uri);
if (!match) return null;
const isBase64 = match[3];
const body = match[4];
if (isBase64) {
return Buffer.from(body, "base64");
}
// CSS allows to use `data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg"><rect width="100%" height="100%" style="stroke: rgb(223,224,225); stroke-width: 2px; fill: none; stroke-dasharray: 6px 3px" /></svg>`
// so we return original body if we can't `decodeURIComponent`
try {
return Buffer.from(decodeURIComponent(body), "ascii");
} catch (_) {
return Buffer.from(body, "ascii");
}
};
module.exports = {
URIRegEx,
decodeDataURI
};

View File

@ -0,0 +1,387 @@
/*
MIT License http://www.opensource.org/licenses/mit-license.php
Author Natsu @xiaoxiaojx
*/
"use strict";
const path = require("path");
const urlUtils = require("url");
const { decodeDataURI } = require("./dataURL");
const { isAbsolute, join } = require("./fs");
/** @typedef {import("../../declarations/WebpackOptions").RuleSetRule["extractSourceMap"]} ExtractSourceMapOptions */
/** @typedef {import("./fs").InputFileSystem} InputFileSystem */
/**
* @typedef {(input: string | Buffer<ArrayBufferLike>, resourcePath: string, fs: InputFileSystem) => Promise<{source: string | Buffer<ArrayBufferLike>, sourceMap: string | RawSourceMap | undefined, fileDependencies: string[]}>} SourceMapExtractorFunction
*/
/** @typedef {import("webpack-sources").RawSourceMap} RawSourceMap */
/**
* @typedef {object} SourceMappingURL
* @property {string} sourceMappingURL
* @property {string} replacementString
*/
// Matches only the last occurrence of sourceMappingURL
const innerRegex = /\s*[#@]\s*sourceMappingURL\s*=\s*([^\s'"]*)\s*/;
const validProtocolPattern = /^[a-z][a-z0-9+.-]*:/i;
const sourceMappingURLRegex = new RegExp(
"(?:" +
"/\\*" +
"(?:\\s*\r?\n(?://)?)?" +
`(?:${innerRegex.source})` +
"\\s*" +
"\\*/" +
"|" +
`//(?:${innerRegex.source})` +
")" +
"\\s*"
);
/**
* Extract source mapping URL from code comments
* @param {string} code source code content
* @returns {SourceMappingURL} source mapping information
*/
function getSourceMappingURL(code) {
const lines = code.split(/^/m);
let match;
for (let i = lines.length - 1; i >= 0; i--) {
match = lines[i].match(sourceMappingURLRegex);
if (match) {
break;
}
}
const sourceMappingURL = match ? match[1] || match[2] || "" : "";
return {
sourceMappingURL: sourceMappingURL
? decodeURI(sourceMappingURL)
: sourceMappingURL,
replacementString: match ? match[0] : ""
};
}
/**
* Get absolute path for source file
* @param {InputFileSystem} fs file system
* @param {string} context context directory
* @param {string} request file request
* @param {string} sourceRoot source root directory
* @returns {string} absolute path
*/
function getAbsolutePath(fs, context, request, sourceRoot) {
if (sourceRoot) {
if (isAbsolute(sourceRoot)) {
return join(fs, sourceRoot, request);
}
return join(fs, join(fs, context, sourceRoot), request);
}
return join(fs, context, request);
}
/**
* Check if value is a URL
* @param {string} value string to check
* @returns {boolean} true if value is a URL
*/
function isURL(value) {
return validProtocolPattern.test(value) && !path.win32.isAbsolute(value);
}
/**
* Fetch source content from data URL
* @param {InputFileSystem} fs file system
* @param {string} sourceURL data URL
* @returns {string} source content promise
*/
function fetchFromDataURL(fs, sourceURL) {
const content = decodeDataURI(sourceURL);
if (content) {
return content.toString("utf8");
}
throw new Error(`Failed to parse source map from "data" URL: ${sourceURL}`);
}
/**
* Fetch source content from file system
* @param {InputFileSystem} fs file system
* @param {string} sourceURL file URL
* @returns {Promise<{path: string, data?: string}>} source content promise
*/
async function fetchFromFilesystem(fs, sourceURL) {
let buffer;
if (isURL(sourceURL)) {
return { path: sourceURL };
}
try {
buffer = await new Promise((resolve, reject) => {
fs.readFile(
sourceURL,
(
/** @type {Error | null} */ error,
/** @type {Buffer<ArrayBufferLike> | undefined} */ data
) => {
if (error) {
return reject(error);
}
return resolve(data);
}
);
});
} catch (error) {
throw new Error(
`Failed to parse source map from '${sourceURL}' file: ${error}`
);
}
return { path: sourceURL, data: buffer.toString() };
}
/**
* Fetch from multiple possible file paths
* @param {InputFileSystem} fs file system
* @param {string[]} possibleRequests array of possible file paths
* @param {string} errorsAccumulator accumulated error messages
* @returns {Promise<{path: string, data?: string}>} source content promise
*/
async function fetchPathsFromFilesystem(
fs,
possibleRequests,
errorsAccumulator = ""
) {
let result;
try {
result = await fetchFromFilesystem(fs, possibleRequests[0]);
} catch (error) {
errorsAccumulator += `${/** @type {Error} */ (error).message}\n\n`;
const [, ...tailPossibleRequests] = possibleRequests;
if (tailPossibleRequests.length === 0) {
/** @type {Error} */ (error).message = errorsAccumulator;
throw error;
}
return fetchPathsFromFilesystem(
fs,
tailPossibleRequests,
errorsAccumulator
);
}
return result;
}
/**
* Fetch source content from URL
* @param {InputFileSystem} fs file system
* @param {string} context context directory
* @param {string} url source URL
* @param {string=} sourceRoot source root directory
* @param {boolean=} skipReading whether to skip reading file content
* @returns {Promise<{sourceURL: string, sourceContent?: string}>} source content promise
*/
async function fetchFromURL(fs, context, url, sourceRoot, skipReading = false) {
// 1. It's an absolute url and it is not `windows` path like `C:\dir\file`
if (isURL(url)) {
// eslint-disable-next-line n/no-deprecated-api
const { protocol } = urlUtils.parse(url);
if (protocol === "data:") {
if (skipReading) {
return { sourceURL: "" };
}
const sourceContent = fetchFromDataURL(fs, url);
return { sourceURL: "", sourceContent };
}
if (skipReading) {
return { sourceURL: url };
}
if (protocol === "file:") {
const pathFromURL = urlUtils.fileURLToPath(url);
const sourceURL = path.normalize(pathFromURL);
const { data: sourceContent } = await fetchFromFilesystem(fs, sourceURL);
return { sourceURL, sourceContent };
}
throw new Error(
`Failed to parse source map: '${url}' URL is not supported`
);
}
// 2. It's a scheme-relative
if (/^\/\//.test(url)) {
throw new Error(
`Failed to parse source map: '${url}' URL is not supported`
);
}
// 3. Absolute path
if (isAbsolute(url)) {
let sourceURL = path.normalize(url);
let sourceContent;
if (!skipReading) {
const possibleRequests = [sourceURL];
if (url.startsWith("/")) {
possibleRequests.push(
getAbsolutePath(fs, context, sourceURL.slice(1), sourceRoot || "")
);
}
const result = await fetchPathsFromFilesystem(fs, possibleRequests);
sourceURL = result.path;
sourceContent = result.data;
}
return { sourceURL, sourceContent };
}
// 4. Relative path
const sourceURL = getAbsolutePath(fs, context, url, sourceRoot || "");
let sourceContent;
if (!skipReading) {
const { data } = await fetchFromFilesystem(fs, sourceURL);
sourceContent = data;
}
return { sourceURL, sourceContent };
}
/**
* Extract source map from code content
* @param {string | Buffer<ArrayBufferLike>} stringOrBuffer The input code content as string or buffer
* @param {string} resourcePath The path to the resource file
* @param {InputFileSystem} fs The file system interface for reading files
* @returns {Promise<{source: string | Buffer<ArrayBufferLike>, sourceMap: string | RawSourceMap | undefined, fileDependencies: string[]}>} Promise resolving to extracted source map information
*/
async function extractSourceMap(stringOrBuffer, resourcePath, fs) {
const input =
typeof stringOrBuffer === "string"
? stringOrBuffer
: stringOrBuffer.toString("utf8");
const inputSourceMap = undefined;
const output = {
source: stringOrBuffer,
sourceMap: inputSourceMap,
fileDependencies: /** @type {string[]} */ ([])
};
const { sourceMappingURL, replacementString } = getSourceMappingURL(input);
if (!sourceMappingURL) {
return output;
}
const baseContext = path.dirname(resourcePath);
const { sourceURL, sourceContent } = await fetchFromURL(
fs,
baseContext,
sourceMappingURL
);
if (sourceURL) {
output.fileDependencies.push(/** @type {string} */ (sourceURL));
}
if (!sourceContent) {
return output;
}
/** @type {RawSourceMap} */
const map = JSON.parse(sourceContent.replace(/^\)\]\}'/, ""));
const context = sourceURL ? path.dirname(sourceURL) : baseContext;
const resolvedSources = await Promise.all(
map.sources.map(
async (/** @type {string} */ source, /** @type {number} */ i) => {
const originalSourceContent =
map.sourcesContent &&
typeof map.sourcesContent[i] !== "undefined" &&
map.sourcesContent[i] !== null
? map.sourcesContent[i]
: undefined;
const skipReading = typeof originalSourceContent !== "undefined";
// We do not skipReading here, because we need absolute paths in sources.
// This is necessary so that for sourceMaps with the same file structure in sources, name collisions do not occur.
// https://github.com/webpack-contrib/source-map-loader/issues/51
let { sourceURL, sourceContent } = await fetchFromURL(
fs,
context,
source,
map.sourceRoot,
skipReading
);
if (skipReading) {
sourceContent = originalSourceContent;
} else if (sourceURL && !isURL(sourceURL)) {
output.fileDependencies.push(/** @type {string} */ (sourceURL));
}
// Return original value of `source` when error happens
return { sourceURL, sourceContent };
}
)
);
/** @type {RawSourceMap} */
const newMap = { ...map };
newMap.sources = [];
newMap.sourcesContent = [];
delete newMap.sourceRoot;
for (const source of resolvedSources) {
const { sourceURL, sourceContent } = source;
newMap.sources.push(sourceURL || "");
newMap.sourcesContent.push(sourceContent || "");
}
const sourcesContentIsEmpty =
newMap.sourcesContent.filter(Boolean).length === 0;
if (sourcesContentIsEmpty) {
delete newMap.sourcesContent;
}
return {
source: input.replace(replacementString, ""),
sourceMap: /** @type {RawSourceMap} */ (newMap),
fileDependencies: output.fileDependencies
};
}
module.exports = extractSourceMap;
module.exports.getSourceMappingURL = getSourceMappingURL;

View File

@ -649,7 +649,15 @@ const lstatReadlinkAbsolute = (fs, p, callback) => {
doReadLink();
};
/**
* @param {string} pathname a path
* @returns {boolean} is absolute
*/
const isAbsolute = (pathname) =>
path.posix.isAbsolute(pathname) || path.win32.isAbsolute(pathname);
module.exports.dirname = dirname;
module.exports.isAbsolute = isAbsolute;
module.exports.join = join;
module.exports.lstatReadlinkAbsolute = lstatReadlinkAbsolute;
module.exports.mkdirp = mkdirp;

File diff suppressed because one or more lines are too long

View File

@ -4734,6 +4734,10 @@
}
]
},
"extractSourceMap": {
"description": "Enable/Disable extracting source map.",
"type": "boolean"
},
"generator": {
"description": "The options for the module generator.",
"type": "object"

View File

@ -212,7 +212,7 @@ describe("Validation", () => {
expect(msg).toMatchInlineSnapshot(`
"Invalid configuration object. Webpack has been initialized using a configuration object that does not match the API schema.
- configuration.module.rules[0].oneOf[0] has an unknown property 'passer'. These properties are valid:
object { assert?, compiler?, dependency?, descriptionData?, enforce?, exclude?, generator?, include?, issuer?, issuerLayer?, layer?, loader?, mimetype?, oneOf?, options?, parser?, realResource?, resolve?, resource?, resourceFragment?, resourceQuery?, rules?, scheme?, sideEffects?, test?, type?, use?, with? }
object { assert?, compiler?, dependency?, descriptionData?, enforce?, exclude?, extractSourceMap?, generator?, include?, issuer?, issuerLayer?, layer?, loader?, mimetype?, oneOf?, options?, parser?, realResource?, resolve?, resource?, resourceFragment?, resourceQuery?, rules?, scheme?, sideEffects?, test?, type?, use?, with? }
-> A rule description with conditions and effects for modules."
`)
);

View File

@ -4748,6 +4748,19 @@ Object {
"multiple": true,
"simpleType": "string",
},
"module-rules-extract-source-map": Object {
"configs": Array [
Object {
"description": "Enable/Disable extracting source map.",
"multiple": true,
"path": "module.rules[].extractSourceMap",
"type": "boolean",
},
],
"description": "Enable/Disable extracting source map.",
"multiple": true,
"simpleType": "boolean",
},
"module-rules-include": Object {
"configs": Array [
Object {

View File

@ -11332,3 +11332,65 @@ const __webpack_exports__a = __webpack_exports__.a;
export { __webpack_exports__HomeLayout as HomeLayout, __webpack_exports__a as a };
"
`;
exports[`ConfigCacheTestCases source-map extract-source-map exported tests should extract source map - 1 1`] = `
Array [
"webpack:///./external-source-map.txt",
"webpack:///external node-commonjs \\"fs\\"",
"webpack:///external node-commonjs \\"path\\"",
"webpack:///webpack/bootstrap",
"webpack:///./extract2.js",
]
`;
exports[`ConfigCacheTestCases source-map extract-source-map exported tests should extract source map - 2 1`] = `
Array [
"webpack:///./external-source-map.txt",
"webpack:///external node-commonjs \\"fs\\"",
"webpack:///external node-commonjs \\"path\\"",
"webpack:///webpack/bootstrap",
"webpack:///./extract3.js",
]
`;
exports[`ConfigCacheTestCases source-map extract-source-map exported tests should extract source map - 3 1`] = `
Array [
"webpack:///./external-source-map.txt",
"webpack:///external node-commonjs \\"fs\\"",
"webpack:///external node-commonjs \\"path\\"",
"webpack:///webpack/bootstrap",
"webpack:///./extract3.js",
]
`;
exports[`ConfigCacheTestCases source-map extract-source-map-css exported tests should compile 1`] = `
Array [
"/*!*********************!*\\\\
!*** css ./app.css ***!
\\\\*********************/
* {
box-sizing: border-box; }
.row {
display: flex;
margin-right: -15px;
margin-left: -15px; }
.col-inner {
display: flex;
align-items: center;
justify-content: center;
color: #fff;
height: 50px;
background: coral; }
.col-s3 {
flex: 0 1 25%;
padding: 0 15px; }
/*# sourceMappingURL=bundle0.css.map*/",
]
`;

View File

@ -10444,3 +10444,65 @@ const __webpack_exports__a = __webpack_exports__.a;
export { __webpack_exports__HomeLayout as HomeLayout, __webpack_exports__a as a };
"
`;
exports[`ConfigTestCases source-map extract-source-map exported tests should extract source map - 1 1`] = `
Array [
"webpack:///./external-source-map.txt",
"webpack:///external node-commonjs \\"fs\\"",
"webpack:///external node-commonjs \\"path\\"",
"webpack:///webpack/bootstrap",
"webpack:///./extract2.js",
]
`;
exports[`ConfigTestCases source-map extract-source-map exported tests should extract source map - 2 1`] = `
Array [
"webpack:///./external-source-map.txt",
"webpack:///external node-commonjs \\"fs\\"",
"webpack:///external node-commonjs \\"path\\"",
"webpack:///webpack/bootstrap",
"webpack:///./extract3.js",
]
`;
exports[`ConfigTestCases source-map extract-source-map exported tests should extract source map - 3 1`] = `
Array [
"webpack:///./external-source-map.txt",
"webpack:///external node-commonjs \\"fs\\"",
"webpack:///external node-commonjs \\"path\\"",
"webpack:///webpack/bootstrap",
"webpack:///./extract3.js",
]
`;
exports[`ConfigTestCases source-map extract-source-map-css exported tests should compile 1`] = `
Array [
"/*!*********************!*\\\\
!*** css ./app.css ***!
\\\\*********************/
* {
box-sizing: border-box; }
.row {
display: flex;
margin-right: -15px;
margin-left: -15px; }
.col-inner {
display: flex;
align-items: center;
justify-content: center;
color: #fff;
height: 50px;
background: coral; }
.col-s3 {
flex: 0 1 25%;
padding: 0 15px; }
/*# sourceMappingURL=bundle0.css.map*/",
]
`;

View File

@ -0,0 +1,29 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`getSourceMappingURL should work with " // # sourceMappingURL = absolute-sourceRoot-source-map.map " url: result 1`] = `"absolute-sourceRoot-source-map.map"`;
exports[`getSourceMappingURL should work with " // #sourceMappingURL=absolute-sourceRoot-source-map.map" url: result 1`] = `"absolute-sourceRoot-source-map.map"`;
exports[`getSourceMappingURL should work with ""\\n /*# sourceMappingURL=data:application/json;base64,"+btoa(unescape(encodeURIComponent(JSON.stringify(sourceMap))))+" */";" url: result 1`] = `""`;
exports[`getSourceMappingURL should work with "/* #sourceMappingURL=absolute-sourceRoot-source-map.map */" url: result 1`] = `"absolute-sourceRoot-source-map.map"`;
exports[`getSourceMappingURL should work with "/*#sourceMappingURL=absolute-sourceRoot-source-map.map*/" url: result 1`] = `"absolute-sourceRoot-source-map.map"`;
exports[`getSourceMappingURL should work with "// # sourceMappingURL=data:application/json;base64,"+btoa(unescape(encodeURIComponent(JSON.stringify(sourceMap))))+"" url: result 1`] = `"data:application/json;base64,"`;
exports[`getSourceMappingURL should work with "// #sourceMappingURL = //hello.com/external-source-map2.map" url: result 1`] = `"//hello.com/external-source-map2.map"`;
exports[`getSourceMappingURL should work with "// #sourceMappingURL = http://hello.com/external-source-map2.map" url: result 1`] = `"http://hello.com/external-source-map2.map"`;
exports[`getSourceMappingURL should work with "// @sourceMappingURL=data:application/source-map;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5saW5lLXNvdXJjZS1tYXAuanMiLCJzb3VyY2VzIjpbImlubGluZS1zb3VyY2UtbWFwLnR4dCJdLCJzb3VyY2VzQ29udGVudCI6WyJ3aXRoIFNvdXJjZU1hcCJdLCJtYXBwaW5ncyI6IkFBQUEifQ==" url: result 1`] = `"data:application/source-map;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5saW5lLXNvdXJjZS1tYXAuanMiLCJzb3VyY2VzIjpbImlubGluZS1zb3VyY2UtbWFwLnR4dCJdLCJzb3VyY2VzQ29udGVudCI6WyJ3aXRoIFNvdXJjZU1hcCJdLCJtYXBwaW5ncyI6IkFBQUEifQ=="`;
exports[`getSourceMappingURL should work with "//#sourceMappingURL=absolute-sourceRoot-source-map.map" url: result 1`] = `"absolute-sourceRoot-source-map.map"`;
exports[`getSourceMappingURL should work with "//@sourceMappingURL=absolute-sourceRoot-source-map.map" url: result 1`] = `"absolute-sourceRoot-source-map.map"`;
exports[`getSourceMappingURL should work with "\\n with SourceMap\\n // #sourceMappingURL = /sample-source-map-1.map\\n // #sourceMappingURL = /sample-source-map-2.map\\n // #sourceMappingURL = /sample-source-map-last.map\\n // comment\\n " url: result 1`] = `"/sample-source-map-last.map"`;
exports[`getSourceMappingURL should work with "\\n with SourceMap\\n \\n // #sourceMappingURL = /sample-source-map.map\\n // comment\\n " url: result 1`] = `"/sample-source-map.map"`;
exports[`getSourceMappingURL should work with "anInvalidDirective = "\\n/*# sourceMappingURL=data:application/json;base64,"+btoa(unescape(encodeURIComponent(JSON.stringify(sourceMap))))+" */";" url: result 1`] = `""`;

View File

@ -0,0 +1,22 @@
* {
box-sizing: border-box; }
.row {
display: flex;
margin-right: -15px;
margin-left: -15px; }
.col-inner {
display: flex;
align-items: center;
justify-content: center;
color: #fff;
height: 50px;
background: coral; }
.col-s3 {
flex: 0 1 25%;
padding: 0 15px; }
/*# sourceMappingURL=app.css.map*/

View File

@ -0,0 +1 @@
{"version":3,"sources":["webpack:///./src/app/app.scss"],"names":[],"mappings":"AAAA;EACE,sBAAsB;;AAGxB;EACE,aAAa;EACb,mBAAmB;EACnB,kBAAkB;;AAIlB;EACE,aAAa;EACb,mBAAmB;EACnB,uBAAuB;EACvB,WAAW;EACX,YAAY;EACZ,iBAAiB;;AAInB;EACE,aAAa;EACb,eAAe","file":"app.css","sourcesContent":["* {\n box-sizing: border-box;\n}\n\n.row {\n display: flex;\n margin-right: -15px;\n margin-left: -15px;\n}\n\n.col {\n &-inner {\n display: flex;\n align-items: center;\n justify-content: center;\n color: #fff;\n height: 50px;\n background: coral;\n //background: red;\n }\n\n &-s3 {\n flex: 0 1 25%;\n padding: 0 15px;\n }\n}\n"],"sourceRoot":""}

View File

@ -0,0 +1,14 @@
import "./app.css";
it("should compile", done => {
const links = document.getElementsByTagName("link");
const css = [];
// Skip first because import it by default
for (const link of links.slice(1)) {
css.push(link.sheet.css);
}
expect(css).toMatchSnapshot();
done();
});

View File

@ -0,0 +1,10 @@
"use strict";
module.exports = {
moduleScope(scope) {
const link = scope.window.document.createElement("link");
link.rel = "stylesheet";
link.href = "bundle0.css";
scope.window.document.head.appendChild(link);
}
};

View File

@ -0,0 +1,20 @@
"use strict";
/** @type {import("../../../../").Configuration} */
module.exports = {
target: "web",
mode: "development",
devtool: "source-map",
module: {
rules: [
{
test: /\.css$/i,
type: "css",
extractSourceMap: true
}
]
},
experiments: {
css: true
}
};

View File

@ -0,0 +1,14 @@
"use strict";
const fs = require("fs");
const path = require("path");
require("./test1");
require("./no-source-map")
it("should extract source map - 1", () => {
const fileData = fs.readFileSync(path.resolve(__dirname, "bundle1.js.map")).toString("utf-8");
const { sources } = JSON.parse(fileData);
expect(sources).toMatchSnapshot();
expect(1).toBe(1)
});

View File

@ -0,0 +1,12 @@
"use strict";
const fs = require("fs");
const path = require("path");
require("./test2");
it("should extract source map - 2", () => {
const fileData = fs.readFileSync(path.resolve(__dirname, "bundle2.js.map")).toString("utf-8");
const { sources } = JSON.parse(fileData);
expect(sources).toMatchSnapshot();
});

View File

@ -0,0 +1,12 @@
"use strict";
const fs = require("fs");
const path = require("path");
require("./test3");
it("should extract source map - 3", () => {
const fileData = fs.readFileSync(path.resolve(__dirname, "bundle2.js.map")).toString("utf-8");
const { sources } = JSON.parse(fileData);
expect(sources).toMatchSnapshot();
});

View File

@ -0,0 +1,5 @@
"use strict";
module.exports = [
/^Pack got invalid because of write to: Compilation\/modules.+no-source-map\.js$/
];

View File

@ -0,0 +1,2 @@
const a = 1;
//#sourceMappingURL=no-source-map.map

View File

@ -0,0 +1,11 @@
"use strict";
const fs = require("fs");
require("./test1");
it("should remove sourceMap comment", () => {
expect(
fs.readFileSync(__filename).toString("utf-8")
).not.toMatch(/\/\/\s*@\s*sourceMappingURL/);
});

View File

@ -0,0 +1,3 @@
const a = 1;
// @ sourceMappingURL = data:application/source-map;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2hhcnNldC1pbmxpbmUtc291cmNlLW1hcC5qcyIsInNvdXJjZXMiOlsiY2hhcnNldC1pbmxpbmUtc291cmNlLW1hcC50eHQiXSwic291cmNlc0NvbnRlbnQiOlsid2l0aCBTb3VyY2VNYXAiXSwibWFwcGluZ3MiOiJBQUFBIn0=
// comment

View File

@ -0,0 +1,3 @@
const a = 1;
// comment
//#sourceMappingURL=test2.map

View File

@ -0,0 +1 @@
{"version":3,"file":"test2.js","sources":["external-source-map.txt"],"sourcesContent":["with SourceMap"],"mappings":"AAAA"}

View File

@ -0,0 +1,3 @@
const a = 1;
// comment
//#sourceMappingURL=/test2.map

View File

@ -0,0 +1,3 @@
"use strict";
module.exports = [[/Failed to parse source map/]];

View File

@ -0,0 +1,52 @@
"use strict";
/** @type {import("../../../../").Configuration[]} */
module.exports = [
{
target: "node",
entry: "./extract1",
devtool: "source-map",
module: {
rules: [
{
extractSourceMap: true
}
]
}
},
{
target: "node",
entry: "./extract2",
devtool: "source-map",
module: {
rules: [
{
extractSourceMap: true
}
]
}
},
{
target: "node",
entry: "./extract3",
devtool: "source-map",
module: {
rules: [
{
extractSourceMap: true
}
]
}
},
{
entry: "./remove-comment",
devtool: "source-map",
module: {
rules: [
{
extractSourceMap: true
}
]
}
}
];

View File

@ -0,0 +1,3 @@
const a = 1;
// comment
//#sourceMappingURL=/a.map

View File

@ -0,0 +1 @@
{"version":3,"file":"test2.js","sources":["external-source-map.txt"],"sourcesContent":[],"mappings":"AAAA"}

View File

@ -0,0 +1,27 @@
/** @typedef {import("webpack").LoaderContext<void>} LoaderContext */
const assert = require("assert")
/**
* @this {LoaderContext}
* @param {string} source The source code to process
* @param {import("webpack-sources").RawSourceMap} sourceMap The source map to process
* @returns {void}
*/
module.exports = function(source, sourceMap) {
const callback = this.async();
const resourcePath = this.resourcePath;
if (resourcePath.endsWith("a.js")) {
assert(sourceMap && sourceMap.version && sourceMap.mappings, "should have source map when extract source map");
}
try {
const withoutConst = source.replace(/const/g, "var");
callback(null, withoutConst, sourceMap);
} catch (err) {
callback(/** @type {Error} */ (err));
}
};

View File

@ -0,0 +1,11 @@
const fs = require("fs");
const path = require("path");
require("./a");
it("should extract source map", () => {
const fileData = fs.readFileSync(path.resolve(__dirname, "bundle0.js.map")).toString("utf-8");
const { sources, sourcesContent } = JSON.parse(fileData);
expect(sources.includes("webpack:///./external-source-map.txt")).toBe(true);
expect(sourcesContent.map(s => s.trim())).toContain("source");
});

View File

@ -0,0 +1,5 @@
"use strict";
module.exports = function filter() {
return process.platform !== "win32";
};

View File

@ -0,0 +1,17 @@
"use strict";
/** @type {import("../../../../").Configuration} */
module.exports = {
target: "node",
entry: "./index",
devtool: "source-map",
module: {
rules: [
{
test: /\.js$/,
extractSourceMap: true,
loader: require.resolve("./babel-loader")
}
]
}
};

View File

@ -0,0 +1,42 @@
"use strict";
const { getSourceMappingURL } = require("../lib/util/extractSourceMap");
describe("getSourceMappingURL", () => {
const cases = [
"/*#sourceMappingURL=absolute-sourceRoot-source-map.map*/",
"/* #sourceMappingURL=absolute-sourceRoot-source-map.map */",
"//#sourceMappingURL=absolute-sourceRoot-source-map.map",
"//@sourceMappingURL=absolute-sourceRoot-source-map.map",
" // #sourceMappingURL=absolute-sourceRoot-source-map.map",
" // # sourceMappingURL = absolute-sourceRoot-source-map.map ",
"// #sourceMappingURL = http://hello.com/external-source-map2.map",
"// #sourceMappingURL = //hello.com/external-source-map2.map",
"// @sourceMappingURL=data:application/source-map;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5saW5lLXNvdXJjZS1tYXAuanMiLCJzb3VyY2VzIjpbImlubGluZS1zb3VyY2UtbWFwLnR4dCJdLCJzb3VyY2VzQ29udGVudCI6WyJ3aXRoIFNvdXJjZU1hcCJdLCJtYXBwaW5ncyI6IkFBQUEifQ==",
`
with SourceMap
// #sourceMappingURL = /sample-source-map.map
// comment
`,
`
with SourceMap
// #sourceMappingURL = /sample-source-map-1.map
// #sourceMappingURL = /sample-source-map-2.map
// #sourceMappingURL = /sample-source-map-last.map
// comment
`,
`"
/*# sourceMappingURL=data:application/json;base64,"+btoa(unescape(encodeURIComponent(JSON.stringify(sourceMap))))+" */";`,
'anInvalidDirective = "\\n/*# sourceMappingURL=data:application/json;base64,"+btoa(unescape(encodeURIComponent(JSON.stringify(sourceMap))))+" */";',
'// # sourceMappingURL=data:application/json;base64,"+btoa(unescape(encodeURIComponent(JSON.stringify(sourceMap))))+"'
];
for (const item of cases) {
it(`should work with "${item}" url`, async () => {
const { sourceMappingURL } = getSourceMappingURL(item);
expect(sourceMappingURL).toMatchSnapshot("result");
});
}
});

16
types.d.ts vendored
View File

@ -10713,6 +10713,11 @@ declare interface ModuleSettings {
*/
type?: string;
/**
* Enable/Disable extracting source map.
*/
extractSourceMap?: boolean;
/**
* Options for the resolver.
*/
@ -11020,6 +11025,7 @@ declare class NormalModule extends Module {
resourceResolveData?: ResourceSchemeData & Partial<ResolveRequest>;
matchResource?: string;
loaders: LoaderItem[];
extractSourceMap?: boolean;
error: null | WebpackError;
/**
@ -11169,6 +11175,11 @@ declare interface NormalModuleCreateData {
* options used for resolving requests from this module
*/
resolveOptions?: ResolveOptions;
/**
* enable/disable extracting source map
*/
extractSourceMap?: boolean;
}
declare abstract class NormalModuleFactory extends ModuleFactory {
hooks: Readonly<{
@ -15080,6 +15091,11 @@ declare interface RuleSetRule {
| RuleSetLogicalConditionsAbsolute
| RuleSetConditionAbsolute[];
/**
* Enable/Disable extracting source map.
*/
extractSourceMap?: boolean;
/**
* The options for the module generator.
*/