mirror of https://github.com/webpack/webpack.git
				
				
				
			
		
			
				
	
	
		
			320 lines
		
	
	
		
			8.2 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
			
		
		
	
	
			320 lines
		
	
	
		
			8.2 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
| /*
 | |
| 	MIT License http://www.opensource.org/licenses/mit-license.php
 | |
| 	Author Natsu @xiaoxiaojx
 | |
| */
 | |
| 
 | |
| "use strict";
 | |
| 
 | |
| const path = require("path");
 | |
| const urlUtils = require("url");
 | |
| const { isAbsolute, join } = require("./fs");
 | |
| 
 | |
| /** @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 {(resourcePath: string) => Promise<string | Buffer<ArrayBufferLike>>} ReadResource
 | |
|  */
 | |
| 
 | |
| /**
 | |
|  * @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 {string} context context directory
 | |
|  * @param {string} request file request
 | |
|  * @param {string} sourceRoot source root directory
 | |
|  * @returns {string} absolute path
 | |
|  */
 | |
| function getAbsolutePath(context, request, sourceRoot) {
 | |
| 	if (sourceRoot) {
 | |
| 		if (isAbsolute(sourceRoot)) {
 | |
| 			return join(undefined, sourceRoot, request);
 | |
| 		}
 | |
| 
 | |
| 		return join(undefined, join(undefined, context, sourceRoot), request);
 | |
| 	}
 | |
| 
 | |
| 	return join(undefined, 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 from multiple possible file paths
 | |
|  * @param {ReadResource} readResource read resource function
 | |
|  * @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 fetchPathsFromURL(
 | |
| 	readResource,
 | |
| 	possibleRequests,
 | |
| 	errorsAccumulator = ""
 | |
| ) {
 | |
| 	let result;
 | |
| 
 | |
| 	try {
 | |
| 		result = await readResource(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 fetchPathsFromURL(
 | |
| 			readResource,
 | |
| 			tailPossibleRequests,
 | |
| 			errorsAccumulator
 | |
| 		);
 | |
| 	}
 | |
| 
 | |
| 	return {
 | |
| 		path: possibleRequests[0],
 | |
| 		data: result.toString("utf8")
 | |
| 	};
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Fetch source content from URL
 | |
|  * @param {ReadResource} readResource The read resource function
 | |
|  * @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 | Buffer<ArrayBufferLike>}>} source content promise
 | |
|  */
 | |
| async function fetchFromURL(
 | |
| 	readResource,
 | |
| 	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:") {
 | |
| 			const sourceContent = skipReading ? "" : await readResource(url);
 | |
| 
 | |
| 			return { sourceURL: "", sourceContent };
 | |
| 		}
 | |
| 
 | |
| 		if (protocol === "file:") {
 | |
| 			const pathFromURL = urlUtils.fileURLToPath(url);
 | |
| 			const sourceURL = path.normalize(pathFromURL);
 | |
| 			const sourceContent = skipReading ? "" : await readResource(sourceURL);
 | |
| 
 | |
| 			return { sourceURL, sourceContent };
 | |
| 		}
 | |
| 
 | |
| 		const sourceContent = skipReading ? "" : await readResource(url);
 | |
| 		return { sourceURL: url, sourceContent };
 | |
| 	}
 | |
| 
 | |
| 	// 3. Absolute path
 | |
| 	if (isAbsolute(url)) {
 | |
| 		let sourceURL = path.normalize(url);
 | |
| 
 | |
| 		let sourceContent;
 | |
| 
 | |
| 		if (!skipReading) {
 | |
| 			const possibleRequests = [sourceURL];
 | |
| 
 | |
| 			if (url.startsWith("/")) {
 | |
| 				possibleRequests.push(
 | |
| 					getAbsolutePath(context, sourceURL.slice(1), sourceRoot || "")
 | |
| 				);
 | |
| 			}
 | |
| 
 | |
| 			const result = await fetchPathsFromURL(readResource, possibleRequests);
 | |
| 
 | |
| 			sourceURL = result.path;
 | |
| 			sourceContent = result.data;
 | |
| 		}
 | |
| 
 | |
| 		return { sourceURL, sourceContent };
 | |
| 	}
 | |
| 
 | |
| 	// 4. Relative path
 | |
| 	const sourceURL = getAbsolutePath(context, url, sourceRoot || "");
 | |
| 	let sourceContent;
 | |
| 
 | |
| 	if (!skipReading) {
 | |
| 		sourceContent = await readResource(sourceURL);
 | |
| 	}
 | |
| 
 | |
| 	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 {ReadResource} readResource The read resource function
 | |
|  * @returns {Promise<{source: string | Buffer<ArrayBufferLike>, sourceMap: string | RawSourceMap | undefined}>} Promise resolving to extracted source map information
 | |
|  */
 | |
| async function extractSourceMap(stringOrBuffer, resourcePath, readResource) {
 | |
| 	const input =
 | |
| 		typeof stringOrBuffer === "string"
 | |
| 			? stringOrBuffer
 | |
| 			: stringOrBuffer.toString("utf8");
 | |
| 	const inputSourceMap = undefined;
 | |
| 	const output = {
 | |
| 		source: stringOrBuffer,
 | |
| 		sourceMap: inputSourceMap
 | |
| 	};
 | |
| 	const { sourceMappingURL, replacementString } = getSourceMappingURL(input);
 | |
| 
 | |
| 	if (!sourceMappingURL) {
 | |
| 		return output;
 | |
| 	}
 | |
| 
 | |
| 	const baseContext = path.dirname(resourcePath);
 | |
| 
 | |
| 	const { sourceURL, sourceContent } = await fetchFromURL(
 | |
| 		readResource,
 | |
| 		baseContext,
 | |
| 		sourceMappingURL
 | |
| 	);
 | |
| 
 | |
| 	if (!sourceContent) {
 | |
| 		return output;
 | |
| 	}
 | |
| 
 | |
| 	/** @type {RawSourceMap} */
 | |
| 	const map = JSON.parse(
 | |
| 		sourceContent.toString("utf8").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(
 | |
| 					readResource,
 | |
| 					context,
 | |
| 					source,
 | |
| 					map.sourceRoot,
 | |
| 					skipReading
 | |
| 				);
 | |
| 
 | |
| 				if (skipReading) {
 | |
| 					sourceContent = originalSourceContent;
 | |
| 				}
 | |
| 
 | |
| 				// 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 ? sourceContent.toString("utf8") : ""
 | |
| 		);
 | |
| 	}
 | |
| 
 | |
| 	const sourcesContentIsEmpty =
 | |
| 		newMap.sourcesContent.filter(Boolean).length === 0;
 | |
| 
 | |
| 	if (sourcesContentIsEmpty) {
 | |
| 		delete newMap.sourcesContent;
 | |
| 	}
 | |
| 
 | |
| 	return {
 | |
| 		source: input.replace(replacementString, ""),
 | |
| 		sourceMap: /** @type {RawSourceMap} */ (newMap)
 | |
| 	};
 | |
| }
 | |
| 
 | |
| module.exports = extractSourceMap;
 | |
| module.exports.getSourceMappingURL = getSourceMappingURL;
 |