@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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gi-tcg/gts-transpiler",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "repository": "https://github.com/piovium/gts.git",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -16,20 +16,22 @@
16
16
  "dist"
17
17
  ],
18
18
  "dependencies": {
19
- "@jridgewell/sourcemap-codec": "^1.5.5",
20
19
  "@sveltejs/acorn-typescript": "^1.0.8",
21
20
  "acorn": "^8.15.0",
21
+ "dedent": "^1.7.2",
22
+ "espolar": "^0.2.1",
22
23
  "esrap": "2.2.1",
23
24
  "magic-string": "^0.30.21",
24
25
  "path-browserify-esm": "^1.0.6",
25
26
  "zimmerframe": "^1.1.4"
26
27
  },
27
28
  "devDependencies": {
29
+ "@types/estree": "^1.0.9",
28
30
  "@types/node": "^24.3.0",
29
- "@types/estree": "^1.0.8",
30
31
  "@volar/language-core": "^2.4.27"
31
32
  },
32
33
  "scripts": {
33
- "build": "tsdown --platform neutral"
34
+ "build": "tsdown --platform neutral",
35
+ "test": "vitest"
34
36
  }
35
37
  }
@@ -251,7 +251,7 @@ export function gtsPlugin(options: GtsPluginOption = {}) {
251
251
  this.isContextual("as"))
252
252
  ) {
253
253
  // Allow omitting the attribute expression for language tooling
254
- const dummy = this.startNode() as AST.Identifier;
254
+ const dummy = this.startNodeAt(this.lastTokEnd, this.lastTokEndLoc) as AST.Identifier;
255
255
  dummy.name = DUMMY_PLACEHOLDER;
256
256
  dummy.isDummy = true;
257
257
  return this.finishNode(dummy, "Identifier");
@@ -297,7 +297,7 @@ export function gtsPlugin(options: GtsPluginOption = {}) {
297
297
  this.type !== tokTypes.name
298
298
  ) {
299
299
  // Allow omitting the identifier after ':' for language tooling
300
- const dummy = this.startNode() as AST.Identifier;
300
+ const dummy = this.startNodeAt(this.lastTokEnd, this.lastTokEndLoc) as AST.Identifier;
301
301
  dummy.name = DUMMY_PLACEHOLDER;
302
302
  dummy.isDummy = true;
303
303
  node.property = this.finishNode(dummy, "Identifier");
@@ -16,6 +16,7 @@ export function parse(input: string, options?: GtsPluginOption): Program {
16
16
  ecmaVersion: "latest",
17
17
  sourceType: "module",
18
18
  locations: true,
19
+ ranges: true,
19
20
  }) as Program;
20
21
  } catch (e) {
21
22
  if (e instanceof SyntaxError && "loc" in e) {
@@ -54,6 +55,7 @@ export function parseLoose(
54
55
  ecmaVersion: "latest",
55
56
  sourceType: "module",
56
57
  locations: true,
58
+ ranges: true,
57
59
  onComment,
58
60
  }) as Program;
59
61
  addComments(ast);
@@ -29,7 +29,7 @@ export function loosePlugin() {
29
29
  });
30
30
 
