@gi-tcg/gts-transpiler 0.3.2 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,13 +1,11 @@
1
- import { decode } from "@jridgewell/sourcemap-codec";
2
1
  import type { CodeInformation, CodeMapping } from "@volar/language-core";
3
- import type { LeafToken } from "./collect_tokens.ts";
4
2
 
5
3
  export interface VolarMappingResult {
6
4
  code: string;
7
5
  mappings: CodeMapping[];
8
6
  }
9
7
 
10
- const DEFAULT_VOLAR_MAPPING_DATA: CodeInformation = {
8
+ export const DEFAULT_VOLAR_MAPPING_DATA: CodeInformation = {
11
9
  completion: true,
12
10
  format: true,
13
11
  navigation: true,
@@ -15,354 +13,9 @@ const DEFAULT_VOLAR_MAPPING_DATA: CodeInformation = {
15
13
  structure: true,
16
14
  verification: true,
17
15
  };
18
-
19
- interface SourceMap {
20
- mappings: string;
21
- }
22
-
23
- interface CodePosition {
24
- line: number;
25
- column: number;
26
- end_line: number;
27
- end_column: number;
28
- code: string;
29
- }
30
-
31
- type LineOffsets = number[];
32
-
33
- type CodeToGeneratedMap = Map<string, CodePosition[]>;
34
-
35
- /**
36
- * Convert byte offset to line/column
37
- * @param offset
38
- * @param line_offsets
39
- * @returns */
40
- export const offset_to_line_col = (
41
- offset: number,
42
- line_offsets: LineOffsets,
43
- ): { line: number; column: number } => {
44
- // Binary search
45
- let left = 0;
46
- let right = line_offsets.length - 1;
47
- let line = 1;
48
-
49
- while (left <= right) {
50
- const mid = Math.floor((left + right) / 2);
51
- if (
52
- offset >= line_offsets[mid] &&
53
- (mid === line_offsets.length - 1 || offset < line_offsets[mid + 1])
54
- ) {
55
- line = mid + 1;
56
- break;
57
- } else if (offset < line_offsets[mid]) {
58
- right = mid - 1;
59
- } else {
60
- left = mid + 1;
61
- }
62
- }
63
-
64
- const column = offset - line_offsets[line - 1];
65
- return { line, column };
16
+ export const VERIFICATION_ONLY_MAPPING_DATA: CodeInformation = {
17
+ verification: true,
66
18
  };
67
19
 
68
- /**
69
- * Build a source-to-generated position lookup map from an esrap source map
70
- * Applies post-processing adjustments during map building for efficiency
71
- * @param source_map - The source map object from esrap (v3 format)
72
- * @param line_offsets - Pre-computed line offsets array
73
- * @param generated_code - The final generated code (after post-processing)
74
- * @returns source-to-generated map
75
- */
76
- export function buildSrcToGenMap(
77
- source_map: SourceMap,
78
- line_offsets: LineOffsets,
79
- generated_code: string,
80
- ): CodeToGeneratedMap {
81
- const map: CodeToGeneratedMap = new Map();
82
-
83
- // Decode the VLQ-encoded mappings string
84
- const decoded = decode(source_map.mappings);
85
-
86
- /**
87
- * Convert line/column position to byte offset
88
- * @param {number} line - 1-based line number
89
- * @param {number} column - 0-based column number
90
- * @returns {number} Byte offset
91
- */
92
- const line_col_to_byte_offset = (line: number, column: number) => {
93
- return line_offsets[line - 1] + column;
94
- };
95
-
96
- // Apply post-processing adjustments to all segments first
97
- const adjusted_segments: {
98
- line: number;
99
- column: number;
100
- sourceLine: number;
101
- sourceColumn: number;
102
- }[][] = [];
103
-
104
- for (
105
- let generated_line = 0;
106
- generated_line < decoded.length;
107
- generated_line++
108
- ) {
109
- const line = decoded[generated_line];
110
- adjusted_segments[generated_line] = [];
111
-
112
- for (const segment of line) {
113
- if (segment.length >= 4) {
114
- let adjusted_line = generated_line + 1;
115
- let adjusted_column = segment[0];
116
- adjusted_segments[generated_line].push({
117
- line: adjusted_line,
118
- column: adjusted_column,
119
- sourceLine: segment[2]!,
120
- sourceColumn: segment[3]!,
121
- });
122
- }
123
- }
124
- }
125
-
126
- // Now build the map using adjusted positions
127
- for (let line_idx = 0; line_idx < adjusted_segments.length; line_idx++) {
128
- const line_segments = adjusted_segments[line_idx];
129
-
130
- for (let seg_idx = 0; seg_idx < line_segments.length; seg_idx++) {
131
- const segment = line_segments[seg_idx];
132
- const line = segment.line;
133
- const column = segment.column;
134
-
135
- // Determine end position using next segment
136
- let end_line = line;
137
- let end_column = column;
138
-
139
- // Look for next segment to determine end position
140
- if (seg_idx + 1 < line_segments.length) {
141
- // Next segment on same line
142
- const next_segment = line_segments[seg_idx + 1];
143
- end_line = next_segment.line;
144
- end_column = next_segment.column;
145
- } else if (
146
- line_idx + 1 < adjusted_segments.length &&
147
- adjusted_segments[line_idx + 1].length > 0
148
- ) {
149
- // Look at first segment of next line
150
- const next_segment = adjusted_segments[line_idx + 1][0];
151
- end_line = next_segment.line;
152
- end_column = next_segment.column;
153
- }
154
-
155
- // Extract code snippet
156
- const start_offset = line_col_to_byte_offset(line, column);
157
- const end_offset = line_col_to_byte_offset(end_line, end_column);
158
- const code_snippet = generated_code.slice(start_offset, end_offset);
159
-
160
- // Create key from source position (1-indexed line, 0-indexed column)
161
- segment.sourceLine += 1;
162
- const key = `${segment.sourceLine}:${segment.sourceColumn}`;
163
-
164
- // Store adjusted generated position with code snippet
165
- const gen_pos = {
166
- line,
167
- column,
168
- end_line,
169
- end_column,
170
- code: code_snippet,
171
- metadata: {},
172
- };
173
-
174
- if (!map.has(key)) {
175
- map.set(key, []);
176
- }
177
- map.get(key)!.push(gen_pos);
178
- }
179
- }
180
-
181
- return map;
182
- }
183
-
184
- /**
185
- * Look up generated position for a given source position
186
- * @param src_line - 1-based line number in source
187
- * @param src_column - 0-based column number in source
188
- * @param srcToGenMap - Lookup map
189
- * @returns Generated position
190
- */
191
- export function getGeneratedPosition(
192
- src_line: number,
193
- src_column: number,
194
- srcToGenMap: CodeToGeneratedMap,
195
- ): CodePosition[] {
196
- const key = `${src_line}:${src_column}`;
197
- const positions = srcToGenMap.get(key);
198
-
199
- // If multiple generated positions map to same source, return the first
200
- return positions || [];
201
- }
202
- // Helper to create a line-to-offset lookup table
203
- function createLineOffsets(content: string): number[] {
204
- const lines = content.split("\n");
205
- const offsets: number[] = [];
206
- let currentOffset = 0;
207
-
208
- for (const line of lines) {
209
- offsets.push(currentOffset);
210
- // +1 for the newline character (handle \r\n vs \n if necessary)
211
- currentOffset += line.length + 1;
212
- }
213
- return offsets;
214
- }
215
-
216
- /**
217
- * Convert line/column to byte offset
218
- * @param line
219
- * @param column
220
- * @param line_offsets
221
- * @returns
222
- */
223
- function locToOffset(
224
- line: number,
225
- column: number,
226
- line_offsets: number[],
227
- ): number {
228
- if (line < 1 || line > line_offsets.length) {
229
- // throw new Error(
230
- // `Location line or line offsets length is out of bounds, line: ${line}, line offsets length: ${line_offsets.length}`
231
- // );
232
- }
233
- return line_offsets[line - 1] + column;
234
- }
235
-
236
- export function convertToVolarMappings(
237
- generated: string,
238
- source: string,
239
- sourceMap: SourceMap,
240
- tokens: LeafToken[],
241
- extraMappings: Map<string, string>,
242
- ): CodeMapping[] {
243
- const sourceLineOffsets = createLineOffsets(source);
244
- const generatedLineOffsets = createLineOffsets(generated);
245
- const srcToGenMap = buildSrcToGenMap(
246
- sourceMap,
247
- generatedLineOffsets,
248
- generated,
249
- );
250
-
251
- const mappings: CodeMapping[] = [];
252
-
253
- for (const token of tokens) {
254
- let sourceStart = locToOffset(
255
- token.loc.start.line,
256
- token.loc.start.column,
257
- sourceLineOffsets,
258
- );
259
- sourceStart += token.sourceStartOffset ?? 0;
260
-
261
- const sourceEnd = locToOffset(
262
- token.loc.end.line,
263
- token.loc.end.column,
264
- sourceLineOffsets,
265
- );
266
-
267
- let sourceLength = token.sourceLength ?? sourceEnd - sourceStart;
268
-
269
- sourceLength += token.sourceLengthOffset ?? 0;
270
- const [genLineCol] = getGeneratedPosition(
271
- token.loc.start.line,
272
- token.loc.start.column,
273
- srcToGenMap,
274
- );
275
- if (!genLineCol) {
276
- // No mapping found for this token - skip it
277
- continue;
278
- }
279
- let genStart = locToOffset(
280
- genLineCol.line,
281
- genLineCol.column,
282
- generatedLineOffsets,
283
- );
284
- if (token.isDummy) {
285
- // A dummy token might be generated for a missing property / argument.
286
- // Notice that when facing this scenario, the parser tries to 'defer' and step through
287
- // all whitespaces and insert the invalid node just before the next token.
288
- // But in a mapping context, we need the caret next to the previous token (commonly the `.` dot)
289
- // to allow triggering completion correctly. So we adjust the sourceStart and sourceLength accordingly.
290
- // After adjustment, the mapping will include all whitespaces as the invalid node and maps to an empty string.
291
- while (sourceStart > 0 && /\s/.test(source[sourceStart - 1])) {
292
- sourceStart--;
293
- sourceLength++;
294
- }
295
- }
296
- const generatedLength = token.generatedLength ?? sourceLength;
297
-
298
- if (
299
- typeof token.generatedStartOffset === "number" &&
300
- token.generatedStartOffset > 0
301
- ) {
302
- // maps verification back to the start of source
303
- mappings.push({
304
- sourceOffsets: [sourceStart],
305
- generatedOffsets: [genStart],
306
- lengths: [0],
307
- generatedLengths: [generatedLength],
308
- data: {
309
- verification: true,
310
- },
311
- });
312
- genStart += token.generatedStartOffset;
313
- }
314
-
315
- mappings.push({
316
- sourceOffsets: [sourceStart],
317
- generatedOffsets: [genStart],
318
- lengths: [sourceLength],
319
- generatedLengths: [generatedLength],
320
- data: DEFAULT_VOLAR_MAPPING_DATA,
321
- });
322
- }
323
-
324
- // Handle extra mappings that purely generated and wants a diagnostic
325
- // that appears on the top of file.
326
- // We'd add these mappings at walker that they will have a `loc` to `1:0`
327
- // so find them by looking up a source position of `1:0` will work
328
- const locMapsToTop = getGeneratedPosition(1, 0, srcToGenMap);
329
- for (const loc of locMapsToTop) {
330
- const offset = locToOffset(loc.line, loc.column, generatedLineOffsets);
331
- mappings.push({
332
- sourceOffsets: [0],
333
- generatedOffsets: [offset],
334
- lengths: [0],
335
- generatedLengths: [0],
336
- data: {
337
- verification: true,
338
- },
339
- });
340
- }
341
-
342
- for (const [loc, codeSnippet] of extraMappings) {
343
- const generatedStart = generated.indexOf(codeSnippet);
344
- if (generatedStart === -1) {
345
- continue;
346
- }
347
- const [lineStr, columnStr] = loc.split(":");
348
- const line = Number(lineStr);
349
- const column = Number(columnStr);
350
- const sourceStart = locToOffset(line, column, sourceLineOffsets);
351
- const sourceLength = 1;
352
- const generatedLength = codeSnippet.length;
353
- mappings.push({
354
- sourceOffsets: [sourceStart],
355
- generatedOffsets: [generatedStart],
356
- lengths: [sourceLength],
357
- generatedLengths: [generatedLength],
358
- data: {
359
- verification: true,
360
- },
361
- });
362
- }
363
-
364
- // Sort mappings by source offset // Sort mappings by source offset
365
- mappings.sort((a, b) => a.sourceOffsets[0] - b.sourceOffsets[0]);
366
-
367
- return mappings;
368
- }
20
+ Object.freeze(DEFAULT_VOLAR_MAPPING_DATA);
21
+ Object.freeze(VERIFICATION_ONLY_MAPPING_DATA);
@@ -1,123 +1,107 @@
1
- import tsPrinter from "esrap/languages/ts";
2
- import type { AST } from "../../types.ts";
3
- import type { Context, Visitors } from "esrap";
4
- import type { Node, SimpleCallExpression } from "estree";
1
+ import type {
2
+ Identifier,
3
+ NewExpression,
4
+ Node,
5
+ SimpleCallExpression,
6
+ } from "estree";
7
+ import {
8
+ type PrintOptions,
9
+ type AST as EspolarAST,
10
+ defaultPrinters,
11
+ } from "espolar";
12
+ import type { CodeInformation } from "@volar/language-core";
13
+ import {
14
+ DEFAULT_VOLAR_MAPPING_DATA,
15
+ VERIFICATION_ONLY_MAPPING_DATA,
16
+ } from "./mappings.ts";
17
+ import type { TypingTranspileState } from "./walker.ts";
5
18
 
6
- const printer = tsPrinter({
7
- getLeadingComments: (node) => (node as AST.Node).leadingComments,
8
- getTrailingComments: (node) => (node as AST.Node).trailingComments,
9
- });
10
-
11
- // Make the print of dummy identifier print nothing.
12
- // Exception: if GTS attribute list's last argument is dummy, e.g.
13
- // foo bar, ;
14
- // ^~ here
15
- // Then the printed JS will be `foo(bar, )` which WILL NOT be syntax error in ES6.
16
- // So we mark the lastArg manually and print an additional comma
17
- // for this dummy identifier, i.e. `foo(bar,,)` and TypeScript will recognize the error.
18
-
19
- const prevIdentifier = printer.Identifier!;
20
- printer.Identifier = function (node, context) {
21
- if (node.isDummy) {
22
- const text = Reflect.get(node, "lastArg") ? "," : "";
23
- context.write(text, node);
24
- } else {
25
- prevIdentifier(node, context);
26
- }
27
- };
28
-
29
- const prevCallNewExpression = printer.CallExpression!;
30
- const newCallNewExpression: typeof prevCallNewExpression = function (
31
- node: SimpleCallExpression,
32
- context,
33
- ) {
34
- const lastArg = node.arguments.at(-1);
35
- if (lastArg) {
36
- Reflect.set(lastArg, "lastArg", true);
37
- }
38
- if (!node.lParenLoc) {
39
- return prevCallNewExpression(node, context);
40
- }
41
- let hasDeferredWrite = false;
42
- let interceptionDone = false;
43
- const lParenFakeNode = {
44
- loc: node.lParenLoc!,
45
- } as AST.Node;
46
-
47
- // Map the print of `(` with the fake lParen node.
48
- // The print of `(` can be happened in two area:
49
- // 1. The wrapped parenthesis towards callee, which follows a `context.visit` to the callee;
50
- // 2. The argument list (what we should map), which follows a `context.append` call
51
- // so we defer the write of `(` to the next call of `context.visit|append`.
52
- // If it is `context.append`, then make `context.write("(")` happens with our fake node
53
- // and mapping will be added to the final code-mapping. Otherwise, keep original write.
54
-
55
- const patchedWrite = (text: string, node?: AST.Node) => {
56
- if (text === "(") {
57
- hasDeferredWrite = true;
58
- } else {
59
- context.write(text, node);
60
- }
61
- };
62
- const patchedVisit = (node: AST.Node) => {
63
- if (hasDeferredWrite) {
64
- context.write("(");
65
- }
66
- return context.visit(node);
67
- };
68
- const patchedAppend = (subcontext: Context) => {
69
- if (hasDeferredWrite) {
70
- context.write("(", lParenFakeNode);
71
- // console.log("Inserted fake [LPAREN] for node at ", lParenFakeNode.loc);
72
- interceptionDone = true;
73
- }
74
- return context.append(subcontext);
75
- };
76
- const proxiedContext = new Proxy(context, {
77
- get(target, prop) {
78
- if (!interceptionDone) {
79
- if (prop === "write") {
80
- return patchedWrite;
19
+ export function getPrintOptions(
20
+ source: string,
21
+ state: TypingTranspileState,
22
+ ): PrintOptions<CodeInformation> {
23
+ return {
24
+ source,
25
+ isUntouched: (node) => {
26
+ if (node.type === "Identifier" && (node as Identifier).isDummy) {
27
+ return false;
28
+ }
29
+ return state.sourceNodes.has(node as Node);
30
+ },
31
+ getLeadingComments: (node) => (node as Node).leadingComments,
32
+ getTrailingComments: (node) => (node as Node).trailingComments,
33
+ getMappingData: () => DEFAULT_VOLAR_MAPPING_DATA,
34
+ printers: {
35
+ // Make the print of dummy identifier print nothing.
36
+ // Exception: if GTS attribute list's last argument is dummy, e.g.
37
+ // foo bar, ;
38
+ // ^~ here
39
+ // Then the printed JS will be `foo(bar, )` which WILL NOT be syntax error in ES6.
40
+ // So we mark the lastArg manually and print an additional comma
41
+ // for this dummy identifier, i.e. `foo(bar,,)` and TypeScript will recognize the error.
42
+ Identifier(node, context) {
43
+ const identifier = node as Identifier;
44
+ if (identifier.isDummy && identifier.range) {
45
+ const text = state.lastArgNodes.has(identifier) ? "," : "";
46
+ // extend the source mapping of dummy id to the before next token
47
+ let firstNonWhiteSpaceIndex = context.source
48
+ .slice(identifier.range[1])
49
+ .search(/\S/);
50
+ const rangeEnd =
51
+ firstNonWhiteSpaceIndex === -1
52
+ ? context.source.length
53
+ : identifier.range[1] + firstNonWhiteSpaceIndex;
54
+ context.writeMapped(text, identifier.range[0], rangeEnd);
55
+ } else {
56
+ return defaultPrinters.Identifier(node, context);
81
57
  }
82
- if (prop === "visit") {
83
- return patchedVisit;
58
+ },
59
+ Literal(node, context) {
60
+ const generatedStart = context.generatedOffset;
61
+ if (state.literalFromIdentifier.has(node) && node.range) {
62
+ const text = JSON.stringify(node.value);
63
+ context.write('"');
64
+ context.writeMapped(text.slice(1, -1), node.range[0], node.range[1]);
65
+ context.write('"');
66
+ } else {
67
+ defaultPrinters.Literal(node, context);
84
68
  }
85
- if (prop === "append") {
86
- return patchedAppend;
69
+ // For generated `import xxx from "yyy"`, add mappings from xxx and yyy
70
+ // to the top-of-file for diagnostics around missing / wrong imports.
71
+ if (state.diagnosticsOnTopNodes.has(node)) {
72
+ const generatedEnd = context.generatedOffset;
73
+ context.createExtraMapping(
74
+ { start: 0, end: 1 },
75
+ generatedStart,
76
+ generatedEnd,
77
+ VERIFICATION_ONLY_MAPPING_DATA,
78
+ );
87
79
  }
80
+ },
81
+ ImportDefaultSpecifier(node, context) {
82
+ const generatedStart = context.generatedOffset;
83
+ defaultPrinters.ImportDefaultSpecifier(node, context);
84
+ if (state.diagnosticsOnTopNodes.has(node as Node)) {
85
+ const generatedEnd = context.generatedOffset;
86
+ context.createExtraMapping(
87
+ { start: 0, end: 1 },
88
+ generatedStart,
89
+ generatedEnd,
90
+ VERIFICATION_ONLY_MAPPING_DATA,
91
+ );
92
+ }
93
+ },
94
+ },
95
+ // Enable triggering signature completion
96
+ experimentalGetLeftParenSourceRange: (node) => {
97
+ const callLike = node as SimpleCallExpression | NewExpression;
98
+ if (callLike.lParenRange) {
99
+ return {
100
+ start: callLike.lParenRange[0],
101
+ end: callLike.lParenRange[1],
102
+ };
88
103
  }
89
- const value = Reflect.get(target, prop);
90
- if (typeof value === "function") {
91
- return value.bind(target);
92
- }
93
- return value;
104
+ return state.namedAttributeCalleeLParenRange.get(callLike.callee);
94
105
  },
95
- });
96
- return prevCallNewExpression(node, proxiedContext);
97
- };
98
- printer.CallExpression = newCallNewExpression;
99
- printer.NewExpression = newCallNewExpression;
100
-
101
- // Handle node with `diagnosticsOnTop`. These nodes and their children will be
102
- // printed with an extra `loc` that point to the beginning of the source file,
103
- // so the source-mapping will include them with a 0:1 -> generated position entry.
104
-
105
- const prevWildcard = printer._!;
106
- printer._ = (node: Node, context, visit) => {
107
- const contextProto = Object.getPrototypeOf(context);
108
- const prevContextWrite: typeof context.write = contextProto.write;
109
- if ("diagnosticsOnTop" in node && node.diagnosticsOnTop) {
110
- contextProto.write = function (text: string, node?: AST.Node) {
111
- node ??= {} as AST.Node;
112
- node.loc ??= {
113
- start: { line: 1, column: 0 },
114
- end: { line: 1, column: 0 },
115
- };
116
- return prevContextWrite.call(this, text, node);
117
- };
118
- }
119
- prevWildcard(node, context, visit);
120
- contextProto.write = prevContextWrite;
121
- };
122
-
123
- export const patchedPrinter: Visitors<AST.Node> = printer;
106
+ };
107
+ }