gen-mapping.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458
  1. import { SetArray, put } from '@jridgewell/set-array';
  2. import { encode } from '@jridgewell/sourcemap-codec';
  3. import { TraceMap, decodedMappings } from '@jridgewell/trace-mapping';
  4. import {
  5. COLUMN,
  6. SOURCES_INDEX,
  7. SOURCE_LINE,
  8. SOURCE_COLUMN,
  9. NAMES_INDEX,
  10. } from './sourcemap-segment';
  11. import type { SourceMapInput } from '@jridgewell/trace-mapping';
  12. import type { SourceMapSegment } from './sourcemap-segment';
  13. import type { DecodedSourceMap, EncodedSourceMap, Pos, Mapping } from './types';
  14. export type { DecodedSourceMap, EncodedSourceMap, Mapping };
  15. export type Options = {
  16. file?: string | null;
  17. sourceRoot?: string | null;
  18. };
  19. const NO_NAME = -1;
  20. /**
  21. * A low-level API to associate a generated position with an original source position. Line and
  22. * column here are 0-based, unlike `addMapping`.
  23. */
  24. export let addSegment: {
  25. (
  26. map: GenMapping,
  27. genLine: number,
  28. genColumn: number,
  29. source?: null,
  30. sourceLine?: null,
  31. sourceColumn?: null,
  32. name?: null,
  33. content?: null,
  34. ): void;
  35. (
  36. map: GenMapping,
  37. genLine: number,
  38. genColumn: number,
  39. source: string,
  40. sourceLine: number,
  41. sourceColumn: number,
  42. name?: null,
  43. content?: string | null,
  44. ): void;
  45. (
  46. map: GenMapping,
  47. genLine: number,
  48. genColumn: number,
  49. source: string,
  50. sourceLine: number,
  51. sourceColumn: number,
  52. name: string,
  53. content?: string | null,
  54. ): void;
  55. };
  56. /**
  57. * A high-level API to associate a generated position with an original source position. Line is
  58. * 1-based, but column is 0-based, due to legacy behavior in `source-map` library.
  59. */
  60. export let addMapping: {
  61. (
  62. map: GenMapping,
  63. mapping: {
  64. generated: Pos;
  65. source?: null;
  66. original?: null;
  67. name?: null;
  68. content?: null;
  69. },
  70. ): void;
  71. (
  72. map: GenMapping,
  73. mapping: {
  74. generated: Pos;
  75. source: string;
  76. original: Pos;
  77. name?: null;
  78. content?: string | null;
  79. },
  80. ): void;
  81. (
  82. map: GenMapping,
  83. mapping: {
  84. generated: Pos;
  85. source: string;
  86. original: Pos;
  87. name: string;
  88. content?: string | null;
  89. },
  90. ): void;
  91. };
  92. /**
  93. * Same as `addSegment`, but will only add the segment if it generates useful information in the
  94. * resulting map. This only works correctly if segments are added **in order**, meaning you should
  95. * not add a segment with a lower generated line/column than one that came before.
  96. */
  97. export let maybeAddSegment: typeof addSegment;
  98. /**
  99. * Same as `addMapping`, but will only add the mapping if it generates useful information in the
  100. * resulting map. This only works correctly if mappings are added **in order**, meaning you should
  101. * not add a mapping with a lower generated line/column than one that came before.
  102. */
  103. export let maybeAddMapping: typeof addMapping;
  104. /**
  105. * Adds/removes the content of the source file to the source map.
  106. */
  107. export let setSourceContent: (map: GenMapping, source: string, content: string | null) => void;
  108. /**
  109. * Returns a sourcemap object (with decoded mappings) suitable for passing to a library that expects
  110. * a sourcemap, or to JSON.stringify.
  111. */
  112. export let toDecodedMap: (map: GenMapping) => DecodedSourceMap;
  113. /**
  114. * Returns a sourcemap object (with encoded mappings) suitable for passing to a library that expects
  115. * a sourcemap, or to JSON.stringify.
  116. */
  117. export let toEncodedMap: (map: GenMapping) => EncodedSourceMap;
  118. /**
  119. * Constructs a new GenMapping, using the already present mappings of the input.
  120. */
  121. export let fromMap: (input: SourceMapInput) => GenMapping;
  122. /**
  123. * Returns an array of high-level mapping objects for every recorded segment, which could then be
  124. * passed to the `source-map` library.
  125. */
  126. export let allMappings: (map: GenMapping) => Mapping[];
  127. // This split declaration is only so that terser can elminiate the static initialization block.
  128. let addSegmentInternal: <S extends string | null | undefined>(
  129. skipable: boolean,
  130. map: GenMapping,
  131. genLine: number,
  132. genColumn: number,
  133. source: S,
  134. sourceLine: S extends string ? number : null | undefined,
  135. sourceColumn: S extends string ? number : null | undefined,
  136. name: S extends string ? string | null | undefined : null | undefined,
  137. content: S extends string ? string | null | undefined : null | undefined,
  138. ) => void;
  139. /**
  140. * Provides the state to generate a sourcemap.
  141. */
  142. export class GenMapping {
  143. private _names = new SetArray();
  144. private _sources = new SetArray();
  145. private _sourcesContent: (string | null)[] = [];
  146. private _mappings: SourceMapSegment[][] = [];
  147. declare file: string | null | undefined;
  148. declare sourceRoot: string | null | undefined;
  149. constructor({ file, sourceRoot }: Options = {}) {
  150. this.file = file;
  151. this.sourceRoot = sourceRoot;
  152. }
  153. static {
  154. addSegment = (map, genLine, genColumn, source, sourceLine, sourceColumn, name, content) => {
  155. return addSegmentInternal(
  156. false,
  157. map,
  158. genLine,
  159. genColumn,
  160. source,
  161. sourceLine,
  162. sourceColumn,
  163. name,
  164. content,
  165. );
  166. };
  167. maybeAddSegment = (
  168. map,
  169. genLine,
  170. genColumn,
  171. source,
  172. sourceLine,
  173. sourceColumn,
  174. name,
  175. content,
  176. ) => {
  177. return addSegmentInternal(
  178. true,
  179. map,
  180. genLine,
  181. genColumn,
  182. source,
  183. sourceLine,
  184. sourceColumn,
  185. name,
  186. content,
  187. );
  188. };
  189. addMapping = (map, mapping) => {
  190. return addMappingInternal(false, map, mapping as Parameters<typeof addMappingInternal>[2]);
  191. };
  192. maybeAddMapping = (map, mapping) => {
  193. return addMappingInternal(true, map, mapping as Parameters<typeof addMappingInternal>[2]);
  194. };
  195. setSourceContent = (map, source, content) => {
  196. const { _sources: sources, _sourcesContent: sourcesContent } = map;
  197. sourcesContent[put(sources, source)] = content;
  198. };
  199. toDecodedMap = (map) => {
  200. const {
  201. file,
  202. sourceRoot,
  203. _mappings: mappings,
  204. _sources: sources,
  205. _sourcesContent: sourcesContent,
  206. _names: names,
  207. } = map;
  208. removeEmptyFinalLines(mappings);
  209. return {
  210. version: 3,
  211. file: file || undefined,
  212. names: names.array,
  213. sourceRoot: sourceRoot || undefined,
  214. sources: sources.array,
  215. sourcesContent,
  216. mappings,
  217. };
  218. };
  219. toEncodedMap = (map) => {
  220. const decoded = toDecodedMap(map);
  221. return {
  222. ...decoded,
  223. mappings: encode(decoded.mappings as SourceMapSegment[][]),
  224. };
  225. };
  226. allMappings = (map) => {
  227. const out: Mapping[] = [];
  228. const { _mappings: mappings, _sources: sources, _names: names } = map;
  229. for (let i = 0; i < mappings.length; i++) {
  230. const line = mappings[i];
  231. for (let j = 0; j < line.length; j++) {
  232. const seg = line[j];
  233. const generated = { line: i + 1, column: seg[COLUMN] };
  234. let source: string | undefined = undefined;
  235. let original: Pos | undefined = undefined;
  236. let name: string | undefined = undefined;
  237. if (seg.length !== 1) {
  238. source = sources.array[seg[SOURCES_INDEX]];
  239. original = { line: seg[SOURCE_LINE] + 1, column: seg[SOURCE_COLUMN] };
  240. if (seg.length === 5) name = names.array[seg[NAMES_INDEX]];
  241. }
  242. out.push({ generated, source, original, name } as Mapping);
  243. }
  244. }
  245. return out;
  246. };
  247. fromMap = (input) => {
  248. const map = new TraceMap(input);
  249. const gen = new GenMapping({ file: map.file, sourceRoot: map.sourceRoot });
  250. putAll(gen._names, map.names);
  251. putAll(gen._sources, map.sources as string[]);
  252. gen._sourcesContent = map.sourcesContent || map.sources.map(() => null);
  253. gen._mappings = decodedMappings(map) as GenMapping['_mappings'];
  254. return gen;
  255. };
  256. // Internal helpers
  257. addSegmentInternal = (
  258. skipable,
  259. map,
  260. genLine,
  261. genColumn,
  262. source,
  263. sourceLine,
  264. sourceColumn,
  265. name,
  266. content,
  267. ) => {
  268. const {
  269. _mappings: mappings,
  270. _sources: sources,
  271. _sourcesContent: sourcesContent,
  272. _names: names,
  273. } = map;
  274. const line = getLine(mappings, genLine);
  275. const index = getColumnIndex(line, genColumn);
  276. if (!source) {
  277. if (skipable && skipSourceless(line, index)) return;
  278. return insert(line, index, [genColumn]);
  279. }
  280. // Sigh, TypeScript can't figure out sourceLine and sourceColumn aren't nullish if source
  281. // isn't nullish.
  282. assert<number>(sourceLine);
  283. assert<number>(sourceColumn);
  284. const sourcesIndex = put(sources, source);
  285. const namesIndex = name ? put(names, name) : NO_NAME;
  286. if (sourcesIndex === sourcesContent.length) sourcesContent[sourcesIndex] = content ?? null;
  287. if (skipable && skipSource(line, index, sourcesIndex, sourceLine, sourceColumn, namesIndex)) {
  288. return;
  289. }
  290. return insert(
  291. line,
  292. index,
  293. name
  294. ? [genColumn, sourcesIndex, sourceLine, sourceColumn, namesIndex]
  295. : [genColumn, sourcesIndex, sourceLine, sourceColumn],
  296. );
  297. };
  298. }
  299. }
  300. function assert<T>(_val: unknown): asserts _val is T {
  301. // noop.
  302. }
  303. function getLine(mappings: SourceMapSegment[][], index: number): SourceMapSegment[] {
  304. for (let i = mappings.length; i <= index; i++) {
  305. mappings[i] = [];
  306. }
  307. return mappings[index];
  308. }
  309. function getColumnIndex(line: SourceMapSegment[], genColumn: number): number {
  310. let index = line.length;
  311. for (let i = index - 1; i >= 0; index = i--) {
  312. const current = line[i];
  313. if (genColumn >= current[COLUMN]) break;
  314. }
  315. return index;
  316. }
  317. function insert<T>(array: T[], index: number, value: T) {
  318. for (let i = array.length; i > index; i--) {
  319. array[i] = array[i - 1];
  320. }
  321. array[index] = value;
  322. }
  323. function removeEmptyFinalLines(mappings: SourceMapSegment[][]) {
  324. const { length } = mappings;
  325. let len = length;
  326. for (let i = len - 1; i >= 0; len = i, i--) {
  327. if (mappings[i].length > 0) break;
  328. }
  329. if (len < length) mappings.length = len;
  330. }
  331. function putAll(strarr: SetArray, array: string[]) {
  332. for (let i = 0; i < array.length; i++) put(strarr, array[i]);
  333. }
  334. function skipSourceless(line: SourceMapSegment[], index: number): boolean {
  335. // The start of a line is already sourceless, so adding a sourceless segment to the beginning
  336. // doesn't generate any useful information.
  337. if (index === 0) return true;
  338. const prev = line[index - 1];
  339. // If the previous segment is also sourceless, then adding another sourceless segment doesn't
  340. // genrate any new information. Else, this segment will end the source/named segment and point to
  341. // a sourceless position, which is useful.
  342. return prev.length === 1;
  343. }
  344. function skipSource(
  345. line: SourceMapSegment[],
  346. index: number,
  347. sourcesIndex: number,
  348. sourceLine: number,
  349. sourceColumn: number,
  350. namesIndex: number,
  351. ): boolean {
  352. // A source/named segment at the start of a line gives position at that genColumn
  353. if (index === 0) return false;
  354. const prev = line[index - 1];
  355. // If the previous segment is sourceless, then we're transitioning to a source.
  356. if (prev.length === 1) return false;
  357. // If the previous segment maps to the exact same source position, then this segment doesn't
  358. // provide any new position information.
  359. return (
  360. sourcesIndex === prev[SOURCES_INDEX] &&
  361. sourceLine === prev[SOURCE_LINE] &&
  362. sourceColumn === prev[SOURCE_COLUMN] &&
  363. namesIndex === (prev.length === 5 ? prev[NAMES_INDEX] : NO_NAME)
  364. );
  365. }
  366. function addMappingInternal<S extends string | null | undefined>(
  367. skipable: boolean,
  368. map: GenMapping,
  369. mapping: {
  370. generated: Pos;
  371. source: S;
  372. original: S extends string ? Pos : null | undefined;
  373. name: S extends string ? string | null | undefined : null | undefined;
  374. content: S extends string ? string | null | undefined : null | undefined;
  375. },
  376. ) {
  377. const { generated, source, original, name, content } = mapping;
  378. if (!source) {
  379. return addSegmentInternal(
  380. skipable,
  381. map,
  382. generated.line - 1,
  383. generated.column,
  384. null,
  385. null,
  386. null,
  387. null,
  388. null,
  389. );
  390. }
  391. const s: string = source;
  392. assert<Pos>(original);
  393. return addSegmentInternal(
  394. skipable,
  395. map,
  396. generated.line - 1,
  397. generated.column,
  398. s,
  399. original.line - 1,
  400. original.column,
  401. name,
  402. content,
  403. );
  404. }