extractSourceMap.js 8.4 KB

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