@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.
@@ -1,123 +1,145 @@
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. [[1]]
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
+ // Same as [[1]]
82
+ ImportDefaultSpecifier(node, context) {
83
+ const generatedStart = context.generatedOffset;
84
+ defaultPrinters.ImportDefaultSpecifier(node, context);
85
+ if (state.diagnosticsOnTopNodes.has(node)) {
86
+ const generatedEnd = context.generatedOffset;
87
+ context.createExtraMapping(
88
+ { start: 0, end: 1 },
89
+ generatedStart,
90
+ generatedEnd,
91
+ VERIFICATION_ONLY_MAPPING_DATA,
92
+ );
93
+ }
94
+ },
95
+ // Same as [[1]]
96
+ ImportSpecifier(node, context) {
97
+ const generatedStart = context.generatedOffset;
98
+ defaultPrinters.ImportSpecifier(node, context);
99
+ if (state.diagnosticsOnTopNodes.has(node)) {
100
+ const generatedEnd = context.generatedOffset;
101
+ context.createExtraMapping(
102
+ { start: 0, end: 1 },
103
+ generatedStart,
104
+ generatedEnd,
105
+ VERIFICATION_ONLY_MAPPING_DATA,
106
+ );
107
+ }
108
+ },
109
+ // If the last import declaration is a generated one, because of we ensured that
110
+ // the generated import declarations are always unsorted, so TSServer will set the
111
+ // auto-insertion point next to this last import declaration.
112
+ // Add a mapping to that position (a written newline character) to the top-of-file,
113
+ // after skipping hashbang and leading comments.
114
+ // NOTE: since the insertion derived from here won't add additional newline *before*
115
+ // the inserted text, so here is a difference behavior from standard TSServer and our
116
+ // language server. We'd try our best.
117
+ ImportDeclaration(node, context) {
118
+ defaultPrinters.ImportDeclaration(node, context);
119
+ if (state.lastImportDeclarationIfGen === node) {
120
+ context.write("\n");
121
+ context.createExtraMapping(
122
+ {
123
+ start: state.contentStartOffset,
124
+ end: state.contentStartOffset + 1,
125
+ },
126
+ context.generatedOffset,
127
+ context.generatedOffset + 1,
128
+ DEFAULT_VOLAR_MAPPING_DATA,
129
+ );
130
+ }
131
+ },
132
+ },
133
+ // Enable triggering signature completion
134
+ experimentalGetLeftParenSourceRange: (node) => {
135
+ const callLike = node as SimpleCallExpression | NewExpression;
136
+ if (callLike.lParenRange) {
137
+ return {
138
+ start: callLike.lParenRange[0],
139
+ end: callLike.lParenRange[1],
140
+ };
88
141
  }
89
- const value = Reflect.get(target, prop);
90
- if (typeof value === "function") {
91
- return value.bind(target);
92
- }
93
- return value;
142
+ return state.namedAttributeCalleeLParenRange.get(callLike.callee);
94
143
  },
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;
144
+ };
145
+ }
@@ -1,5 +1,12 @@
1
1
  import type { ExpressionStatement } from "estree";
2
2
  import type { TypingTranspileState } from "./walker.ts";
3
+ import dedent from "dedent";
4
+ import type { CodeMapping } from "@volar/language-core";
5
+
6
+ interface MatchInfo {
7
+ sourceEnd: number;
8
+ lengthOffset: number;
9
+ }
3
10
 
4
11
  type ReplacementPayload =
5
12
  | {
@@ -23,7 +30,7 @@ type ReplacementPayload =
23
30
  defType: string;
24
31
  collectedAttrs: string[];
25
32
  finalMetaType: string;
26
- errorLoc?: string;
33
+ errorRange?: [number, number];
27
34
  }
