@gi-tcg/gts-transpiler 0.3.2 → 0.3.4

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.4",
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.5.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
  }
@@ -10,7 +10,6 @@ import type {
10
10
  ModuleDeclaration,
11
11
  Node,
12
12
  ObjectExpression,
13
- ObjectPattern,
14
13
  Pattern,
15
14
  Program,
16
15
  Statement,
@@ -35,14 +34,12 @@ export interface TranspileState {
35
34
  readonly fnArgId: Identifier;
36
35
  readonly shortcutFunctionParameters: Pattern[];
37
36
  readonly rootVmId: Identifier;
38
- readonly queryFnId: Identifier;
39
37
  readonly queryParameters: Pattern[];
38
+ readonly QueryLit: Literal;
39
+ readonly QueryAllLit: Literal;
40
40
 
41
41
  readonly runtimeImportSource: string;
42
42
  readonly providerImportSource: string;
43
- readonly queryArg: ObjectPattern;
44
-
45
- hasQueryExpressions: boolean;
46
43
 
47
44
  externalizedBindings: ExternalizedBinding[];
48
45
  /** Internal counters / state for emitting per-define nodes & bindings */
@@ -66,6 +63,7 @@ export const commonGtsVisitor: Visitors<Node, TranspileState> = {
66
63
  shorthand: false,
67
64
  value: state.ActionLit,
68
65
  loc: node.loc,
66
+ range: node.range,
69
67
  },
70
68
  {
71
69
  type: "Property",
@@ -108,6 +106,7 @@ export const commonGtsVisitor: Visitors<Node, TranspileState> = {
108
106
  },
109
107
  ],
110
108
  loc: node.loc,
109
+ range: node.range,
111
110
  };
112
111
  },
113
112
  GTSShortcutFunctionExpression(
@@ -120,6 +119,7 @@ export const commonGtsVisitor: Visitors<Node, TranspileState> = {
120
119
  body: visit(node.body) as Expression | BlockStatement,
121
120
  expression: node.expression,
122
121
  loc: node.loc,
122
+ range: node.range,
123
123
  };
124
124
  },
125
125
  GTSShortcutArgumentExpression(node, { state, visit }): MemberExpression {
@@ -130,15 +130,21 @@ export const commonGtsVisitor: Visitors<Node, TranspileState> = {
130
130
  optional: false,
131
131
  property: visit(node.property) as Identifier,
132
132
  loc: node.loc,
133
+ range: node.range,
133
134
  };
134
135
  },
135
136
  GTSQueryExpression(node, { state, visit }) {
136
- state.hasQueryExpressions = true;
137
137
  return {
138
138
  ...node,
139
139
  type: "CallExpression",
140
140
  optional: false,
141
- callee: state.queryFnId,
141
+ callee: {
142
+ type: "MemberExpression",
143
+ object: state.fnArgId,
144
+ property: node.star ? state.QueryAllLit : state.QueryLit,
145
+ computed: true,
146
+ optional: false,
147
+ },
142
148
  arguments: [
143
149
  {
144
150
  type: "ArrowFunctionExpression",
@@ -146,47 +152,14 @@ export const commonGtsVisitor: Visitors<Node, TranspileState> = {
146
152
  params: state.queryParameters,
147
153
  expression: true,
148
154
  loc: node.argument.loc,
149
- },
150
- {
151
- type: "ObjectExpression",
152
- properties: [
153
- {
154
- type: "Property",
155
- key: { type: "Identifier", name: "star" },
156
- computed: false,
157
- kind: "init",
158
- method: false,
159
- shorthand: false,
160
- value: {
161
- type: "Literal",
162
- value: !!node.star,
163
- },
164
- },
165
- {
166
- type: "Property",
167
- key: { type: "Identifier", name: "context" },
168
- computed: false,
169
- kind: "init",
170
- method: false,
171
- shorthand: false,
172
- value: state.fnArgId,
173
- }
174
- ],
155
+ range: node.argument.range,
175
156
  },
176
157
  ],
177
- // loc: node.loc,
178
158
  };
179
159
  },
180
160
  };
181
161
 
182
162
  const gtsVisitor: Visitors<Node, TranspileState> = {
183
- // _(node, { next }) {
184
- // console.log(node.type, !!node.leadingComments)
185
- // if (node.leadingComments) {
186
- // console.log(node.leadingComments);
187
- // }
188
- // return next();
189
- // },
190
163
  Program(node, { state, visit }) {
191
164
  const body: Program["body"] = [];
192
165
  for (const stmt of node.body) {
@@ -197,22 +170,6 @@ const gtsVisitor: Visitors<Node, TranspileState> = {
197
170
  body.unshift(...state.bindingStatements);
198
171
  state.bindingStatements = [];
199
172
 
200
- if (state.hasQueryExpressions) {
201
- body.unshift({
202
- type: "ImportDeclaration",
203
- specifiers: [
204
- {
205
- type: "ImportDefaultSpecifier",
206
- local: state.queryFnId,
207
- },
208
- ],
209
- source: {
210
- type: "Literal",
211
- value: `${state.providerImportSource}/query`,
212
- },
213
- attributes: [],
214
- });
215
- }
216
173
  body.unshift(
217
174
  {
218
175
  type: "ImportDeclaration",
@@ -279,6 +236,7 @@ const gtsVisitor: Visitors<Node, TranspileState> = {
279
236
  },
280
237
  ],
281
238
  loc: node.loc,
239
+ range: node.range,
282
240
  });
283
241
 
284
242
  state.bindingStatements.push({
@@ -297,6 +255,7 @@ const gtsVisitor: Visitors<Node, TranspileState> = {
297
255
  },
298
256
  ],
299
257
  loc: node.loc,
258
+ range: node.range,
300
259
  });
301
260
 
302
261
  for (let i = 0; i < newBindings.length; i++) {
@@ -338,6 +297,7 @@ const gtsVisitor: Visitors<Node, TranspileState> = {
338
297
  arguments: [state.rootVmId, nodeVarId],
339
298
  },
340
299
  loc: node.loc,
300
+ range: node.range,
341
301
  };
342
302
  },
343
303
  GTSNamedAttributeDefinition(node, { visit, state }) {
@@ -363,6 +323,7 @@ const gtsVisitor: Visitors<Node, TranspileState> = {
363
323
  shorthand: false,
364
324
  value: nameValue,
365
325
  loc: node.loc,
326
+ range: node.range,
366
327
  });
367
328
  const body = { ...namedBody, properties };
368
329
  if (node.bindingName) {
@@ -414,8 +375,10 @@ const gtsVisitor: Visitors<Node, TranspileState> = {
414
375
  body: positionals,
415
376
  expression: true,
416
377
  loc: positionals.loc,
378
+ range: positionals.range,
417
379
  },
418
380
  loc: positionals.loc,
381
+ range: positionals.range,
419
382
  },
420
383
  {
421
384
  type: "Property",
@@ -426,9 +389,11 @@ const gtsVisitor: Visitors<Node, TranspileState> = {
426
389
  shorthand: false,
427
390
  value: named,
428
391
  loc: named.loc,
392
+ range: named.range,
429
393
  },
430
394
  ],
431
395
  loc: node.loc,
396
+ range: node.range,
432
397
  };