31
31
  createDummyIdentifier() {
32
- const dummy = this.startNode() as AST.Identifier;
32
+ const dummy = this.startNodeAt(this.lastTokEnd, this.lastTokEndLoc) as AST.Identifier;
33
33
  dummy.name = DUMMY_PLACEHOLDER;
34
34
  dummy.isDummy = true;
35
35
  return this.finishNode(dummy, "Identifier");
@@ -2,15 +2,15 @@ import { tokTypes, type Parser } from "acorn";
2
2
  import type { AST, Parse } from "../types.ts";
3
3
 
4
4
  /**
5
- * A plugin that records the location of the left parenthesis in CallExpression and
5
+ * A plugin that records the location of the left parenthesis in CallExpression and
6
6
  * NewExpression.
7
- *
8
- * This is useful for Language tooling, that we can maps the '(' token from its location,
9
- * to provide * a signature help for user when they press `(` after a function name.
10
7
  *
11
- * The recorded location will be stored in `lParenLoc` property of the
8
+ * This is useful for Language tooling, that we can maps the '(' token from its location,
9
+ * to provide a signature help for user when they press `(` after a function name.
10
+ *
11
+ * The recorded location will be stored in `lParenLoc` property of the
12
12
  * CallExpression/NewExpression node.
13
- * @returns
13
+ * @returns the plugin function
14
14
  */
15
15
  export function recordCallLParenPlugin() {
16
16
  return function recordCallLParenPluginTransformer(
@@ -26,12 +26,9 @@ export function recordCallLParenPlugin() {
26
26
  optionalChained?: boolean,
27
27
  forInit?: boolean | "await",
28
28
  ): AST.Expression {
29
- let recordedLParenLoc: AST.SourceLocation | null = null;
29
+ let recordedLParenLoc: [number, number] | null = null;
30
30
  if (!noCalls && this.type === tokTypes.parenL) {
31
- recordedLParenLoc = {
32
- start: this.startLoc,
33
- end: this.endLoc,
34
- };
31
+ recordedLParenLoc = [this.start, this.end];
35
32
  }
36
33
  const result = super.parseSubscript(
37
34
  base,
@@ -43,18 +40,15 @@ export function recordCallLParenPlugin() {
43
40
  forInit,
44
41
  );
45
42
  if (recordedLParenLoc && result.type === "CallExpression") {
46
- result.lParenLoc = recordedLParenLoc;
43
+ result.lParenRange = recordedLParenLoc;
47
44
  }
48
45
  return result;
49
46
  }
50
47
 
51
- private _capturedLParenLocFromNew: AST.SourceLocation | null = null;
48
+ private _capturedLParenRangeFromNew: [number, number] | null = null;
52
49
  private readonly _patchedEat = (type: any) => {
53
50
  if (type === tokTypes.parenL) {
54
- this._capturedLParenLocFromNew = {
55
- start: this.startLoc,
56
- end: this.endLoc,
57
- };
51
+ this._capturedLParenRangeFromNew = [this.start, this.end];
58
52
  }
59
53
  return this.eat(type);
60
54
  };
@@ -74,9 +68,12 @@ export function recordCallLParenPlugin() {
74
68
 
75
69
  override parseNew() {
76
70
  const result = super.parseNew.apply(this.#proxiedThis);
77
- if (this._capturedLParenLocFromNew && result.type === "NewExpression") {
78
- result.lParenLoc = this._capturedLParenLocFromNew;
79
- this._capturedLParenLocFromNew = null;
71
+ if (
72
+ this._capturedLParenRangeFromNew &&
73
+ result.type === "NewExpression"
74
+ ) {
75
+ result.lParenRange = this._capturedLParenRangeFromNew;
76
+ this._capturedLParenRangeFromNew = null;
80
77
  }
81
78
  return result;
82
79
  }
@@ -66,6 +66,7 @@ export const commonGtsVisitor: Visitors<Node, TranspileState> = {
66
66
  shorthand: false,
67
67
  value: state.ActionLit,
68
68
  loc: node.loc,
69
+ range: node.range,
69
70
  },
70
71
  {
71
72
  type: "Property",
@@ -108,6 +109,7 @@ export const commonGtsVisitor: Visitors<Node, TranspileState> = {
108
109
  },
109
110
  ],
110
111
  loc: node.loc,
112
+ range: node.range,
111
113
  };
112
114
  },
113
115
  GTSShortcutFunctionExpression(
@@ -120,6 +122,7 @@ export const commonGtsVisitor: Visitors<Node, TranspileState> = {
120
122
  body: visit(node.body) as Expression | BlockStatement,
121
123
  expression: node.expression,
122
124
  loc: node.loc,
125
+ range: node.range,
123
126
  };
124
127
  },
125
128
  GTSShortcutArgumentExpression(node, { state, visit }): MemberExpression {
@@ -130,6 +133,7 @@ export const commonGtsVisitor: Visitors<Node, TranspileState> = {
130
133
  optional: false,
131
134
  property: visit(node.property) as Identifier,
132
135
  loc: node.loc,
136
+ range: node.range,
133
137
  };
134
138
  },
135
139
  GTSQueryExpression(node, { state, visit }) {
@@ -146,6 +150,7 @@ export const commonGtsVisitor: Visitors<Node, TranspileState> = {
146
150
  params: state.queryParameters,
147
151
  expression: true,
148
152
  loc: node.argument.loc,
153
+ range: node.argument.range,
149
154
  },
150
155
  {
151
156
  type: "ObjectExpression",
@@ -174,7 +179,6 @@ export const commonGtsVisitor: Visitors<Node, TranspileState> = {
174
179
  ],
175
180
  },
176
181
  ],
177
- // loc: node.loc,
178
182
  };
179
183
  },
180
184
  };
@@ -279,6 +283,7 @@ const gtsVisitor: Visitors<Node, TranspileState> = {
279
283
  },
280
284
  ],
281
285
  loc: node.loc,
286
+ range: node.range,
282
287
  });
