gen-mapping.ts 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614
  1. import { SetArray, put, remove } from './set-array';
  2. import {
  3. encode,
  4. // encodeGeneratedRanges,
  5. // encodeOriginalScopes
  6. } from '@jridgewell/sourcemap-codec';
  7. import { TraceMap, decodedMappings } from '@jridgewell/trace-mapping';
  8. import {
  9. COLUMN,
  10. SOURCES_INDEX,
  11. SOURCE_LINE,
  12. SOURCE_COLUMN,
  13. NAMES_INDEX,
  14. } from './sourcemap-segment';
  15. import type { SourceMapInput } from '@jridgewell/trace-mapping';
  16. // import type { OriginalScope, GeneratedRange } from '@jridgewell/sourcemap-codec';
  17. import type { SourceMapSegment } from './sourcemap-segment';
  18. import type {
  19. DecodedSourceMap,
  20. EncodedSourceMap,
  21. Pos,
  22. Mapping,
  23. // BindingExpressionRange,
  24. // OriginalPos,
  25. // OriginalScopeInfo,
  26. // GeneratedRangeInfo,
  27. } from './types';
  28. export type { DecodedSourceMap, EncodedSourceMap, Mapping };
  29. export type Options = {
  30. file?: string | null;
  31. sourceRoot?: string | null;
  32. };
  33. const NO_NAME = -1;
  34. /**
  35. * Provides the state to generate a sourcemap.
  36. */
  37. export class GenMapping {
  38. declare private _names: SetArray<string>;
  39. declare private _sources: SetArray<string>;
  40. declare private _sourcesContent: (string | null)[];
  41. declare private _mappings: SourceMapSegment[][];
  42. // private declare _originalScopes: OriginalScope[][];
  43. // private declare _generatedRanges: GeneratedRange[];
  44. declare private _ignoreList: SetArray<number>;
  45. declare file: string | null | undefined;
  46. declare sourceRoot: string | null | undefined;
  47. constructor({ file, sourceRoot }: Options = {}) {
  48. this._names = new SetArray();
  49. this._sources = new SetArray();
  50. this._sourcesContent = [];
  51. this._mappings = [];
  52. // this._originalScopes = [];
  53. // this._generatedRanges = [];
  54. this.file = file;
  55. this.sourceRoot = sourceRoot;
  56. this._ignoreList = new SetArray();
  57. }
  58. }
  59. interface PublicMap {
  60. _names: GenMapping['_names'];
  61. _sources: GenMapping['_sources'];
  62. _sourcesContent: GenMapping['_sourcesContent'];
  63. _mappings: GenMapping['_mappings'];
  64. // _originalScopes: GenMapping['_originalScopes'];
  65. // _generatedRanges: GenMapping['_generatedRanges'];
  66. _ignoreList: GenMapping['_ignoreList'];
  67. }
  68. /**
  69. * Typescript doesn't allow friend access to private fields, so this just casts the map into a type
  70. * with public access modifiers.
  71. */
  72. function cast(map: unknown): PublicMap {
  73. return map as any;
  74. }
  75. /**
  76. * A low-level API to associate a generated position with an original source position. Line and
  77. * column here are 0-based, unlike `addMapping`.
  78. */
  79. export function addSegment(
  80. map: GenMapping,
  81. genLine: number,
  82. genColumn: number,
  83. source?: null,
  84. sourceLine?: null,
  85. sourceColumn?: null,
  86. name?: null,
  87. content?: null,
  88. ): void;
  89. export function addSegment(
  90. map: GenMapping,
  91. genLine: number,
  92. genColumn: number,
  93. source: string,
  94. sourceLine: number,
  95. sourceColumn: number,
  96. name?: null,
  97. content?: string | null,
  98. ): void;
  99. export function addSegment(
  100. map: GenMapping,
  101. genLine: number,
  102. genColumn: number,
  103. source: string,
  104. sourceLine: number,
  105. sourceColumn: number,
  106. name: string,
  107. content?: string | null,
  108. ): void;
  109. export function addSegment(
  110. map: GenMapping,
  111. genLine: number,
  112. genColumn: number,
  113. source?: string | null,
  114. sourceLine?: number | null,
  115. sourceColumn?: number | null,
  116. name?: string | null,
  117. content?: string | null,
  118. ): void {
  119. return addSegmentInternal(
  120. false,
  121. map,
  122. genLine,
  123. genColumn,
  124. source,
  125. sourceLine,
  126. sourceColumn,
  127. name,
  128. content,
  129. );
  130. }
  131. /**
  132. * A high-level API to associate a generated position with an original source position. Line is
  133. * 1-based, but column is 0-based, due to legacy behavior in `source-map` library.
  134. */
  135. export function addMapping(
  136. map: GenMapping,
  137. mapping: {
  138. generated: Pos;
  139. source?: null;
  140. original?: null;
  141. name?: null;
  142. content?: null;
  143. },
  144. ): void;
  145. export function addMapping(
  146. map: GenMapping,
  147. mapping: {
  148. generated: Pos;
  149. source: string;
  150. original: Pos;
  151. name?: null;
  152. content?: string | null;
  153. },
  154. ): void;
  155. export function addMapping(
  156. map: GenMapping,
  157. mapping: {
  158. generated: Pos;
  159. source: string;
  160. original: Pos;
  161. name: string;
  162. content?: string | null;
  163. },
  164. ): void;
  165. export function addMapping(
  166. map: GenMapping,
  167. mapping: {
  168. generated: Pos;
  169. source?: string | null;
  170. original?: Pos | null;
  171. name?: string | null;
  172. content?: string | null;
  173. },
  174. ): void {
  175. return addMappingInternal(false, map, mapping as Parameters<typeof addMappingInternal>[2]);
  176. }
  177. /**
  178. * Same as `addSegment`, but will only add the segment if it generates useful information in the
  179. * resulting map. This only works correctly if segments are added **in order**, meaning you should
  180. * not add a segment with a lower generated line/column than one that came before.
  181. */
  182. export const maybeAddSegment: typeof addSegment = (
  183. map,
  184. genLine,
  185. genColumn,
  186. source,
  187. sourceLine,
  188. sourceColumn,
  189. name,
  190. content,
  191. ) => {
  192. return addSegmentInternal(
  193. true,
  194. map,
  195. genLine,
  196. genColumn,
  197. source,
  198. sourceLine,
  199. sourceColumn,
  200. name,
  201. content,
  202. );
  203. };
  204. /**
  205. * Same as `addMapping`, but will only add the mapping if it generates useful information in the
  206. * resulting map. This only works correctly if mappings are added **in order**, meaning you should
  207. * not add a mapping with a lower generated line/column than one that came before.
  208. */
  209. export const maybeAddMapping: typeof addMapping = (map, mapping) => {
  210. return addMappingInternal(true, map, mapping as Parameters<typeof addMappingInternal>[2]);
  211. };
  212. /**
  213. * Adds/removes the content of the source file to the source map.
  214. */
  215. export function setSourceContent(map: GenMapping, source: string, content: string | null): void {
  216. const {
  217. _sources: sources,
  218. _sourcesContent: sourcesContent,
  219. // _originalScopes: originalScopes,
  220. } = cast(map);
  221. const index = put(sources, source);
  222. sourcesContent[index] = content;
  223. // if (index === originalScopes.length) originalScopes[index] = [];
  224. }
  225. export function setIgnore(map: GenMapping, source: string, ignore = true) {
  226. const {
  227. _sources: sources,
  228. _sourcesContent: sourcesContent,
  229. _ignoreList: ignoreList,
  230. // _originalScopes: originalScopes,
  231. } = cast(map);
  232. const index = put(sources, source);
  233. if (index === sourcesContent.length) sourcesContent[index] = null;
  234. // if (index === originalScopes.length) originalScopes[index] = [];
  235. if (ignore) put(ignoreList, index);
  236. else remove(ignoreList, index);
  237. }
  238. /**
  239. * Returns a sourcemap object (with decoded mappings) suitable for passing to a library that expects
  240. * a sourcemap, or to JSON.stringify.
  241. */
  242. export function toDecodedMap(map: GenMapping): DecodedSourceMap {
  243. const {
  244. _mappings: mappings,
  245. _sources: sources,
  246. _sourcesContent: sourcesContent,
  247. _names: names,
  248. _ignoreList: ignoreList,
  249. // _originalScopes: originalScopes,
  250. // _generatedRanges: generatedRanges,
  251. } = cast(map);
  252. removeEmptyFinalLines(mappings);
  253. return {
  254. version: 3,
  255. file: map.file || undefined,
  256. names: names.array,
  257. sourceRoot: map.sourceRoot || undefined,
  258. sources: sources.array,
  259. sourcesContent,
  260. mappings,
  261. // originalScopes,
  262. // generatedRanges,
  263. ignoreList: ignoreList.array,
  264. };
  265. }
  266. /**
  267. * Returns a sourcemap object (with encoded mappings) suitable for passing to a library that expects
  268. * a sourcemap, or to JSON.stringify.
  269. */
  270. export function toEncodedMap(map: GenMapping): EncodedSourceMap {
  271. const decoded = toDecodedMap(map);
  272. return Object.assign({}, decoded, {
  273. // originalScopes: decoded.originalScopes.map((os) => encodeOriginalScopes(os)),
  274. // generatedRanges: encodeGeneratedRanges(decoded.generatedRanges as GeneratedRange[]),
  275. mappings: encode(decoded.mappings as SourceMapSegment[][]),
  276. });
  277. }
  278. /**
  279. * Constructs a new GenMapping, using the already present mappings of the input.
  280. */
  281. export function fromMap(input: SourceMapInput): GenMapping {
  282. const map = new TraceMap(input);
  283. const gen = new GenMapping({ file: map.file, sourceRoot: map.sourceRoot });
  284. putAll(cast(gen)._names, map.names);
  285. putAll(cast(gen)._sources, map.sources as string[]);
  286. cast(gen)._sourcesContent = map.sourcesContent || map.sources.map(() => null);
  287. cast(gen)._mappings = decodedMappings(map) as GenMapping['_mappings'];
  288. // TODO: implement originalScopes/generatedRanges
  289. if (map.ignoreList) putAll(cast(gen)._ignoreList, map.ignoreList);
  290. return gen;
  291. }
  292. /**
  293. * Returns an array of high-level mapping objects for every recorded segment, which could then be
  294. * passed to the `source-map` library.
  295. */
  296. export function allMappings(map: GenMapping): Mapping[] {
  297. const out: Mapping[] = [];
  298. const { _mappings: mappings, _sources: sources, _names: names } = cast(map);
  299. for (let i = 0; i < mappings.length; i++) {
  300. const line = mappings[i];
  301. for (let j = 0; j < line.length; j++) {
  302. const seg = line[j];
  303. const generated = { line: i + 1, column: seg[COLUMN] };
  304. let source: string | undefined = undefined;
  305. let original: Pos | undefined = undefined;
  306. let name: string | undefined = undefined;
  307. if (seg.length !== 1) {
  308. source = sources.array[seg[SOURCES_INDEX]];
  309. original = { line: seg[SOURCE_LINE] + 1, column: seg[SOURCE_COLUMN] };
  310. if (seg.length === 5) name = names.array[seg[NAMES_INDEX]];
  311. }
  312. out.push({ generated, source, original, name } as Mapping);
  313. }
  314. }
  315. return out;
  316. }
  317. // This split declaration is only so that terser can elminiate the static initialization block.
  318. function addSegmentInternal<S extends string | null | undefined>(
  319. skipable: boolean,
  320. map: GenMapping,
  321. genLine: number,
  322. genColumn: number,
  323. source: S,
  324. sourceLine: S extends string ? number : null | undefined,
  325. sourceColumn: S extends string ? number : null | undefined,
  326. name: S extends string ? string | null | undefined : null | undefined,
  327. content: S extends string ? string | null | undefined : null | undefined,
  328. ): void {
  329. const {
  330. _mappings: mappings,
  331. _sources: sources,
  332. _sourcesContent: sourcesContent,
  333. _names: names,
  334. // _originalScopes: originalScopes,
  335. } = cast(map);
  336. const line = getIndex(mappings, genLine);
  337. const index = getColumnIndex(line, genColumn);
  338. if (!source) {
  339. if (skipable && skipSourceless(line, index)) return;
  340. return insert(line, index, [genColumn]);
  341. }
  342. // Sigh, TypeScript can't figure out sourceLine and sourceColumn aren't nullish if source
  343. // isn't nullish.
  344. assert<number>(sourceLine);
  345. assert<number>(sourceColumn);
  346. const sourcesIndex = put(sources, source);
  347. const namesIndex = name ? put(names, name) : NO_NAME;
  348. if (sourcesIndex === sourcesContent.length) sourcesContent[sourcesIndex] = content ?? null;
  349. // if (sourcesIndex === originalScopes.length) originalScopes[sourcesIndex] = [];
  350. if (skipable && skipSource(line, index, sourcesIndex, sourceLine, sourceColumn, namesIndex)) {
  351. return;
  352. }
  353. return insert(
  354. line,
  355. index,
  356. name
  357. ? [genColumn, sourcesIndex, sourceLine, sourceColumn, namesIndex]
  358. : [genColumn, sourcesIndex, sourceLine, sourceColumn],
  359. );
  360. }
  361. function assert<T>(_val: unknown): asserts _val is T {
  362. // noop.
  363. }
  364. function getIndex<T>(arr: T[][], index: number): T[] {
  365. for (let i = arr.length; i <= index; i++) {
  366. arr[i] = [];
  367. }
  368. return arr[index];
  369. }
  370. function getColumnIndex(line: SourceMapSegment[], genColumn: number): number {
  371. let index = line.length;
  372. for (let i = index - 1; i >= 0; index = i--) {
  373. const current = line[i];
  374. if (genColumn >= current[COLUMN]) break;
  375. }
  376. return index;
  377. }
  378. function insert<T>(array: T[], index: number, value: T) {
  379. for (let i = array.length; i > index; i--) {
  380. array[i] = array[i - 1];
  381. }
  382. array[index] = value;
  383. }
  384. function removeEmptyFinalLines(mappings: SourceMapSegment[][]) {
  385. const { length } = mappings;
  386. let len = length;
  387. for (let i = len - 1; i >= 0; len = i, i--) {
  388. if (mappings[i].length > 0) break;
  389. }
  390. if (len < length) mappings.length = len;
  391. }
  392. function putAll<T extends string | number>(setarr: SetArray<T>, array: T[]) {
  393. for (let i = 0; i < array.length; i++) put(setarr, array[i]);
  394. }
  395. function skipSourceless(line: SourceMapSegment[], index: number): boolean {
  396. // The start of a line is already sourceless, so adding a sourceless segment to the beginning
  397. // doesn't generate any useful information.
  398. if (index === 0) return true;
  399. const prev = line[index - 1];
  400. // If the previous segment is also sourceless, then adding another sourceless segment doesn't
  401. // genrate any new information. Else, this segment will end the source/named segment and point to
  402. // a sourceless position, which is useful.
  403. return prev.length === 1;
  404. }
  405. function skipSource(
  406. line: SourceMapSegment[],
  407. index: number,
  408. sourcesIndex: number,
  409. sourceLine: number,
  410. sourceColumn: number,
  411. namesIndex: number,
  412. ): boolean {
  413. // A source/named segment at the start of a line gives position at that genColumn
  414. if (index === 0) return false;
  415. const prev = line[index - 1];
  416. // If the previous segment is sourceless, then we're transitioning to a source.
  417. if (prev.length === 1) return false;
  418. // If the previous segment maps to the exact same source position, then this segment doesn't
  419. // provide any new position information.
  420. return (
  421. sourcesIndex === prev[SOURCES_INDEX] &&
  422. sourceLine === prev[SOURCE_LINE] &&
  423. sourceColumn === prev[SOURCE_COLUMN] &&
  424. namesIndex === (prev.length === 5 ? prev[NAMES_INDEX] : NO_NAME)
  425. );
  426. }
  427. function addMappingInternal<S extends string | null | undefined>(
  428. skipable: boolean,
  429. map: GenMapping,
  430. mapping: {
  431. generated: Pos;
  432. source: S;
  433. original: S extends string ? Pos : null | undefined;
  434. name: S extends string ? string | null | undefined : null | undefined;
  435. content: S extends string ? string | null | undefined : null | undefined;
  436. },
  437. ) {
  438. const { generated, source, original, name, content } = mapping;
  439. if (!source) {
  440. return addSegmentInternal(
  441. skipable,
  442. map,
  443. generated.line - 1,
  444. generated.column,
  445. null,
  446. null,
  447. null,
  448. null,
  449. null,
  450. );
  451. }
  452. assert<Pos>(original);
  453. return addSegmentInternal(
  454. skipable,
  455. map,
  456. generated.line - 1,
  457. generated.column,
  458. source as string,
  459. original.line - 1,
  460. original.column,
  461. name,
  462. content,
  463. );
  464. }
  465. /*
  466. export function addOriginalScope(
  467. map: GenMapping,
  468. data: {
  469. start: Pos;
  470. end: Pos;
  471. source: string;
  472. kind: string;
  473. name?: string;
  474. variables?: string[];
  475. },
  476. ): OriginalScopeInfo {
  477. const { start, end, source, kind, name, variables } = data;
  478. const {
  479. _sources: sources,
  480. _sourcesContent: sourcesContent,
  481. _originalScopes: originalScopes,
  482. _names: names,
  483. } = cast(map);
  484. const index = put(sources, source);
  485. if (index === sourcesContent.length) sourcesContent[index] = null;
  486. if (index === originalScopes.length) originalScopes[index] = [];
  487. const kindIndex = put(names, kind);
  488. const scope: OriginalScope = name
  489. ? [start.line - 1, start.column, end.line - 1, end.column, kindIndex, put(names, name)]
  490. : [start.line - 1, start.column, end.line - 1, end.column, kindIndex];
  491. if (variables) {
  492. scope.vars = variables.map((v) => put(names, v));
  493. }
  494. const len = originalScopes[index].push(scope);
  495. return [index, len - 1, variables];
  496. }
  497. */
  498. // Generated Ranges
  499. /*
  500. export function addGeneratedRange(
  501. map: GenMapping,
  502. data: {
  503. start: Pos;
  504. isScope: boolean;
  505. originalScope?: OriginalScopeInfo;
  506. callsite?: OriginalPos;
  507. },
  508. ): GeneratedRangeInfo {
  509. const { start, isScope, originalScope, callsite } = data;
  510. const {
  511. _originalScopes: originalScopes,
  512. _sources: sources,
  513. _sourcesContent: sourcesContent,
  514. _generatedRanges: generatedRanges,
  515. } = cast(map);
  516. const range: GeneratedRange = [
  517. start.line - 1,
  518. start.column,
  519. 0,
  520. 0,
  521. originalScope ? originalScope[0] : -1,
  522. originalScope ? originalScope[1] : -1,
  523. ];
  524. if (originalScope?.[2]) {
  525. range.bindings = originalScope[2].map(() => [[-1]]);
  526. }
  527. if (callsite) {
  528. const index = put(sources, callsite.source);
  529. if (index === sourcesContent.length) sourcesContent[index] = null;
  530. if (index === originalScopes.length) originalScopes[index] = [];
  531. range.callsite = [index, callsite.line - 1, callsite.column];
  532. }
  533. if (isScope) range.isScope = true;
  534. generatedRanges.push(range);
  535. return [range, originalScope?.[2]];
  536. }
  537. export function setEndPosition(range: GeneratedRangeInfo, pos: Pos) {
  538. range[0][2] = pos.line - 1;
  539. range[0][3] = pos.column;
  540. }
  541. export function addBinding(
  542. map: GenMapping,
  543. range: GeneratedRangeInfo,
  544. variable: string,
  545. expression: string | BindingExpressionRange,
  546. ) {
  547. const { _names: names } = cast(map);
  548. const bindings = (range[0].bindings ||= []);
  549. const vars = range[1];
  550. const index = vars!.indexOf(variable);
  551. const binding = getIndex(bindings, index);
  552. if (typeof expression === 'string') binding[0] = [put(names, expression)];
  553. else {
  554. const { start } = expression;
  555. binding.push([put(names, expression.expression), start.line - 1, start.column]);
  556. }
  557. }
  558. */