@gi-tcg/gts-transpiler 0.2.0 → 0.3.0

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.
@@ -2,7 +2,6 @@ import type { AST } from "../../types";
2
2
  import { walk } from "zimmerframe";
3
3
  import type { SourceInfo, TranspileResult } from "..";
4
4
  import { print } from "esrap";
5
- import tsPrinter from "esrap/languages/ts";
6
5
  import {
7
6
  initialTranspileState,
8
7
  type TranspileOption,
@@ -13,16 +12,19 @@ import { applyReplacements } from "./replacements";
13
12
  import type { Program } from "estree";
14
13
  import { convertToVolarMappings, type VolarMappingResult } from "./mappings";
15
14
  import { collectLeafTokens, type LeafToken } from "./collect_tokens";
15
+ import { patchedPrinter } from "./printer";
16
16
 
17
17
  interface TypingTranspileOption extends TranspileOption {
18
18
  leafTokens: LeafToken[];
19
- // "row:col" -> "replacement string"
20
- additionalMappings: Map<string, string>;
19
+ /**
20
+ * "row:col" -> "replacement string"
21
+ */
22
+ extraMappings: Map<string, string>;
21
23
  }
22
24
 
23
25
  function gtsToTypings(
24
26
  ast: AST.Program,
25
- option: TypingTranspileOption
27
+ option: TypingTranspileOption,
26
28
  ): TranspileResult {
27
29
  const state: TypingTranspileState = {
28
30
  ...(initialTranspileState(option) as Pick<
@@ -32,34 +34,20 @@ function gtsToTypings(
32
34
  leafTokens: option.leafTokens,
33
35
  idCounter: 0,
34
36
  typingPendingStatements: [],
35
- prefaceInserted: false,
36
- rootVmId: { type: "Identifier", name: "__root_vm" },
37
+ rootVmId: { type: "Identifier", name: "__gts_root_vm" },
38
+ utilNsId: { type: "Identifier", name: "__gts_util" },
37
39
  replacementTag: { type: "Identifier", name: "__gts_replacement_tag" },
38
- symbolsId: {
39
- Meta: { type: "Identifier", name: "__gts_symbols_meta" },
40
- NamedDefinition: { type: "Identifier", name: "__gts_symbols_namedDef" },
41
- },
40
+ MetaLit: { type: "Literal", value: "~meta" },
41
+ NamedDefinitionLit: { type: "Literal", value: "~namedDefinition" },
42
42
  defineLeadingComments: [],
43
43
  vmDefTypeIdStack: [],
44
44
  metaTypeIdStack: [],
45
45
  finalMetaTypeIdStack: [],
46
46
  attrsOfCurrentVm: [],
47
- additionalMappings: option.additionalMappings,
47
+ extraMappings: option.extraMappings,
48
48
  };
49
49
  const newAst = walk(ast as AST.Node, state, gtsToTypingsWalker);
50
- const printer = tsPrinter({
51
- getLeadingComments: (node) => (node as AST.Node).leadingComments,
52
- getTrailingComments: (node) => (node as AST.Node).trailingComments,
53
- });
54
- const prevIdentifier = printer.Identifier!;
55
- printer.Identifier = function (node, context) {
56
- if (node.isDummy) {
57
- context.write("", node);
58
- } else {
59
- prevIdentifier(node, context);
60
- }
61
- };
62
- const { code, map } = print(newAst, printer, {
50
+ const { code, map } = print(newAst, patchedPrinter, {
63
51
  indent: " ",
64
52
  });
65
53
  return {
@@ -71,21 +59,21 @@ function gtsToTypings(
71
59
  export function transformForVolar(
72
60
  ast: Program,
73
61
  option: TranspileOption,
74
- sourceInfo: Required<SourceInfo>
62
+ sourceInfo: Required<SourceInfo>,
75
63
  ): VolarMappingResult {
76
64
  const tokens = collectLeafTokens(ast);
77
- const additionalMappings = new Map<string, string>();
65
+ const extraMappings = new Map<string, string>();
78
66
  const { code, sourceMap } = gtsToTypings(ast, {
79
67
  ...option,
80
68
  leafTokens: tokens,
81
- additionalMappings,
69
+ extraMappings,
82
70
  });
83
71
  const volarMappings = convertToVolarMappings(
84
72
  code,
85
73
  sourceInfo.content,
86
74
  sourceMap,
87
75
  tokens,
88
- additionalMappings
76
+ extraMappings,
89
77
  );
90
78
  return {
91
79
  code,
@@ -192,19 +192,12 @@ export function getGeneratedPosition(
192
192
  src_line: number,
193
193
  src_column: number,
194
194
  srcToGenMap: CodeToGeneratedMap,
195
- ): CodePosition | undefined {
195
+ ): CodePosition[] {
196
196
  const key = `${src_line}:${src_column}`;
197
197
  const positions = srcToGenMap.get(key);
198
198
 
199
- if (!positions || positions.length === 0) {
200
- // No mapping found in source map - this shouldn't happen since all tokens should have mappings
201
- // throw new Error(
202
- // `No source map entry for position "${src_line}:${src_column}"`
203
- // );
204
- }
205
-
206
199
  // If multiple generated positions map to same source, return the first
207
- return positions?.[0];
200
+ return positions || [];
208
201
  }
209
202
  // Helper to create a line-to-offset lookup table
210
203
  function createLineOffsets(content: string): number[] {
@@ -245,7 +238,7 @@ export function convertToVolarMappings(
245
238
  source: string,
246
239
  sourceMap: SourceMap,
247
240
  tokens: LeafToken[],
248
- additionalMappings: Map<string, string>,
241
+ extraMappings: Map<string, string>,
249
242
  ): CodeMapping[] {
250
243
  const sourceLineOffsets = createLineOffsets(source);
251
244
  const generatedLineOffsets = createLineOffsets(generated);
@@ -263,13 +256,18 @@ export function convertToVolarMappings(
263
256
  token.loc.start.column,
264
257
  sourceLineOffsets,
265
258
  );
259
+ sourceStart += token.sourceStartOffset ?? 0;
260
+
266
261
  const sourceEnd = locToOffset(
267
262
  token.loc.end.line,
268
263
  token.loc.end.column,
269
264
  sourceLineOffsets,
270
265
  );
266
+
271
267
  let sourceLength = token.sourceLength ?? sourceEnd - sourceStart;
272
- const genLineCol = getGeneratedPosition(
268
+
269
+ sourceLength += token.sourceLengthOffset ?? 0;
270
+ const [genLineCol] = getGeneratedPosition(
273
271
  token.loc.start.line,
274
272
  token.loc.start.column,
275
273
  srcToGenMap,
@@ -283,19 +281,6 @@ export function convertToVolarMappings(
283
281
  genLineCol.column,
284
282
  generatedLineOffsets,
285
283
  );
286
- if (token.locationAdjustment) {
287
- // maps verification back to the start of source
288
- mappings.push({
289
- sourceOffsets: [sourceStart],
290
- generatedOffsets: [genStart],
291
- lengths: [0],
292
- generatedLengths: [token.locationAdjustment.generatedLength],
293
- data: {
294
- verification: true,
295
- },
296
- });
297
- genStart += token.locationAdjustment.startOffset;
298
- }
299
284
  if (token.isDummy) {
300
285
  // A dummy token might be generated for a missing property / argument.
301
286
  // Notice that when facing this scenario, the parser tries to 'defer' and step through
@@ -308,9 +293,25 @@ export function convertToVolarMappings(
308
293
  sourceLength++;
309
294
  }
310
295
  }
311
-
312
296
  const generatedLength = token.generatedLength ?? sourceLength;
313
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
+
314
315
  mappings.push({
315
316
  sourceOffsets: [sourceStart],
316
317
  generatedOffsets: [genStart],
@@ -320,7 +321,25 @@ export function convertToVolarMappings(
320
321
  });
321
322
  }
322
323
 
323
- for (const [loc, codeSnippet] of additionalMappings) {
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) {
324
343
  const generatedStart = generated.indexOf(codeSnippet);
325
344
  if (generatedStart === -1) {
326
345
  continue;
@@ -0,0 +1,123 @@
1
+ import tsPrinter from "esrap/languages/ts";
2
+ import type { AST } from "../../types";
3
+ import type { Context, Visitors } from "esrap";
4
+ import type { Node, SimpleCallExpression } from "estree";
5
+
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;
81
+ }
82
+ if (prop === "visit") {
83
+ return patchedVisit;
84
+ }
85
+ if (prop === "append") {
86
+ return patchedAppend;
87
+ }
88
+ }
89
+ const value = Reflect.get(target, prop);
90
+ if (typeof value === "function") {
91
+ return value.bind(target);
92
+ }
93
+ return value;
94
+ },
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;
@@ -2,6 +2,9 @@ import type { ExpressionStatement } from "estree";
2
2
  import type { TypingTranspileState } from "./walker";
3
3
 
4
4
  type ReplacementPayload =
5
+ | {
6
+ type: "preface";
7
+ }
5
8
  | {
6
9
  type: "enterVMFromRoot";
7
10
  vm: string;
@@ -27,6 +30,7 @@ type ReplacementPayload =
27
30
  defType: string;
28
31
  metaType: string;
29
32
  lhs: string;
33
+ attrName: string;
30
34
  }
31
35
  | {
32
36
  type: "createBindingTyping";
@@ -76,32 +80,106 @@ export function applyReplacements(
76
80
  "\\b" + state.replacementTag.name + "`(.*?)`",
77
81
  "gm",
78
82
  );
79
- const {
80
- symbolsId: { NamedDefinition, Meta },
81
- } = state;
83
+ const { NamedDefinitionLit, MetaLit } = state;
84
+ const NamedDefinition = JSON.stringify(NamedDefinitionLit.value);
85
+ const Meta = JSON.stringify(MetaLit.value);
86
+ const oneLine = (
87
+ strings: TemplateStringsArray,
88
+ ...values: Array<string | number | boolean>
89
+ ): string =>
90
+ strings
91
+ .reduce(
92
+ (result, chunk, index) =>
93
+ result + chunk + (index < values.length ? values[index] : ""),
94
+ "",
95
+ )
96
+ .replace(/\n[ \t]*/g, " ")
97
+ .trim();
82
98
  // All replacement should be written in one line to avoid messing up source map
83
99
  return code.replace(replacementRegex, (_, rawPayload) => {
84
100
  const payload: ReplacementPayload = JSON.parse(rawPayload);
85
- if (payload.type === "enterVMFromRoot") {
86
- return `type ${payload.defType} = (typeof ${payload.vm})[${NamedDefinition.name}]; type ${payload.metaType} = ${payload.defType}[${Meta.name}];`;
101
+ if (payload.type === "preface") {
102
+ return oneLine`
103
+ namespace ${state.utilNsId.name} {
104
+ export type UniqueKeyProbSegment = "__gts_unique_prob_seg__";
105
+ export type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;
106
+ }
107
+ `;
108
+ } else if (payload.type === "enterVMFromRoot") {
109
+ return oneLine`
110
+ type ${payload.defType} = (typeof ${payload.vm})[${NamedDefinition}];
111
+ type ${payload.metaType} = ${payload.defType}[${Meta}];
112
+ `;
87
113
  } else if (payload.type === "enterVMFromAttr") {
88
- return `type ${payload.defType} = ${payload.returnType} extends { namedDefinition: infer Def } ? Def : { [${Meta.name}]: unknown }; type ${payload.metaType} = ${payload.defType}[${Meta.name}];`;
114
+ return oneLine`
115
+ type ${payload.defType} = ${payload.returnType} extends { namedDefinition: infer Def } ? Def : { ${Meta}: unknown };
116
+ type ${payload.metaType} = ${payload.defType}[${Meta}];
117
+ `;
89
118
  } else if (payload.type === "exitVM") {
90
119
  const lhs = `${payload.finalMetaType}_lhs`;
91
120
  const requiredAttrsNs = `${payload.finalMetaType}_rans`;
92
121
  const collectedAttrsExpr = `${payload.collectedAttrs.join(" | ")}`;
93
122
  const needleString = `"${requiredAttrsNs}_NeedleString" as any as "required attributes are missing"`;
94
123
  if (payload.errorLoc) {
95
- state.additionalMappings.set(payload.errorLoc, needleString);
124
+ state.extraMappings.set(payload.errorLoc, needleString);
96
125
  }
97
- return `type ${payload.finalMetaType} = ${payload.metaType}; const ${lhs}: { [${Meta.name}]: ${payload.metaType} } & Omit<${payload.defType}, ${Meta.name}> = 0 as any; type ${lhs} = typeof ${lhs}; namespace ${requiredAttrsNs} { export type Collected = ${collectedAttrsExpr}; export type Expected = { [K in keyof ${payload.defType}]: ${lhs}[K] extends { required(this: ${lhs}): true } ? K : never }[keyof ${payload.defType}]; }; ((_: ${requiredAttrsNs}.Expected extends ${requiredAttrsNs}.Collected ? string : ${requiredAttrsNs}.Expected) => 0)(${needleString});`;
126
+ return oneLine`
127
+ type ${payload.finalMetaType} = ${payload.metaType};
128
+ let ${lhs}!: { ${Meta}: ${payload.metaType} } & Omit<${payload.defType}, ${Meta}>;
129
+ type ${lhs} = typeof ${lhs};
130
+ namespace ${requiredAttrsNs} {
131
+ export type Collected = ${collectedAttrsExpr};
132
+ export type Expected = { [K in keyof ${payload.defType}]: ${lhs}[K] extends { required(this: ${lhs}): true } ? K : never }[keyof ${payload.defType}];
133
+ };
134
+ ((_: ${requiredAttrsNs}.Expected extends ${requiredAttrsNs}.Collected ? string : ${requiredAttrsNs}.Expected) => 0)(${needleString});
135
+ `;
98
136
  } else if (payload.type === "enterAttr") {
99
- return `const ${payload.lhs}: { [${Meta.name}]: ${payload.metaType} } & Omit<${payload.defType}, ${Meta.name}> = 0 as any;`;
137
+ const uniqueKeyLhs = `${payload.lhs}_uniqueKey_lhs`;
138
+ const uniqueKey = `${payload.lhs}_uniqueKey`;
139
+ const uniqueKeyForThis = `${payload.lhs}_uniqueKeyFor_${payload.lhs}`;
140
+ const uniqueKeyHelperIntf = `${payload.defType}_uniqueKeyProbeHelper`;
141
+ const omittedKeys = `${payload.lhs}_omittedKeys`;
142
+ return oneLine`
143
+ type ${uniqueKeyLhs} = {
144
+ ${Meta}: ${payload.metaType};
145
+ uniqueKey: ${payload.defType} extends { [${payload.attrName}]: { uniqueKey: infer UniqueKey } } ? UniqueKey : () => 0;
146
+
147
+ let ${uniqueKeyLhs}!: ${uniqueKeyLhs};
148
+ let ${uniqueKey} = ${uniqueKeyLhs}.uniqueKey();
149
+ type ${uniqueKey} = typeof ${uniqueKey};
150
+ let ${uniqueKeyForThis}!: \`\${${uniqueKey}}\${${state.utilNsId.name}.UniqueKeyProbSegment}${payload.lhs}\`;
151
+ interface ${uniqueKeyHelperIntf} {
152
+ [${uniqueKeyForThis}]: 1;
153
+ }
154
+ type ${omittedKeys} = ${Meta} | (
155
+ ${uniqueKey} extends 0
156
+ ? never /* no unique requirement */
157
+ : string extends keyof ${uniqueKeyHelperIntf}
158
+ ? keyof ${payload.defType} /* too loose, disable all */
159
+ : ${state.utilNsId.name}.UnionToIntersection<
160
+ keyof ${uniqueKeyHelperIntf} & \`\${${uniqueKey}}\${${state.utilNsId.name}.UniqueKeyProbSegment}\${string}\`
161
+ > extends never
162
+ ? ${payload.attrName} /* have duplicate, disable this */
163
+ : never
164
+ );
165
+ let ${payload.lhs}!: { ${Meta}: ${payload.metaType} } & Omit<${payload.defType}, ${omittedKeys}>;
166
+ `;
100
167
  } else if (payload.type === "createBindingTyping") {
101
168
  const typingIdLhs = `${payload.typingId}_lhs`;
102
- return `type ${typingIdLhs} = { [${Meta.name}]: ${payload.finalMetaType}; as: ${payload.defType}[${payload.attrName}] extends { as: infer As } ? As : unknown }; let ${typingIdLhs}!: ${typingIdLhs}; let ${payload.typingId} = ${typingIdLhs}.as(); type ${payload.typingId} = typeof ${payload.typingId};`;
169
+ return oneLine`
170
+ type ${typingIdLhs} = {
171
+ ${Meta}: ${payload.finalMetaType};
172
+ as: ${payload.defType} extends { [${payload.attrName}]: { as: infer As } } ? As : unknown;
173
+ };
174
+ let ${typingIdLhs}!: ${typingIdLhs};
175
+ let ${payload.typingId} = ${typingIdLhs}.as();
176
+ type ${payload.typingId} = typeof ${payload.typingId};
177
+ `;
103
178
  } else if (payload.type === "exitAttr") {
104
- return `type ${payload.returnType} = typeof ${payload.returnType}; type ${payload.newMetaType} = ${payload.returnType} extends { rewriteMeta: infer NewMeta extends {} } ? NewMeta : ${payload.oldMetaType}`;
179
+ return oneLine`
180
+ type ${payload.returnType} = typeof ${payload.returnType};
181
+ type ${payload.newMetaType} = ${payload.returnType} extends { rewriteMeta: infer NewMeta extends {} } ? NewMeta : ${payload.oldMetaType}
182
+ `;
105
183
  } else {
106
184
  return "";
107
185
  }