283
288
 
284
289
  state.bindingStatements.push({
@@ -297,6 +302,7 @@ const gtsVisitor: Visitors<Node, TranspileState> = {
297
302
  },
298
303
  ],
299
304
  loc: node.loc,
305
+ range: node.range,
300
306
  });
301
307
 
302
308
  for (let i = 0; i < newBindings.length; i++) {
@@ -338,6 +344,7 @@ const gtsVisitor: Visitors<Node, TranspileState> = {
338
344
  arguments: [state.rootVmId, nodeVarId],
339
345
  },
340
346
  loc: node.loc,
347
+ range: node.range,
341
348
  };
342
349
  },
343
350
  GTSNamedAttributeDefinition(node, { visit, state }) {
@@ -363,6 +370,7 @@ const gtsVisitor: Visitors<Node, TranspileState> = {
363
370
  shorthand: false,
364
371
  value: nameValue,
365
372
  loc: node.loc,
373
+ range: node.range,
366
374
  });
367
375
  const body = { ...namedBody, properties };
368
376
  if (node.bindingName) {
@@ -414,8 +422,10 @@ const gtsVisitor: Visitors<Node, TranspileState> = {
414
422
  body: positionals,
415
423
  expression: true,
416
424
  loc: positionals.loc,
425
+ range: positionals.range,
417
426
  },
418
427
  loc: positionals.loc,
428
+ range: positionals.range,
419
429
  },
420
430
  {
421
431
  type: "Property",
@@ -426,9 +436,11 @@ const gtsVisitor: Visitors<Node, TranspileState> = {
426
436
  shorthand: false,
427
437
  value: named,
428
438
  loc: named.loc,
439
+ range: named.range,
429
440
  },
430
441
  ],
431
442
  loc: node.loc,
443
+ range: node.range,
432
444
  };
433
445
  return partialBody;
434
446
  },
@@ -447,6 +459,7 @@ const gtsVisitor: Visitors<Node, TranspileState> = {
447
459
  }
448
460
  }),
449
461
  loc: node.loc,
462
+ range: node.range,
450
463
  };
451
464
  },
452
465
  GTSNamedAttributeBlock(node, { visit }): ObjectExpression {
@@ -471,6 +484,7 @@ const gtsVisitor: Visitors<Node, TranspileState> = {
471
484
  },
472
485
  ],
473
486
  loc: node.loc,
487
+ range: node.range,
474
488
  };
475
489
  },
476
490
  ...commonGtsVisitor,
@@ -1,6 +1,6 @@
1
1
  import type { Program } from "estree";
2
2
  import { eraseTs } from "./erase_ts.ts";
3
- import { print } from "esrap";
3
+ import { print, type Visitors } from "esrap";
4
4
  import jsPrinter from "esrap/languages/ts";
5
5
  import type { SourceMap } from "magic-string";
6
6
  import { gtsToTs, type TranspileOption } from "./gts.ts";
