extractSourceMap.js 8.4 KB

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