/* 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, resourcePath: string, fs: InputFileSystem) => Promise<{source: string | Buffer, sourceMap: string | RawSourceMap | undefined, fileDependencies: string[]}>} SourceMapExtractorFunction */ /** @typedef {import("webpack-sources").RawSourceMap} RawSourceMap */ /** * @typedef {(resourcePath: string) => Promise>} 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}>} 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} 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, 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;