@@ -18,11 +18,11 @@ export interface SourceInfo {
18
18
  export function transform(
19
19
  ast: Program,
20
20
  option: TranspileOption = {},
21
- sourceInfo: SourceInfo = {}
21
+ sourceInfo: SourceInfo = {},
22
22
  ): TranspileResult {
23
23
  const ts = gtsToTs(ast, option);
24
24
  const js = eraseTs(ts);
25
- const { code, map } = print(js, jsPrinter(), {
25
+ const { code, map } = print(js, jsPrinter() as Visitors, {
26
26
  indent: " ",
27
27
  sourceMapContent: sourceInfo.content,
28
28
  sourceMapSource: sourceInfo.filename,
@@ -1,33 +1,6 @@
1
- import type { Node, SourceLocation } from "estree";
1
+ import type { Node, Program, SourceLocation } from "estree";
2
2
  import { walk } from "zimmerframe";
3
3
 
4
- function isLeafNode(node: any): boolean {
5
- for (const key in node) {
6
- const val = node[key];
7
-
8
- // Ignore non-child properties (metadata)
9
- if (key === "loc" || key === "start" || key === "end" || key === "range") {
10
- continue;
11
- }
12
-
13
- // Check if the property is a Node
14
- // (An object with a 'type' property is generally an AST node)
15
- if (val && typeof val === "object" && typeof val.type === "string") {
16
- return false; // Found a valid child node
17
- }
18
-
19
- // Check if the property is an Array of Nodes (e.g., body: [...])
20
- if (
21
- Array.isArray(val) &&
22
- val.length > 0 &&
23
- typeof val[0].type === "string"
24
- ) {
25
- return false; // Found an array of child nodes
26
- }
27
- }
28
- return true;
29
- }
30
-
31
4
  export interface LeafToken {
32
5
  loc: SourceLocation;
33
6
  isDummy?: boolean;
@@ -53,15 +26,35 @@ export interface LeafToken {
53
26
  generatedLength?: number;
54
27
  }
55
28
 
56
- export function collectLeafTokens(ast: any): LeafToken[] {
57
- const state = {
58
- tokens: [] as LeafToken[],
29
+ export function collectLeafTokens(source: string, ast: Program): LeafToken[] {
30
+ interface CollectTokenState {
31
+ tokens: LeafToken[];
32
+ /** Whether the node is from source and purely TypeScript */
33
+ pureSource: boolean;
34
+ /** Whether the node is visited once (for detecting leaf node) */
35
+ visited: boolean;
36
+ }
37
+ const state: CollectTokenState = {
38
+ tokens: [],
39
+ pureSource: true,
40
+ visited: false,
59
41
  };
60
42
  walk(ast as Node, state, {
61
43
  _(node, { state, next }) {
62
- if (isLeafNode(node) && node.loc) {
44
+ state.visited = true;
45
+ let currNodePureSource = !!node.loc && !node.type.startsWith("GTS");
46
+ const subState = { tokens: [], pureSource: true, visited: false };
47
+ next(subState);
48
+ currNodePureSource &&= subState.pureSource;
49
+ state.pureSource &&= currNodePureSource;
50
+ // record original source for purely branch node
51
+ if (subState.visited && node.range && currNodePureSource) {
52
+ const [start, end] = node.range;
53
+ node.pureSource = source.slice(start, end);
54
+ }
55
+ if (currNodePureSource) {
63
56
  const token: LeafToken = {
64
- loc: node.loc,
57
+ loc: node.loc!,
65
58
  };
66
59
  if ("isDummy" in node && node.isDummy) {
67
60
  token.isDummy = true;
@@ -70,8 +63,9 @@ export function collectLeafTokens(ast: any): LeafToken[] {
70
63
  token.generatedLength = 1;
71
64
  }
72
65
  state.tokens.push(token);
66
+ } else {
67
+ state.tokens.push(...subState.tokens);
73
68
  }
74
- next();
75
69
  },
76
70
  NewExpression(node, { state, next }) {
77
71
  const lParenLoc = node.lParenLoc;
@@ -1,7 +1,7 @@
1
1
  import type { AST } from "../../types.ts";
2
2
  import { walk } from "zimmerframe";
3
3
  import type { SourceInfo, TranspileResult } from "../index.ts";
4
- import { print } from "esrap";
4
+ import { print } from "espolar";
5
5
  import {
6
6
  initialTranspileState,
7
7
  type TranspileOption,
@@ -10,28 +10,19 @@ import {
10
10
  import { gtsToTypingsWalker, type TypingTranspileState } from "./walker.ts";
11
11
  import { applyReplacements } from "./replacements.ts";
12
12
  import type { Program } from "estree";
13
- import { convertToVolarMappings, type VolarMappingResult } from "./mappings.ts";
14
- import { collectLeafTokens, type LeafToken } from "./collect_tokens.ts";
15
- import { patchedPrinter } from "./printer.ts";
13
+ import { VERIFICATION_ONLY_MAPPING_DATA, type VolarMappingResult } from "./mappings.ts";
14
+ import { getPrintOptions } from "./printer.ts";
16
15
 
17
- interface TypingTranspileOption extends TranspileOption {
18
- leafTokens: LeafToken[];
19
- /**
20
- * "row:col" -> "replacement string"
21
- */
22
- extraMappings: Map<string, string>;
23
- }
24
-
25
- function gtsToTypings(
26
- ast: AST.Program,
27
- option: TypingTranspileOption,
28
- ): TranspileResult {
16
+ export function transformForVolar(
17
+ ast: Program,
18
+ option: TranspileOption,
19
+ sourceInfo: Required<SourceInfo>,
20
+ ): VolarMappingResult {
29
21
  const state: TypingTranspileState = {
30
22
  ...(initialTranspileState(option) as Pick<
31
23
  TypingTranspileState,
32
24
  keyof TranspileState
33
25
  >),
34
- leafTokens: option.leafTokens,
35
26
  idCounter: 0,
36
27
  typingPendingStatements: [],
37
28
  rootVmId: { type: "Identifier", name: "__gts_root_vm" },
@@ -44,40 +35,49 @@ function gtsToTypings(
44
35
  metaTypeIdStack: [],
45
36
  finalMetaTypeIdStack: [],
46
37
  attrsOfCurrentVm: [],
47
- extraMappings: option.extraMappings,
38
+
39
+ sourceNodes: new WeakSet(),
40
+ namedAttributeCalleeLParenRange: new WeakMap(),
41
+ literalFromIdentifier: new WeakSet(),
42
+ lastArgNodes: new WeakSet(),
43
+ diagnosticsOnTopNodes: new WeakSet(),
44
+ extraMappings: [],
48
45
  };
49
- const newAst = walk(ast as AST.Node, state, gtsToTypingsWalker);
50
- const { code, map } = print(newAst, patchedPrinter, {
51
- indent: " ",
46
+ // mark sourceNodes before the transformation
47
+ walk(ast as AST.Node, state, {
48
+ _(node, { state, next }) {
49
+ if (node.range) {
50
+ state.sourceNodes.add(node);
51
+ }
52
+ next();
53
+ },
52
54
  });
53
- return {
54
- code: applyReplacements(state, code),
55
- sourceMap: map,
56
- };
57
- }
58
-
59
- export function transformForVolar(
60
- ast: Program,
61
- option: TranspileOption,
62
- sourceInfo: Required<SourceInfo>,
63
- ): VolarMappingResult {
64
- const tokens = collectLeafTokens(ast);
65
- const extraMappings = new Map<string, string>();
66
- const { code, sourceMap } = gtsToTypings(ast, {
67
- ...option,
68
- leafTokens: tokens,
69
- extraMappings,
55
+ const newAst = walk(ast as AST.Node, state, gtsToTypingsWalker);
56
+ // mark lastArgNodes
57
+ walk(newAst as AST.Node, state, {
58
+ CallExpression(node, { state }) {
59
+ const lastArg = node.arguments.at(-1);
60
+ if (lastArg) {
61
+ state.lastArgNodes.add(lastArg);
62
+ }
63
+ }
70
64
  });
71
- const volarMappings = convertToVolarMappings(
72
- code,
73
- sourceInfo.content,
74
- sourceMap,
75
- tokens,
76
- extraMappings,
77
- );
65
+ const printOptions = getPrintOptions(sourceInfo.content, state);
66
+ let { code, mappings } = print(newAst, printOptions);
67
+ code = applyReplacements(state, code, mappings);
68
+ for (const extraMapping of state.extraMappings) {
69
+ const genOffset = code.indexOf(extraMapping.generatedNeedle);
70
+ mappings.push({
71
+ sourceOffsets: [extraMapping.sourceOffset],
72
+ lengths: [extraMapping.length],
73
+ generatedOffsets: [genOffset],
74
+ generatedLengths: [extraMapping.generatedNeedle.length],
75
+ data: VERIFICATION_ONLY_MAPPING_DATA,
76
+ });
77
+ }
78
78
  return {
79
79
  code,
80
- mappings: volarMappings,
80
+ mappings,
81
81
  };
82
82
  }
83
83