| [9af201e] | 1 | /*
|
|---|
| 2 | MIT License http://www.opensource.org/licenses/mit-license.php
|
|---|
| 3 | Author Natsu @xiaoxiaojx
|
|---|
| 4 | */
|
|---|
| 5 |
|
|---|
| 6 | "use strict";
|
|---|
| 7 |
|
|---|
| 8 | const path = require("path");
|
|---|
| 9 | const urlUtils = require("url");
|
|---|
| 10 | const { isAbsolute, join } = require("./fs");
|
|---|
| 11 |
|
|---|
| 12 | /** @typedef {import("./fs").InputFileSystem} InputFileSystem */
|
|---|
| 13 | /** @typedef {string | Buffer<ArrayBufferLike>} StringOrBuffer */
|
|---|
| 14 | /** @typedef {(input: StringOrBuffer, resourcePath: string, fs: InputFileSystem) => Promise<{ source: StringOrBuffer, sourceMap: string | RawSourceMap | undefined, fileDependencies: string[] }>} SourceMapExtractorFunction */
|
|---|
| 15 | /** @typedef {import("webpack-sources").RawSourceMap} RawSourceMap */
|
|---|
| 16 | /** @typedef {(resourcePath: string) => Promise<StringOrBuffer>} ReadResource */
|
|---|
| 17 |
|
|---|
| 18 | /**
|
|---|
| 19 | * Defines the source mapping url type used by this module.
|
|---|
| 20 | * @typedef {object} SourceMappingURL
|
|---|
| 21 | * @property {string} sourceMappingURL
|
|---|
| 22 | * @property {string} replacementString
|
|---|
| 23 | */
|
|---|
| 24 |
|
|---|
| 25 | // Matches only the last occurrence of sourceMappingURL
|
|---|
| 26 | const innerRegex = /\s*[#@]\s*sourceMappingURL\s*=\s*([^\s'"]*)\s*/;
|
|---|
| 27 |
|
|---|
| 28 | const validProtocolPattern = /^[a-z][a-z0-9+.-]*:/i;
|
|---|
| 29 |
|
|---|
| 30 | const sourceMappingURLRegex = new RegExp(
|
|---|
| 31 | "(?:" +
|
|---|
| 32 | "/\\*" +
|
|---|
| 33 | "(?:\\s*\r?\n(?://)?)?" +
|
|---|
| 34 | `(?:${innerRegex.source})` +
|
|---|
| 35 | "\\s*" +
|
|---|
| 36 | "\\*/" +
|
|---|
| 37 | "|" +
|
|---|
| 38 | `//(?:${innerRegex.source})` +
|
|---|
| 39 | ")" +
|
|---|
| 40 | "\\s*"
|
|---|
| 41 | );
|
|---|
| 42 |
|
|---|
| 43 | /**
|
|---|
| 44 | * Extract source mapping URL from code comments
|
|---|
| 45 | * @param {string} code source code content
|
|---|
| 46 | * @returns {SourceMappingURL} source mapping information
|
|---|
| 47 | */
|
|---|
| 48 | function getSourceMappingURL(code) {
|
|---|
| 49 | const lines = code.split(/^/m);
|
|---|
| 50 | /** @type {RegExpMatchArray | null | undefined} */
|
|---|
| 51 | let match;
|
|---|
| 52 |
|
|---|
| 53 | for (let i = lines.length - 1; i >= 0; i--) {
|
|---|
| 54 | match = lines[i].match(sourceMappingURLRegex);
|
|---|
| 55 | if (match) {
|
|---|
| 56 | break;
|
|---|
| 57 | }
|
|---|
| 58 | }
|
|---|
| 59 |
|
|---|
| 60 | const sourceMappingURL = match ? match[1] || match[2] || "" : "";
|
|---|
| 61 |
|
|---|
| 62 | return {
|
|---|
| 63 | sourceMappingURL: sourceMappingURL
|
|---|
| 64 | ? decodeURI(sourceMappingURL)
|
|---|
| 65 | : sourceMappingURL,
|
|---|
| 66 | replacementString: match ? match[0] : ""
|
|---|
| 67 | };
|
|---|
| 68 | }
|
|---|
| 69 |
|
|---|
| 70 | /**
|
|---|
| 71 | * Get absolute path for source file
|
|---|
| 72 | * @param {string} context context directory
|
|---|
| 73 | * @param {string} request file request
|
|---|
| 74 | * @param {string} sourceRoot source root directory
|
|---|
| 75 | * @returns {string} absolute path
|
|---|
| 76 | */
|
|---|
| 77 | function getAbsolutePath(context, request, sourceRoot) {
|
|---|
| 78 | if (sourceRoot) {
|
|---|
| 79 | if (isAbsolute(sourceRoot)) {
|
|---|
| 80 | return join(undefined, sourceRoot, request);
|
|---|
| 81 | }
|
|---|
| 82 |
|
|---|
| 83 | return join(undefined, join(undefined, context, sourceRoot), request);
|
|---|
| 84 | }
|
|---|
| 85 |
|
|---|
| 86 | return join(undefined, context, request);
|
|---|
| 87 | }
|
|---|
| 88 |
|
|---|
| 89 | /**
|
|---|
| 90 | * Check if value is a URL
|
|---|
| 91 | * @param {string} value string to check
|
|---|
| 92 | * @returns {boolean} true if value is a URL
|
|---|
| 93 | */
|
|---|
| 94 | function isURL(value) {
|
|---|
| 95 | return validProtocolPattern.test(value) && !path.win32.isAbsolute(value);
|
|---|
| 96 | }
|
|---|
| 97 |
|
|---|
| 98 | /**
|
|---|
| 99 | * Fetch from multiple possible file paths
|
|---|
| 100 | * @param {ReadResource} readResource read resource function
|
|---|
| 101 | * @param {string[]} possibleRequests array of possible file paths
|
|---|
| 102 | * @param {string} errorsAccumulator accumulated error messages
|
|---|
| 103 | * @returns {Promise<{ path: string, data?: string }>} source content promise
|
|---|
| 104 | */
|
|---|
| 105 | async function fetchPathsFromURL(
|
|---|
| 106 | readResource,
|
|---|
| 107 | possibleRequests,
|
|---|
| 108 | errorsAccumulator = ""
|
|---|
| 109 | ) {
|
|---|
| 110 | /** @type {StringOrBuffer} */
|
|---|
| 111 | let result;
|
|---|
| 112 |
|
|---|
| 113 | try {
|
|---|
| 114 | result = await readResource(possibleRequests[0]);
|
|---|
| 115 | } catch (error) {
|
|---|
| 116 | errorsAccumulator += `${/** @type {Error} */ (error).message}\n\n`;
|
|---|
| 117 |
|
|---|
| 118 | const [, ...tailPossibleRequests] = possibleRequests;
|
|---|
| 119 |
|
|---|
| 120 | if (tailPossibleRequests.length === 0) {
|
|---|
| 121 | /** @type {Error} */ (error).message = errorsAccumulator;
|
|---|
| 122 |
|
|---|
| 123 | throw error;
|
|---|
| 124 | }
|
|---|
| 125 |
|
|---|
| 126 | return fetchPathsFromURL(
|
|---|
| 127 | readResource,
|
|---|
| 128 | tailPossibleRequests,
|
|---|
| 129 | errorsAccumulator
|
|---|
| 130 | );
|
|---|
| 131 | }
|
|---|
| 132 |
|
|---|
| 133 | return {
|
|---|
| 134 | path: possibleRequests[0],
|
|---|
| 135 | data: result.toString("utf8")
|
|---|
| 136 | };
|
|---|
| 137 | }
|
|---|
| 138 |
|
|---|
| 139 | /**
|
|---|
| 140 | * Fetch source content from URL
|
|---|
| 141 | * @param {ReadResource} readResource The read resource function
|
|---|
| 142 | * @param {string} context context directory
|
|---|
| 143 | * @param {string} url source URL
|
|---|
| 144 | * @param {string=} sourceRoot source root directory
|
|---|
| 145 | * @param {boolean=} skipReading whether to skip reading file content
|
|---|
| 146 | * @returns {Promise<{ sourceURL: string, sourceContent?: StringOrBuffer }>} source content promise
|
|---|
| 147 | */
|
|---|
| 148 | async function fetchFromURL(
|
|---|
| 149 | readResource,
|
|---|
| 150 | context,
|
|---|
| 151 | url,
|
|---|
| 152 | sourceRoot,
|
|---|
| 153 | skipReading = false
|
|---|
| 154 | ) {
|
|---|
| 155 | // 1. It's an absolute url and it is not `windows` path like `C:\dir\file`
|
|---|
| 156 | if (isURL(url)) {
|
|---|
| 157 | // eslint-disable-next-line n/no-deprecated-api
|
|---|
| 158 | const { protocol } = urlUtils.parse(url);
|
|---|
| 159 | if (protocol === "data:") {
|
|---|
| 160 | const sourceContent = skipReading ? "" : await readResource(url);
|
|---|
| 161 |
|
|---|
| 162 | return { sourceURL: "", sourceContent };
|
|---|
| 163 | }
|
|---|
| 164 |
|
|---|
| 165 | if (protocol === "file:") {
|
|---|
| 166 | const pathFromURL = urlUtils.fileURLToPath(url);
|
|---|
| 167 | const sourceURL = path.normalize(pathFromURL);
|
|---|
| 168 | const sourceContent = skipReading ? "" : await readResource(sourceURL);
|
|---|
| 169 |
|
|---|
| 170 | return { sourceURL, sourceContent };
|
|---|
| 171 | }
|
|---|
| 172 |
|
|---|
| 173 | const sourceContent = skipReading ? "" : await readResource(url);
|
|---|
| 174 | return { sourceURL: url, sourceContent };
|
|---|
| 175 | }
|
|---|
| 176 |
|
|---|
| 177 | // 3. Absolute path
|
|---|
| 178 | if (isAbsolute(url)) {
|
|---|
| 179 | let sourceURL = path.normalize(url);
|
|---|
| 180 |
|
|---|
| 181 | /** @type {undefined | StringOrBuffer} */
|
|---|
| 182 | let sourceContent;
|
|---|
| 183 |
|
|---|
| 184 | if (!skipReading) {
|
|---|
| 185 | /** @type {string[]} */
|
|---|
| 186 | const possibleRequests = [sourceURL];
|
|---|
| 187 |
|
|---|
| 188 | if (url.startsWith("/")) {
|
|---|
| 189 | possibleRequests.push(
|
|---|
| 190 | getAbsolutePath(context, sourceURL.slice(1), sourceRoot || "")
|
|---|
| 191 | );
|
|---|
| 192 | }
|
|---|
| 193 |
|
|---|
| 194 | const result = await fetchPathsFromURL(readResource, possibleRequests);
|
|---|
| 195 |
|
|---|
| 196 | sourceURL = result.path;
|
|---|
| 197 | sourceContent = result.data;
|
|---|
| 198 | }
|
|---|
| 199 |
|
|---|
| 200 | return { sourceURL, sourceContent };
|
|---|
| 201 | }
|
|---|
| 202 |
|
|---|
| 203 | // 4. Relative path
|
|---|
| 204 | const sourceURL = getAbsolutePath(context, url, sourceRoot || "");
|
|---|
| 205 | /** @type {undefined | StringOrBuffer} */
|
|---|
| 206 | let sourceContent;
|
|---|
| 207 |
|
|---|
| 208 | if (!skipReading) {
|
|---|
| 209 | sourceContent = await readResource(sourceURL);
|
|---|
| 210 | }
|
|---|
| 211 |
|
|---|
| 212 | return { sourceURL, sourceContent };
|
|---|
| 213 | }
|
|---|
| 214 |
|
|---|
| 215 | /**
|
|---|
| 216 | * Extract source map from code content
|
|---|
| 217 | * @param {StringOrBuffer} stringOrBuffer The input code content as string or buffer
|
|---|
| 218 | * @param {string} resourcePath The path to the resource file
|
|---|
| 219 | * @param {ReadResource} readResource The read resource function
|
|---|
| 220 | * @returns {Promise<{ source: StringOrBuffer, sourceMap: string | RawSourceMap | undefined }>} Promise resolving to extracted source map information
|
|---|
| 221 | */
|
|---|
| 222 | async function extractSourceMap(stringOrBuffer, resourcePath, readResource) {
|
|---|
| 223 | const input =
|
|---|
| 224 | typeof stringOrBuffer === "string"
|
|---|
| 225 | ? stringOrBuffer
|
|---|
| 226 | : stringOrBuffer.toString("utf8");
|
|---|
| 227 | const inputSourceMap = undefined;
|
|---|
| 228 | const output = {
|
|---|
| 229 | source: stringOrBuffer,
|
|---|
| 230 | sourceMap: inputSourceMap
|
|---|
| 231 | };
|
|---|
| 232 | const { sourceMappingURL, replacementString } = getSourceMappingURL(input);
|
|---|
| 233 |
|
|---|
| 234 | if (!sourceMappingURL) {
|
|---|
| 235 | return output;
|
|---|
| 236 | }
|
|---|
| 237 |
|
|---|
| 238 | const baseContext = path.dirname(resourcePath);
|
|---|
| 239 |
|
|---|
| 240 | const { sourceURL, sourceContent } = await fetchFromURL(
|
|---|
| 241 | readResource,
|
|---|
| 242 | baseContext,
|
|---|
| 243 | sourceMappingURL
|
|---|
| 244 | );
|
|---|
| 245 |
|
|---|
| 246 | if (!sourceContent) {
|
|---|
| 247 | return output;
|
|---|
| 248 | }
|
|---|
| 249 |
|
|---|
| 250 | /** @type {RawSourceMap} */
|
|---|
| 251 | const map = JSON.parse(
|
|---|
| 252 | sourceContent.toString("utf8").replace(/^\)\]\}'/, "")
|
|---|
| 253 | );
|
|---|
| 254 |
|
|---|
| 255 | const context = sourceURL ? path.dirname(sourceURL) : baseContext;
|
|---|
| 256 |
|
|---|
| 257 | const resolvedSources = await Promise.all(
|
|---|
| 258 | map.sources.map(
|
|---|
| 259 | async (/** @type {string} */ source, /** @type {number} */ i) => {
|
|---|
| 260 | const originalSourceContent =
|
|---|
| 261 | map.sourcesContent &&
|
|---|
| 262 | typeof map.sourcesContent[i] !== "undefined" &&
|
|---|
| 263 | map.sourcesContent[i] !== null
|
|---|
| 264 | ? map.sourcesContent[i]
|
|---|
| 265 | : undefined;
|
|---|
| 266 | const skipReading = typeof originalSourceContent !== "undefined";
|
|---|
| 267 | // We do not skipReading here, because we need absolute paths in sources.
|
|---|
| 268 | // This is necessary so that for sourceMaps with the same file structure in sources, name collisions do not occur.
|
|---|
| 269 | // https://github.com/webpack-contrib/source-map-loader/issues/51
|
|---|
| 270 | let { sourceURL, sourceContent } = await fetchFromURL(
|
|---|
| 271 | readResource,
|
|---|
| 272 | context,
|
|---|
| 273 | source,
|
|---|
| 274 | map.sourceRoot,
|
|---|
| 275 | skipReading
|
|---|
| 276 | );
|
|---|
| 277 |
|
|---|
| 278 | if (skipReading) {
|
|---|
| 279 | sourceContent = originalSourceContent;
|
|---|
| 280 | }
|
|---|
| 281 |
|
|---|
| 282 | // Return original value of `source` when error happens
|
|---|
| 283 | return { sourceURL, sourceContent };
|
|---|
| 284 | }
|
|---|
| 285 | )
|
|---|
| 286 | );
|
|---|
| 287 |
|
|---|
| 288 | /** @type {RawSourceMap} */
|
|---|
| 289 | const newMap = { ...map };
|
|---|
| 290 |
|
|---|
| 291 | newMap.sources = [];
|
|---|
| 292 | newMap.sourcesContent = [];
|
|---|
| 293 |
|
|---|
| 294 | delete newMap.sourceRoot;
|
|---|
| 295 |
|
|---|
| 296 | for (const source of resolvedSources) {
|
|---|
| 297 | const { sourceURL, sourceContent } = source;
|
|---|
| 298 |
|
|---|
| 299 | newMap.sources.push(sourceURL || "");
|
|---|
| 300 | newMap.sourcesContent.push(
|
|---|
| 301 | sourceContent ? sourceContent.toString("utf8") : ""
|
|---|
| 302 | );
|
|---|
| 303 | }
|
|---|
| 304 |
|
|---|
| 305 | const sourcesContentIsEmpty =
|
|---|
| 306 | newMap.sourcesContent.filter(Boolean).length === 0;
|
|---|
| 307 |
|
|---|
| 308 | if (sourcesContentIsEmpty) {
|
|---|
| 309 | delete newMap.sourcesContent;
|
|---|
| 310 | }
|
|---|
| 311 |
|
|---|
| 312 | return {
|
|---|
| 313 | source: input.replace(replacementString, ""),
|
|---|
| 314 | sourceMap: /** @type {RawSourceMap} */ (newMap)
|
|---|
| 315 | };
|
|---|
| 316 | }
|
|---|
| 317 |
|
|---|
| 318 | module.exports = extractSourceMap;
|
|---|
| 319 | module.exports.getSourceMappingURL = getSourceMappingURL;
|
|---|