@gi-tcg/gts-transpiler 0.1.1 → 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.
- package/dist/index.d.ts +45 -0
- package/dist/index.js +708 -441
- package/package.json +9 -10
- package/src/config.ts +221 -0
- package/src/index.ts +12 -3
- package/src/keywords.ts +1 -1
- package/src/parse/comment.d.ts +10 -0
- package/src/parse/gts_plugin.ts +18 -11
- package/src/parse/index.ts +16 -6
- package/src/parse/loose_plugin.ts +2 -2
- package/src/parse/record_call_lparen_plugin.ts +85 -0
- package/src/transform/constants.ts +11 -11
- package/src/transform/gts.ts +198 -209
- package/src/transform/volar/collect_tokens.ts +44 -18
- package/src/transform/volar/index.ts +18 -29
- package/src/transform/volar/mappings.ts +57 -38
- package/src/transform/volar/printer.ts +123 -0
- package/src/transform/volar/replacements.ts +96 -15
- package/src/transform/volar/walker.ts +92 -204
- package/src/types.ts +16 -4
- package/src/transform/NOTES.md +0 -112
|
@@ -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
|
-
|
|
20
|
-
|
|
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<
|
|
@@ -31,34 +33,21 @@ function gtsToTypings(
|
|
|
31
33
|
>),
|
|
32
34
|
leafTokens: option.leafTokens,
|
|
33
35
|
idCounter: 0,
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
36
|
+
typingPendingStatements: [],
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
},
|
|
40
|
+
MetaLit: { type: "Literal", value: "~meta" },
|
|
41
|
+
NamedDefinitionLit: { type: "Literal", value: "~namedDefinition" },
|
|
42
|
+
defineLeadingComments: [],
|
|
42
43
|
vmDefTypeIdStack: [],
|
|
43
44
|
metaTypeIdStack: [],
|
|
44
45
|
finalMetaTypeIdStack: [],
|
|
45
46
|
attrsOfCurrentVm: [],
|
|
46
|
-
|
|
47
|
+
extraMappings: option.extraMappings,
|
|
47
48
|
};
|
|
48
49
|
const newAst = walk(ast as AST.Node, state, gtsToTypingsWalker);
|
|
49
|
-
const
|
|
50
|
-
getLeadingComments: (node) => (node as AST.Node).leadingComments,
|
|
51
|
-
getTrailingComments: (node) => (node as AST.Node).trailingComments,
|
|
52
|
-
});
|
|
53
|
-
const prevIdentifier = printer.Identifier!;
|
|
54
|
-
printer.Identifier = function (node, context) {
|
|
55
|
-
if (node.isDummy) {
|
|
56
|
-
context.write("", node);
|
|
57
|
-
} else {
|
|
58
|
-
prevIdentifier(node, context);
|
|
59
|
-
}
|
|
60
|
-
};
|
|
61
|
-
const { code, map } = print(newAst, printer, {
|
|
50
|
+
const { code, map } = print(newAst, patchedPrinter, {
|
|
62
51
|
indent: " ",
|
|
63
52
|
});
|
|
64
53
|
return {
|
|
@@ -70,21 +59,21 @@ function gtsToTypings(
|
|
|
70
59
|
export function transformForVolar(
|
|
71
60
|
ast: Program,
|
|
72
61
|
option: TranspileOption,
|
|
73
|
-
sourceInfo: Required<SourceInfo
|
|
62
|
+
sourceInfo: Required<SourceInfo>,
|
|
74
63
|
): VolarMappingResult {
|
|
75
64
|
const tokens = collectLeafTokens(ast);
|
|
76
|
-
const
|
|
65
|
+
const extraMappings = new Map<string, string>();
|
|
77
66
|
const { code, sourceMap } = gtsToTypings(ast, {
|
|
78
67
|
...option,
|
|
79
68
|
leafTokens: tokens,
|
|
80
|
-
|
|
69
|
+
extraMappings,
|
|
81
70
|
});
|
|
82
71
|
const volarMappings = convertToVolarMappings(
|
|
83
72
|
code,
|
|
84
73
|
sourceInfo.content,
|
|
85
74
|
sourceMap,
|
|
86
75
|
tokens,
|
|
87
|
-
|
|
76
|
+
extraMappings,
|
|
88
77
|
);
|
|
89
78
|
return {
|
|
90
79
|
code,
|
|
@@ -39,8 +39,8 @@ type CodeToGeneratedMap = Map<string, CodePosition[]>;
|
|
|
39
39
|
* @returns */
|
|
40
40
|
export const offset_to_line_col = (
|
|
41
41
|
offset: number,
|
|
42
|
-
line_offsets: LineOffsets
|
|
43
|
-
) => {
|
|
42
|
+
line_offsets: LineOffsets,
|
|
43
|
+
): { line: number; column: number } => {
|
|
44
44
|
// Binary search
|
|
45
45
|
let left = 0;
|
|
46
46
|
let right = line_offsets.length - 1;
|
|
@@ -71,13 +71,13 @@ export const offset_to_line_col = (
|
|
|
71
71
|
* @param source_map - The source map object from esrap (v3 format)
|
|
72
72
|
* @param line_offsets - Pre-computed line offsets array
|
|
73
73
|
* @param generated_code - The final generated code (after post-processing)
|
|
74
|
-
* @returns
|
|
74
|
+
* @returns source-to-generated map
|
|
75
75
|
*/
|
|
76
76
|
export function buildSrcToGenMap(
|
|
77
77
|
source_map: SourceMap,
|
|
78
78
|
line_offsets: LineOffsets,
|
|
79
|
-
generated_code: string
|
|
80
|
-
) {
|
|
79
|
+
generated_code: string,
|
|
80
|
+
): CodeToGeneratedMap {
|
|
81
81
|
const map: CodeToGeneratedMap = new Map();
|
|
82
82
|
|
|
83
83
|
// Decode the VLQ-encoded mappings string
|
|
@@ -191,20 +191,13 @@ export function buildSrcToGenMap(
|
|
|
191
191
|
export function getGeneratedPosition(
|
|
192
192
|
src_line: number,
|
|
193
193
|
src_column: number,
|
|
194
|
-
srcToGenMap: CodeToGeneratedMap
|
|
195
|
-
) {
|
|
194
|
+
srcToGenMap: CodeToGeneratedMap,
|
|
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
|
|
200
|
+
return positions || [];
|
|
208
201
|
}
|
|
209
202
|
// Helper to create a line-to-offset lookup table
|
|
210
203
|
function createLineOffsets(content: string): number[] {
|
|
@@ -230,7 +223,7 @@ function createLineOffsets(content: string): number[] {
|
|
|
230
223
|
function locToOffset(
|
|
231
224
|
line: number,
|
|
232
225
|
column: number,
|
|
233
|
-
line_offsets: number[]
|
|
226
|
+
line_offsets: number[],
|
|
234
227
|
): number {
|
|
235
228
|
if (line < 1 || line > line_offsets.length) {
|
|
236
229
|
// throw new Error(
|
|
@@ -245,14 +238,14 @@ export function convertToVolarMappings(
|
|
|
245
238
|
source: string,
|
|
246
239
|
sourceMap: SourceMap,
|
|
247
240
|
tokens: LeafToken[],
|
|
248
|
-
|
|
241
|
+
extraMappings: Map<string, string>,
|
|
249
242
|
): CodeMapping[] {
|
|
250
243
|
const sourceLineOffsets = createLineOffsets(source);
|
|
251
244
|
const generatedLineOffsets = createLineOffsets(generated);
|
|
252
245
|
const srcToGenMap = buildSrcToGenMap(
|
|
253
246
|
sourceMap,
|
|
254
247
|
generatedLineOffsets,
|
|
255
|
-
generated
|
|
248
|
+
generated,
|
|
256
249
|
);
|
|
257
250
|
|
|
258
251
|
const mappings: CodeMapping[] = [];
|
|
@@ -261,18 +254,23 @@ export function convertToVolarMappings(
|
|
|
261
254
|
let sourceStart = locToOffset(
|
|
262
255
|
token.loc.start.line,
|
|
263
256
|
token.loc.start.column,
|
|
264
|
-
sourceLineOffsets
|
|
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
|
-
sourceLineOffsets
|
|
264
|
+
sourceLineOffsets,
|
|
270
265
|
);
|
|
266
|
+
|
|
271
267
|
let sourceLength = token.sourceLength ?? sourceEnd - sourceStart;
|
|
272
|
-
|
|
268
|
+
|
|
269
|
+
sourceLength += token.sourceLengthOffset ?? 0;
|
|
270
|
+
const [genLineCol] = getGeneratedPosition(
|
|
273
271
|
token.loc.start.line,
|
|
274
272
|
token.loc.start.column,
|
|
275
|
-
srcToGenMap
|
|
273
|
+
srcToGenMap,
|
|
276
274
|
);
|
|
277
275
|
if (!genLineCol) {
|
|
278
276
|
// No mapping found for this token - skip it
|
|
@@ -281,21 +279,8 @@ export function convertToVolarMappings(
|
|
|
281
279
|
let genStart = locToOffset(
|
|
282
280
|
genLineCol.line,
|
|
283
281
|
genLineCol.column,
|
|
284
|
-
generatedLineOffsets
|
|
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
|
-
|
|
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";
|
|
@@ -45,7 +49,7 @@ type ReplacementPayload =
|
|
|
45
49
|
|
|
46
50
|
export const createReplacementHolder = (
|
|
47
51
|
state: TypingTranspileState,
|
|
48
|
-
value: ReplacementPayload
|
|
52
|
+
value: ReplacementPayload,
|
|
49
53
|
): ExpressionStatement => {
|
|
50
54
|
const rawValue = JSON.stringify(value);
|
|
51
55
|
return {
|
|
@@ -68,37 +72,114 @@ export const createReplacementHolder = (
|
|
|
68
72
|
};
|
|
69
73
|
};
|
|
70
74
|
|
|
71
|
-
export function applyReplacements(
|
|
75
|
+
export function applyReplacements(
|
|
76
|
+
state: TypingTranspileState,
|
|
77
|
+
code: string,
|
|
78
|
+
): string {
|
|
72
79
|
const replacementRegex = new RegExp(
|
|
73
80
|
"\\b" + state.replacementTag.name + "`(.*?)`",
|
|
74
|
-
"gm"
|
|
81
|
+
"gm",
|
|
75
82
|
);
|
|
76
|
-
const {
|
|
77
|
-
|
|
78
|
-
|
|
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();
|
|
79
98
|
// All replacement should be written in one line to avoid messing up source map
|
|
80
99
|
return code.replace(replacementRegex, (_, rawPayload) => {
|
|
81
100
|
const payload: ReplacementPayload = JSON.parse(rawPayload);
|
|
82
|
-
if (payload.type === "
|
|
83
|
-
return `
|
|
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
|
+
`;
|
|
84
113
|
} else if (payload.type === "enterVMFromAttr") {
|
|
85
|
-
return `
|
|
114
|
+
return oneLine`
|
|
115
|
+
type ${payload.defType} = ${payload.returnType} extends { namedDefinition: infer Def } ? Def : { ${Meta}: unknown };
|
|
116
|
+
type ${payload.metaType} = ${payload.defType}[${Meta}];
|
|
117
|
+
`;
|
|
86
118
|
} else if (payload.type === "exitVM") {
|
|
87
119
|
const lhs = `${payload.finalMetaType}_lhs`;
|
|
88
120
|
const requiredAttrsNs = `${payload.finalMetaType}_rans`;
|
|
89
121
|
const collectedAttrsExpr = `${payload.collectedAttrs.join(" | ")}`;
|
|
90
|
-
const needleString = `
|
|
122
|
+
const needleString = `"${requiredAttrsNs}_NeedleString" as any as "required attributes are missing"`;
|
|
91
123
|
if (payload.errorLoc) {
|
|
92
|
-
state.
|
|
124
|
+
state.extraMappings.set(payload.errorLoc, needleString);
|
|
93
125
|
}
|
|
94
|
-
return `
|
|
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
|
+
`;
|
|
95
136
|
} else if (payload.type === "enterAttr") {
|
|
96
|
-
|
|
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
|
+
`;
|
|
97
167
|
} else if (payload.type === "createBindingTyping") {
|
|
98
168
|
const typingIdLhs = `${payload.typingId}_lhs`;
|
|
99
|
-
return `
|
|
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
|
+
`;
|
|
100
178
|
} else if (payload.type === "exitAttr") {
|
|
101
|
-
return `
|
|
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
|
+
`;
|
|
102
183
|
} else {
|
|
103
184
|
return "";
|
|
104
185
|
}
|