trace-mapping.mjs 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  1. import { decode, encode } from '@jridgewell/sourcemap-codec';
  2. import resolveUri from '@jridgewell/resolve-uri';
  3. function resolve(input, base) {
  4. // The base is always treated as a directory, if it's not empty.
  5. // https://github.com/mozilla/source-map/blob/8cb3ee57/lib/util.js#L327
  6. // https://github.com/chromium/chromium/blob/da4adbb3/third_party/blink/renderer/devtools/front_end/sdk/SourceMap.js#L400-L401
  7. if (base && !base.endsWith('/'))
  8. base += '/';
  9. return resolveUri(input, base);
  10. }
  11. /**
  12. * Removes everything after the last "/", but leaves the slash.
  13. */
  14. function stripFilename(path) {
  15. if (!path)
  16. return '';
  17. const index = path.lastIndexOf('/');
  18. return path.slice(0, index + 1);
  19. }
  20. function maybeSort(mappings, owned) {
  21. const unsortedIndex = nextUnsortedSegmentLine(mappings, 0);
  22. if (unsortedIndex === mappings.length)
  23. return mappings;
  24. // If we own the array (meaning we parsed it from JSON), then we're free to directly mutate it. If
  25. // not, we do not want to modify the consumer's input array.
  26. if (!owned)
  27. mappings = mappings.slice();
  28. for (let i = unsortedIndex; i < mappings.length; i = nextUnsortedSegmentLine(mappings, i + 1)) {
  29. mappings[i] = sortSegments(mappings[i], owned);
  30. }
  31. return mappings;
  32. }
  33. function nextUnsortedSegmentLine(mappings, start) {
  34. for (let i = start; i < mappings.length; i++) {
  35. if (!isSorted(mappings[i]))
  36. return i;
  37. }
  38. return mappings.length;
  39. }
  40. function isSorted(line) {
  41. for (let j = 1; j < line.length; j++) {
  42. if (line[j][0] < line[j - 1][0]) {
  43. return false;
  44. }
  45. }
  46. return true;
  47. }
  48. function sortSegments(line, owned) {
  49. if (!owned)
  50. line = line.slice();
  51. return line.sort(sortComparator);
  52. }
  53. function sortComparator(a, b) {
  54. return a[0] - b[0];
  55. }
  56. /**
  57. * A binary search implementation that returns the index if a match is found.
  58. * If no match is found, then the left-index (the index associated with the item that comes just
  59. * before the desired index) is returned. To maintain proper sort order, a splice would happen at
  60. * the next index:
  61. *
  62. * ```js
  63. * const array = [1, 3];
  64. * const needle = 2;
  65. * const index = binarySearch(array, needle, (item, needle) => item - needle);
  66. *
  67. * assert.equal(index, 0);
  68. * array.splice(index + 1, 0, needle);
  69. * assert.deepEqual(array, [1, 2, 3]);
  70. * ```
  71. */
  72. function binarySearch(haystack, needle, low, high) {
  73. while (low <= high) {
  74. const mid = low + ((high - low) >> 1);
  75. const cmp = haystack[mid][0] - needle;
  76. if (cmp === 0) {
  77. return mid;
  78. }
  79. if (cmp < 0) {
  80. low = mid + 1;
  81. }
  82. else {
  83. high = mid - 1;
  84. }
  85. }
  86. return low - 1;
  87. }
  88. function memoizedState() {
  89. return {
  90. lastKey: -1,
  91. lastNeedle: -1,
  92. lastIndex: -1,
  93. };
  94. }
  95. /**
  96. * This overly complicated beast is just to record the last tested line/column and the resulting
  97. * index, allowing us to skip a few tests if mappings are monotonically increasing.
  98. */
  99. function memoizedBinarySearch(haystack, needle, state, key) {
  100. const { lastKey, lastNeedle, lastIndex } = state;
  101. let low = 0;
  102. let high = haystack.length - 1;
  103. if (key === lastKey) {
  104. if (needle === lastNeedle) {
  105. return lastIndex;
  106. }
  107. if (needle >= lastNeedle) {
  108. // lastIndex may be -1 if the previous needle was not found.
  109. low = Math.max(lastIndex, 0);
  110. }
  111. else {
  112. high = lastIndex;
  113. }
  114. }
  115. state.lastKey = key;
  116. state.lastNeedle = needle;
  117. return (state.lastIndex = binarySearch(haystack, needle, low, high));
  118. }
  119. const INVALID_MAPPING = Object.freeze({
  120. source: null,
  121. line: null,
  122. column: null,
  123. name: null,
  124. });
  125. /**
  126. * Returns the encoded (VLQ string) form of the SourceMap's mappings field.
  127. */
  128. let encodedMappings;
  129. /**
  130. * Returns the decoded (array of lines of segments) form of the SourceMap's mappings field.
  131. */
  132. let decodedMappings;
  133. /**
  134. * A low-level API to find the segment associated with a generated line/column (think, from a
  135. * stack trace). Line and column here are 0-based, unlike `originalPositionFor`.
  136. */
  137. let traceSegment;
  138. /**
  139. * A higher-level API to find the source/line/column associated with a generated line/column
  140. * (think, from a stack trace). Line is 1-based, but column is 0-based, due to legacy behavior in
  141. * `source-map` library.
  142. */
  143. let originalPositionFor;
  144. /**
  145. * Iterates each mapping in generated position order.
  146. */
  147. let eachMapping;
  148. /**
  149. * A helper that skips sorting of the input map's mappings array, which can be expensive for larger
  150. * maps.
  151. */
  152. let presortedDecodedMap;
  153. class TraceMap {
  154. constructor(map, mapUrl) {
  155. this._binarySearchMemo = memoizedState();
  156. const isString = typeof map === 'string';
  157. const parsed = isString ? JSON.parse(map) : map;
  158. const { version, file, names, sourceRoot, sources, sourcesContent } = parsed;
  159. this.version = version;
  160. this.file = file;
  161. this.names = names;
  162. this.sourceRoot = sourceRoot;
  163. this.sources = sources;
  164. this.sourcesContent = sourcesContent;
  165. if (sourceRoot || mapUrl) {
  166. const from = resolve(sourceRoot || '', stripFilename(mapUrl));
  167. this.resolvedSources = sources.map((s) => resolve(s || '', from));
  168. }
  169. else {
  170. this.resolvedSources = sources.map((s) => s || '');
  171. }
  172. const { mappings } = parsed;
  173. if (typeof mappings === 'string') {
  174. this._encoded = mappings;
  175. this._decoded = decode(mappings);
  176. }
  177. else {
  178. this._encoded = undefined;
  179. this._decoded = maybeSort(mappings, isString);
  180. }
  181. }
  182. }
  183. (() => {
  184. encodedMappings = (map) => {
  185. var _a;
  186. return ((_a = map._encoded) !== null && _a !== void 0 ? _a : (map._encoded = encode(map._decoded)));
  187. };
  188. decodedMappings = (map) => {
  189. return map._decoded;
  190. };
  191. traceSegment = (map, line, column) => {
  192. const decoded = map._decoded;
  193. // It's common for parent source maps to have pointers to lines that have no
  194. // mapping (like a "//# sourceMappingURL=") at the end of the child file.
  195. if (line >= decoded.length)
  196. return null;
  197. const segments = decoded[line];
  198. const index = memoizedBinarySearch(segments, column, map._binarySearchMemo, line);
  199. // we come before any mapped segment
  200. if (index < 0)
  201. return null;
  202. return segments[index];
  203. };
  204. originalPositionFor = (map, { line, column }) => {
  205. if (line < 1)
  206. throw new Error('`line` must be greater than 0 (lines start at line 1)');
  207. if (column < 0) {
  208. throw new Error('`column` must be greater than or equal to 0 (columns start at column 0)');
  209. }
  210. const segment = traceSegment(map, line - 1, column);
  211. if (segment == null)
  212. return INVALID_MAPPING;
  213. if (segment.length == 1)
  214. return INVALID_MAPPING;
  215. const { names, resolvedSources } = map;
  216. return {
  217. source: resolvedSources[segment[1]],
  218. line: segment[2] + 1,
  219. column: segment[3],
  220. name: segment.length === 5 ? names[segment[4]] : null,
  221. };
  222. };
  223. eachMapping = (map, cb) => {
  224. const decoded = map._decoded;
  225. const { names, resolvedSources } = map;
  226. for (let i = 0; i < decoded.length; i++) {
  227. const line = decoded[i];
  228. for (let j = 0; j < line.length; j++) {
  229. const seg = line[j];
  230. const generatedLine = i + 1;
  231. const generatedColumn = seg[0];
  232. let source = null;
  233. let originalLine = null;
  234. let originalColumn = null;
  235. let name = null;
  236. if (seg.length !== 1) {
  237. source = resolvedSources[seg[1]];
  238. originalLine = seg[2] + 1;
  239. originalColumn = seg[3];
  240. }
  241. if (seg.length === 5)
  242. name = names[seg[4]];
  243. cb({
  244. generatedLine,
  245. generatedColumn,
  246. source,
  247. originalLine,
  248. originalColumn,
  249. name,
  250. });
  251. }
  252. }
  253. };
  254. presortedDecodedMap = (map, mapUrl) => {
  255. const clone = Object.assign({}, map);
  256. clone.mappings = [];
  257. const tracer = new TraceMap(clone, mapUrl);
  258. tracer._decoded = map.mappings;
  259. return tracer;
  260. };
  261. })();
  262. export { TraceMap, decodedMappings, eachMapping, encodedMappings, originalPositionFor, presortedDecodedMap, traceSegment };
  263. //# sourceMappingURL=trace-mapping.mjs.map