433
398
  return partialBody;
434
399
  },
@@ -447,6 +412,7 @@ const gtsVisitor: Visitors<Node, TranspileState> = {
447
412
  }
448
413
  }),
449
414
  loc: node.loc,
415
+ range: node.range,
450
416
  };
451
417
  },
452
418
  GTSNamedAttributeBlock(node, { visit }): ObjectExpression {
@@ -471,6 +437,7 @@ const gtsVisitor: Visitors<Node, TranspileState> = {
471
437
  },
472
438
  ],
473
439
  loc: node.loc,
440
+ range: node.range,
474
441
  };
475
442
  },
476
443
  ...commonGtsVisitor,
@@ -541,26 +508,14 @@ export const initialTranspileState = (
541
508
  fnArgId,
542
509
  shortcutFunctionParameters,
543
510
  rootVmId: { type: "Identifier", name: "__gts_rootVm" },
544
- queryFnId: { type: "Identifier", name: "__gts_query" },
545
511
  queryParameters,
512
+ QueryLit: { type: "Literal", value: "~query" },
513
+ QueryAllLit: { type: "Literal", value: "~queryAll" },
546
514
 
547
515
  runtimeImportSource: option.runtimeImportSource ?? "@gi-tcg/gts-runtime",
548
516
  providerImportSource: option.providerImportSource ?? "@gi-tcg/core/gts",
549
- queryArg: {
550
- type: "ObjectPattern",
551
- properties: (option.queryBindings ?? []).map((name) => ({
552
- type: "Property",
553
- key: { type: "Identifier", name },
554
- computed: false,
555
- kind: "init",
556
- method: false,
557
- shorthand: true,
558
- value: { type: "Identifier", name },
559
- })),
560
- },
561
517
 
562
518
  externalizedBindings: [],
563
- hasQueryExpressions: false,
564
519
  defineIdCounter: 0,
565
520
 
566
521
  bindingStatements: [],
@@ -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;
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Get the character offset after hashbang and file-scope leading comments.
3
+ * This is for simulating the behavior of TSServer insert auto-imports if no
4
+ * imports are present.
5
+ * @param source
6
+ * @returns
7
+ */
8
+ export function getContentStartOffset(source: string): number {
9
+ let result = 0;
10
+ /** Current visiting character */
11
+ let pos = 0;
12
+ if (source.startsWith("#!")) {
13
+ const nl = source.indexOf("\n", pos);
14
+ if (nl === -1) {
15
+ return source.length;
16
+ }
17
+ pos = nl + 1;
18
+ }
19
+ const skipWhitespaces = () => {
20
+ let newlineCount = 0;
21
+ for (; pos < source.length; pos++) {
22
+ const ch = source[pos];
23
+ if (ch === " " || ch === "\t" || ch === "\r") {
24
+ continue;
25
+ }
26
+ if (ch === "\n") {
27
+ newlineCount++;
28
+ continue;
29
+ }
30
+ break;
31
+ }
32
+ return newlineCount;
33
+ }
34
+ skipWhitespaces();
35
+ while (pos < source.length) {
36
+ let newlineCount = 0;
37
+ // Eat a comment
38
+ if (source[pos] === "/" && pos + 1 < source.length) {
39
+ if (source[pos + 1] === "/") {
40
+ const nl = source.indexOf("\n", pos);
41
+ pos = nl === -1 ? source.length : nl + 1;
42
+ newlineCount++;
43
+ } else if (source[pos + 1] === "*") {
44
+ pos += 2;
45
+ while (pos < source.length) {
46
+ if (
47
+ source[pos] === "*" &&
48
+ pos + 1 < source.length &&
49
+ source[pos + 1] === "/"
50
+ ) {
51
+ pos += 2;
52
+ break;
53
+ }
54
+ pos++;
55
+ }
56
+ }
57
+ result = pos;
58
+ newlineCount += skipWhitespaces();
59
+ // If there are already 2 newlines in whitespace, the leading comments are ended
60
+ if (newlineCount >= 2) {
61
+ break;
62
+ }
63
+ } else {
64
+ // Not a comment, stop here
65
+ break;
66
+ }
67
+ }
68
+ return result;
69
+ }