28
35
  | {
29
36
  type: "enterAttr";
@@ -75,55 +82,54 @@ export const createReplacementHolder = (
75
82
  export function applyReplacements(
76
83
  state: TypingTranspileState,
77
84
  code: string,
85
+ mappings: CodeMapping[],
78
86
  ): string {
79
87
  const replacementRegex = new RegExp(
80
- "\\b" + state.replacementTag.name + "`(.*?)`",
88
+ "\\b" + state.replacementTag.name + "`(.*?)(?<!\\\\)`",
81
89
  "gm",
82
90
  );
83
91
  const { NamedDefinitionLit, MetaLit } = state;
84
92
  const NamedDefinition = JSON.stringify(NamedDefinitionLit.value);
85
93
  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();
98
- // All replacement should be written in one line to avoid messing up source map
99
- return code.replace(replacementRegex, (_, rawPayload) => {
100
- const payload: ReplacementPayload = JSON.parse(rawPayload);
101
- if (payload.type === "preface") {
102
- return oneLine`
94
+ const matchInfos: MatchInfo[] = [];
95
+
96
+ const result = code.replace(
97
+ replacementRegex,
98
+ (match, rawPayload: string, offset: number) => {
99
+ const payload: ReplacementPayload = JSON.parse(
100
+ rawPayload.replace(/\\`/g, "`"),
101
+ );
102
+ let replacement: string;
103
+ if (payload.type === "preface") {
104
+ replacement = dedent`
103
105
  namespace ${state.utilNsId.name} {
104
106
  export type UniqueKeyProbSegment = "__gts_unique_prob_seg__";
105
107
  export type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;
106
108
  }
107
109
  `;
108
- } else if (payload.type === "enterVMFromRoot") {
109
- return oneLine`
110
+ } else if (payload.type === "enterVMFromRoot") {
111
+ replacement = dedent`
110
112
  type ${payload.defType} = (typeof ${payload.vm})[${NamedDefinition}];
111
113
  type ${payload.metaType} = ${payload.defType}[${Meta}];
112
114
  `;
113
- } else if (payload.type === "enterVMFromAttr") {
114
- return oneLine`
115
+ } else if (payload.type === "enterVMFromAttr") {
116
+ replacement = dedent`
115
117
  type ${payload.defType} = ${payload.returnType} extends { namedDefinition: infer Def } ? Def : { ${Meta}: unknown };
116
118
  type ${payload.metaType} = ${payload.defType}[${Meta}];
117
119
  `;
118
- } else if (payload.type === "exitVM") {
119
- const lhs = `${payload.finalMetaType}_lhs`;
120
- const requiredAttrsNs = `${payload.finalMetaType}_rans`;
121
- const collectedAttrsExpr = `${payload.collectedAttrs.join(" | ")}`;
122
- const needleString = `"${requiredAttrsNs}_NeedleString" as any as "required attributes are missing"`;
123
- if (payload.errorLoc) {
124
- state.extraMappings.set(payload.errorLoc, needleString);
125
- }
126
- return oneLine`
120
+ } else if (payload.type === "exitVM") {
121
+ const lhs = `${payload.finalMetaType}_lhs`;
122
+ const requiredAttrsNs = `${payload.finalMetaType}_rans`;
123
+ const collectedAttrsExpr = `${payload.collectedAttrs.join(" | ")}`;
124
+ const needleString = `"${requiredAttrsNs}_NeedleString" as any as "required attributes are missing"`;
125
+ if (payload.errorRange) {
126
+ state.extraMappings.push({
127
+ sourceOffset: payload.errorRange[0],
128
+ length: payload.errorRange[1] - payload.errorRange[0],
129
+ generatedNeedle: needleString,
130
+ });
131
+ }
132
+ replacement = dedent`
127
133
  type ${payload.finalMetaType} = ${payload.metaType};
128
134
  let ${lhs}!: { ${Meta}: ${payload.metaType} } & Omit<${payload.defType}, ${Meta}>;
129
135
  type ${lhs} = typeof ${lhs};
@@ -133,16 +139,17 @@ export function applyReplacements(
133
139
  };
134
140
  ((_: ${requiredAttrsNs}.Expected extends ${requiredAttrsNs}.Collected ? string : ${requiredAttrsNs}.Expected) => 0)(${needleString});
135
141
  `;
136
- } else if (payload.type === "enterAttr") {
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`
142
+ } else if (payload.type === "enterAttr") {
143
+ const uniqueKeyLhs = `${payload.lhs}_uniqueKey_lhs`;
144
+ const uniqueKey = `${payload.lhs}_uniqueKey`;
145
+ const uniqueKeyForThis = `${payload.lhs}_uniqueKeyFor_${payload.lhs}`;
146
+ const uniqueKeyHelperIntf = `${payload.defType}_uniqueKeyProbeHelper`;
147
+ const omittedKeys = `${payload.lhs}_omittedKeys`;
148
+ replacement = dedent`
143
149
  type ${uniqueKeyLhs} = {
144
150
  ${Meta}: ${payload.metaType};
145
151
  uniqueKey: ${payload.defType} extends { [${payload.attrName}]: { uniqueKey: infer UniqueKey } } ? UniqueKey : () => 0;
152
+ };
146
153
 
147
154
  let ${uniqueKeyLhs}!: ${uniqueKeyLhs};
148
155
  let ${uniqueKey} = ${uniqueKeyLhs}.uniqueKey();
@@ -164,9 +171,9 @@ export function applyReplacements(
164
171
  );
165
172
  let ${payload.lhs}!: { ${Meta}: ${payload.metaType} } & Omit<${payload.defType}, ${omittedKeys}>;
166
173
  `;
167
- } else if (payload.type === "createBindingTyping") {
168
- const typingIdLhs = `${payload.typingId}_lhs`;
169
- return oneLine`
174
+ } else if (payload.type === "createBindingTyping") {
175
+ const typingIdLhs = `${payload.typingId}_lhs`;
176
+ replacement = dedent`
170
177
  type ${typingIdLhs} = {
171
178
  ${Meta}: ${payload.finalMetaType};
172
179
  as: ${payload.defType} extends { [${payload.attrName}]: { as: infer As } } ? As : unknown;
@@ -175,13 +182,34 @@ export function applyReplacements(
175
182
  let ${payload.typingId} = ${typingIdLhs}.as();
176
183
  type ${payload.typingId} = typeof ${payload.typingId};
177
184
  `;
178
- } else if (payload.type === "exitAttr") {
179
- return oneLine`
185
+ } else if (payload.type === "exitAttr") {
186
+ replacement = dedent`
180
187
  type ${payload.returnType} = typeof ${payload.returnType};
181
188
  type ${payload.newMetaType} = ${payload.returnType} extends { rewriteMeta: infer NewMeta extends {} } ? NewMeta : ${payload.oldMetaType}
182
189
  `;
183
- } else {
184
- return "";
190
+ } else {
191
+ replacement = "";
192
+ }
193
+ matchInfos.push({
194
+ sourceEnd: offset + match.length,
195
+ lengthOffset: replacement.length - match.length,
196
+ });
197
+ return replacement;
198
+ },
199
+ );
200
+
201
+ for (const mapping of mappings) {
202
+ for (let i = 0; i < mapping.generatedOffsets.length; i++) {
203
+ const orig = mapping.generatedOffsets[i];
204
+ let shift = 0;
205
+ for (const info of matchInfos) {
206
+ if (orig >= info.sourceEnd) {
207
+ shift += info.lengthOffset;
208
+ }
209
+ }
210
+ mapping.generatedOffsets[i] = orig + shift;
185
211
  }
186
- });
212
+ }
213
+
214
+ return result;
187
215
  }