@backtickjs/compiler 0.0.0-stage → 0.1.1

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.
Files changed (107) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +25 -2
  3. package/dist/CodeInformation.d.ts +8 -0
  4. package/dist/CodeInformation.js +2 -0
  5. package/dist/CodeInformation.js.map +1 -0
  6. package/dist/SourceRange.d.ts +4 -0
  7. package/dist/SourceRange.js +2 -0
  8. package/dist/SourceRange.js.map +1 -0
  9. package/dist/buildMappings.d.ts +10 -0
  10. package/dist/buildMappings.js +21 -0
  11. package/dist/buildMappings.js.map +1 -0
  12. package/dist/compileModule.d.ts +25 -0
  13. package/dist/compileModule.js +78 -0
  14. package/dist/compileModule.js.map +1 -0
  15. package/dist/compileScript.d.ts +24 -0
  16. package/dist/compileScript.js +66 -0
  17. package/dist/compileScript.js.map +1 -0
  18. package/dist/diagnostics.d.ts +8 -0
  19. package/dist/diagnostics.js +2 -0
  20. package/dist/diagnostics.js.map +1 -0
  21. package/dist/emitScript.d.ts +52 -0
  22. package/dist/emitScript.js +201 -0
  23. package/dist/emitScript.js.map +1 -0
  24. package/dist/emitScripts.d.ts +10 -0
  25. package/dist/emitScripts.js +23 -0
  26. package/dist/emitScripts.js.map +1 -0
  27. package/dist/flattenScripts.d.ts +2 -0
  28. package/dist/flattenScripts.js +10 -0
  29. package/dist/flattenScripts.js.map +1 -0
  30. package/dist/hashText.d.ts +1 -0
  31. package/dist/hashText.js +18 -0
  32. package/dist/hashText.js.map +1 -0
  33. package/dist/hostElements.d.ts +3 -0
  34. package/dist/hostElements.js +28 -0
  35. package/dist/hostElements.js.map +1 -0
  36. package/dist/index.d.ts +16 -0
  37. package/dist/index.js +10 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/isComponentTag.d.ts +1 -0
  40. package/dist/isComponentTag.js +15 -0
  41. package/dist/isComponentTag.js.map +1 -0
  42. package/dist/nodeFactory.d.ts +3 -0
  43. package/dist/nodeFactory.js +5 -0
  44. package/dist/nodeFactory.js.map +1 -0
  45. package/dist/parseFile.d.ts +34 -0
  46. package/dist/parseFile.js +200 -0
  47. package/dist/parseFile.js.map +1 -0
  48. package/dist/printVirtualCode.d.ts +9 -0
  49. package/dist/printVirtualCode.js +52 -0
  50. package/dist/printVirtualCode.js.map +1 -0
  51. package/dist/resolveBindings.d.ts +76 -0
  52. package/dist/resolveBindings.js +240 -0
  53. package/dist/resolveBindings.js.map +1 -0
  54. package/dist/rewriteFile.d.ts +12 -0
  55. package/dist/rewriteFile.js +22 -0
  56. package/dist/rewriteFile.js.map +1 -0
  57. package/dist/rewriteScript.d.ts +27 -0
  58. package/dist/rewriteScript.js +268 -0
  59. package/dist/rewriteScript.js.map +1 -0
  60. package/dist/segmentsToString.d.ts +9 -0
  61. package/dist/segmentsToString.js +4 -0
  62. package/dist/segmentsToString.js.map +1 -0
  63. package/dist/tagRoot.d.ts +2 -0
  64. package/dist/tagRoot.js +10 -0
  65. package/dist/tagRoot.js.map +1 -0
  66. package/dist/transform.d.ts +17 -0
  67. package/dist/transform.js +61 -0
  68. package/dist/transform.js.map +1 -0
  69. package/dist/transpile.d.ts +27 -0
  70. package/dist/transpile.js +41 -0
  71. package/dist/transpile.js.map +1 -0
  72. package/dist/unmangle.d.ts +2 -0
  73. package/dist/unmangle.js +9 -0
  74. package/dist/unmangle.js.map +1 -0
  75. package/dist/virtualScript.d.ts +22 -0
  76. package/dist/virtualScript.js +305 -0
  77. package/dist/virtualScript.js.map +1 -0
  78. package/dist/virtualize.d.ts +8 -0
  79. package/dist/virtualize.js +10 -0
  80. package/dist/virtualize.js.map +1 -0
  81. package/package.json +58 -3
  82. package/src/CodeInformation.ts +14 -0
  83. package/src/SourceRange.ts +4 -0
  84. package/src/buildMappings.ts +31 -0
  85. package/src/compileModule.ts +132 -0
  86. package/src/compileScript.ts +97 -0
  87. package/src/diagnostics.ts +9 -0
  88. package/src/emitScript.ts +297 -0
  89. package/src/emitScripts.ts +39 -0
  90. package/src/flattenScripts.ts +13 -0
  91. package/src/hashText.ts +17 -0
  92. package/src/hostElements.ts +34 -0
  93. package/src/index.ts +20 -0
  94. package/src/isComponentTag.ts +15 -0
  95. package/src/nodeFactory.ts +18 -0
  96. package/src/parseFile.ts +331 -0
  97. package/src/printVirtualCode.ts +93 -0
  98. package/src/resolveBindings.ts +417 -0
  99. package/src/rewriteFile.ts +52 -0
  100. package/src/rewriteScript.ts +413 -0
  101. package/src/segmentsToString.ts +16 -0
  102. package/src/tagRoot.ts +14 -0
  103. package/src/transform.ts +96 -0
  104. package/src/transpile.ts +49 -0
  105. package/src/unmangle.ts +10 -0
  106. package/src/virtualScript.ts +355 -0
  107. package/src/virtualize.ts +29 -0
@@ -0,0 +1,355 @@
1
+ import type ts from "typescript";
2
+ import type { CodeInformation } from "./CodeInformation.js";
3
+ import type { ClientScript, Splice } from "./parseFile.js";
4
+ import type { BindingResolution } from "./resolveBindings.js";
5
+ import type { Segment } from "./segmentsToString.js";
6
+ import { tagRoot } from "./tagRoot.js";
7
+ import { mangle } from "./unmangle.js";
8
+
9
+ // What the editor reads through a wrapper the virtual code adds: nothing.
10
+ const WRAPPER: CodeInformation = { semantic: false, navigation: false };
11
+ // What it reads through a name the virtual code adds over a span of the
12
+ // script: only what is reported there. Hover and navigation stay with the
13
+ // script's own names inside it.
14
+ const REPORTED: CodeInformation = { semantic: false, navigation: false };
15
+ // A host tag's name, written as a value: everything but its color, which stays
16
+ // the tag's, as JSX colors it.
17
+ const TAG_NAME: CodeInformation = {
18
+ semantic: { shouldHighlight: () => false },
19
+ };
20
+
21
+ /**
22
+ * A script as the typechecker reads it: its own text, as written, wrapped as
23
+ * `cs.lift((() => …)())`, edited only where a name means something else on
24
+ * the client.
25
+ *
26
+ * - its own binding, renamed (`x` to `__cs_x`), so it can't hide a host binding
27
+ * a splice names; a shorthand keeps its property name (`x: __cs_x`)
28
+ * - a name nothing declares, read as the client's global (`cs.globalThis.x`)
29
+ * - a splice, as the host value the client is handed: `$x` as
30
+ * `cs.splice((x))`, `${…}` as `cs.splice(…)` around the host code
31
+ * - a splice written as a tag, `<$Card>`, as JSX on a name the host value is
32
+ * handed to: `(($Card) => <$Card …>…</$Card>)(cs.splice((Card)))`, and
33
+ * `(void (Card), …)` where it closes, for the closing tag's name
34
+ *
35
+ * Everything else, types included, is TypeScript's to read as written. Each
36
+ * piece maps back to the text it came from; what the wrapper adds maps to no
37
+ * editor feature.
38
+ */
39
+ export function virtualScript(
40
+ ts: typeof import("typescript"),
41
+ script: ClientScript,
42
+ bindings: BindingResolution,
43
+ awaits: boolean,
44
+ renderSplice: (splice: Splice) => Segment[],
45
+ ): Segment[] {
46
+ const file = script.fileWithPlaceholders;
47
+ const text = script.textWithPlaceholders;
48
+ const out: Segment[] = [];
49
+
50
+ const verbatim = (from: number, to: number): void => {
51
+ if (to > from) {
52
+ out.push([
53
+ text.slice(from, to),
54
+ undefined,
55
+ script.toSourceOffset(from),
56
+ to - from,
57
+ ]);
58
+ }
59
+ };
60
+ const mapped = (
61
+ written: string,
62
+ node: ts.Node,
63
+ data?: CodeInformation,
64
+ ): void => {
65
+ const { start, end } = script.toSourceRange(node);
66
+ out.push(
67
+ data === undefined
68
+ ? [written, undefined, start, end - start]
69
+ : [written, undefined, start, end - start, data],
70
+ );
71
+ };
72
+ // Text the virtual code adds, reported at `at` in the host file if at all.
73
+ const added = (written: string, at?: number, data = WRAPPER): void => {
74
+ out.push(at === undefined ? written : [written, undefined, at, 0, data]);
75
+ };
76
+
77
+ const emit = (node: ts.Node, from = node.getStart(file)): void => {
78
+ if (ts.isIdentifier(node)) {
79
+ identifier(node);
80
+ return;
81
+ }
82
+ if (
83
+ (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node)) &&
84
+ isHostTag(node)
85
+ ) {
86
+ hostTag(node);
87
+ return;
88
+ }
89
+ const children = node.getChildren(file);
90
+ if (children.length === 0) {
91
+ verbatim(from, node.getEnd());
92
+ return;
93
+ }
94
+ let at = from;
95
+ for (const child of children) {
96
+ if (child.getEnd() <= at) {
97
+ continue;
98
+ }
99
+ verbatim(at, child.getStart(file));
100
+ emit(child);
101
+ at = child.getEnd();
102
+ }
103
+ verbatim(at, node.getEnd());
104
+ };
105
+
106
+ const identifier = (node: ts.Identifier): void => {
107
+ if (inTypePosition(ts, node)) {
108
+ verbatim(node.getStart(file), node.getEnd());
109
+ return;
110
+ }
111
+ const splice = script.splices[node.text];
112
+ if (splice !== undefined && splice.refs.includes(node)) {
113
+ // Parenthesized whole, so what it stands in reads it as one value: in
114
+ // `new $Animated.Value(0)`, `new` takes `$Animated.Value`, not the call.
115
+ const range = script.toSourceRange(node);
116
+ if (splice.kind === "braced") {
117
+ added("(cs.splice(", splice.expression.getStart(script.sourceFile));
118
+ out.push(...renderSplice(splice));
119
+ added("))", splice.expression.getEnd());
120
+ } else {
121
+ // The name parenthesized too, as 1-char padding: `(count)` against
122
+ // `$count` starts the name where it follows the sigil, so a
123
+ // completion's replacement span round-trips to the bare name.
124
+ added("(cs.splice(", range.start);
125
+ mapped(`(${splice.expression.text})`, node);
126
+ added("))", range.end);
127
+ }
128
+ return;
129
+ }
130
+ const parent = node.parent;
131
+ const shorthand =
132
+ (ts.isShorthandPropertyAssignment(parent) && parent.name === node) ||
133
+ (ts.isBindingElement(parent) &&
134
+ parent.name === node &&
135
+ parent.propertyName === undefined &&
136
+ parent.dotDotDotToken === undefined &&
137
+ ts.isObjectBindingPattern(parent.parent));
138
+ if (bindings.has(node)) {
139
+ if (shorthand) {
140
+ mapped(node.text, node);
141
+ added(": ");
142
+ }
143
+ mapped(mangle(node.text), node);
144
+ return;
145
+ }
146
+ if (node.text !== "undefined" && isReference(ts, node)) {
147
+ if (shorthand) {
148
+ mapped(node.text, node);
149
+ added(": ");
150
+ }
151
+ added("cs.globalThis.");
152
+ mapped(node.text, node);
153
+ return;
154
+ }
155
+ verbatim(node.getStart(file), node.getEnd());
156
+ };
157
+
158
+ // A tag that splices a host value, `<$Card>`, which the client is handed
159
+ // as a component.
160
+ const isHostTag = (
161
+ node: ts.JsxElement | ts.JsxSelfClosingElement,
162
+ ): boolean => {
163
+ const root = tagRoot(
164
+ ts,
165
+ ts.isJsxElement(node) ? node.openingElement.tagName : node.tagName,
166
+ );
167
+ return (
168
+ root !== undefined &&
169
+ script.splices[root.text]?.refs.includes(root) === true
170
+ );
171
+ };
172
+
173
+ // A host tag is checked as JSX, as a tag is anywhere: on a parameter the
174
+ // host value is handed to, `(($Card) => <$Card …>…</$Card>)(cs.splice((Card)))`.
175
+ // JSX then checks it however the component is declared (a class, generic,
176
+ // overloaded) and allows what the framework allows on any tag (React's
177
+ // `key`). Its props and children are written as they were. The parameter
178
+ // takes the tag's written name, which a script can't otherwise bind, so an
179
+ // error about the tag names it as written, and is reported there; the name
180
+ // itself is read through `(Card)`, mapped to it, so definition, rename and
181
+ // references reach the host binding. Where the tag closes, `void (Card)`
182
+ // beside it does the same for the closing tag's name.
183
+ const hostTag = (node: ts.JsxElement | ts.JsxSelfClosingElement): void => {
184
+ const opening = ts.isJsxElement(node) ? node.openingElement : node;
185
+ const tagName = opening.tagName;
186
+ // What the tag starts with, `$Card`, or `$Animated` in `<$Animated.View>`:
187
+ // the splice. The rest, `.View`, is written as it was.
188
+ const root = tagRoot(ts, tagName)!;
189
+ const tag = script.toSourceRange(root);
190
+ // The binding's name, parenthesized over its `$` as an unbraced splice is.
191
+ const name = `(${(script.splices[root.text]!.expression as ts.Identifier).text})`;
192
+ // Where JSX stands, an expression is written in braces: a child of an
193
+ // element or fragment, or an attribute's value.
194
+ const parent = node.parent;
195
+ const braced =
196
+ ts.isJsxAttribute(parent) ||
197
+ ts.isJsxElement(parent) ||
198
+ ts.isJsxFragment(parent);
199
+ if (braced) {
200
+ added("{");
201
+ }
202
+ const closing = ts.isJsxElement(node)
203
+ ? tagRoot(ts, node.closingElement.tagName)!
204
+ : undefined;
205
+ if (closing !== undefined) {
206
+ added("(void ");
207
+ mapped(name, closing, TAG_NAME);
208
+ added(", ");
209
+ }
210
+ added(`((${root.text}) => <`);
211
+ mapped(root.text, root, REPORTED);
212
+ verbatim(root.getEnd(), tagName.getEnd());
213
+ // Its attributes and the opening's end, as written. What stands between
214
+ // the name and the first attribute maps to nothing: the end of the name is
215
+ // the name's, not the attribute's.
216
+ let at = tagName.getEnd();
217
+ for (const [index, attribute] of opening.attributes.properties.entries()) {
218
+ if (index === 0) {
219
+ added(text.slice(at, attribute.getStart(file)));
220
+ } else {
221
+ verbatim(at, attribute.getStart(file));
222
+ }
223
+ emit(attribute);
224
+ at = attribute.getEnd();
225
+ }
226
+ if (opening.attributes.properties.length === 0) {
227
+ added(text.slice(at, opening.getEnd()));
228
+ } else {
229
+ verbatim(at, opening.getEnd());
230
+ }
231
+ if (ts.isJsxElement(node)) {
232
+ at = opening.getEnd();
233
+ for (const child of node.children) {
234
+ verbatim(at, child.getStart(file));
235
+ emit(child);
236
+ at = child.getEnd();
237
+ }
238
+ verbatim(at, node.closingElement.getStart(file));
239
+ added("</");
240
+ mapped(root.text, closing!, REPORTED);
241
+ verbatim(closing!.getEnd(), node.closingElement.tagName.getEnd());
242
+ added(">");
243
+ }
244
+ added(")(cs.splice(", tag.start);
245
+ mapped(name, tagName, TAG_NAME);
246
+ added("))", tag.end);
247
+ if (ts.isJsxElement(node)) {
248
+ added(")");
249
+ }
250
+ if (braced) {
251
+ added("}");
252
+ }
253
+ };
254
+
255
+ // The wrapper the script is checked in, mapped to what it stands for: its
256
+ // opening to `cs\``, its close to the closing backtick, so where it meets the
257
+ // script is where the script starts and ends. No editor feature reads
258
+ // through it.
259
+ const tag = script.sourceNode.getStart(script.sourceFile);
260
+ const opened = script.sourceNode.template.getStart(script.sourceFile) + 1;
261
+ const closed = script.sourceNode.getEnd() - 1;
262
+ out.push([
263
+ awaits ? "cs.lift(await (async () => " : "cs.lift((() => ",
264
+ undefined,
265
+ tag,
266
+ opened - tag,
267
+ WRAPPER,
268
+ ]);
269
+ emit(file, 0);
270
+ out.push([")())", undefined, closed, 1, WRAPPER]);
271
+ return out;
272
+ }
273
+
274
+ // Whether a name is read as a value where it stands, rather than naming a
275
+ // property, an attribute, a label or what a declaration declares.
276
+ function isReference(
277
+ ts: typeof import("typescript"),
278
+ node: ts.Identifier,
279
+ ): boolean {
280
+ const parent = node.parent;
281
+ if (
282
+ (ts.isPropertyAccessExpression(parent) ||
283
+ ts.isPropertyAssignment(parent) ||
284
+ ts.isMethodDeclaration(parent) ||
285
+ ts.isPropertyDeclaration(parent) ||
286
+ ts.isGetAccessorDeclaration(parent) ||
287
+ ts.isSetAccessorDeclaration(parent) ||
288
+ ts.isPropertySignature(parent) ||
289
+ ts.isMethodSignature(parent) ||
290
+ ts.isEnumMember(parent) ||
291
+ ts.isJsxAttribute(parent) ||
292
+ ts.isVariableDeclaration(parent) ||
293
+ ts.isParameter(parent) ||
294
+ ts.isFunctionDeclaration(parent) ||
295
+ ts.isFunctionExpression(parent) ||
296
+ ts.isClassDeclaration(parent) ||
297
+ ts.isClassExpression(parent) ||
298
+ ts.isTypeAliasDeclaration(parent) ||
299
+ ts.isInterfaceDeclaration(parent) ||
300
+ ts.isEnumDeclaration(parent) ||
301
+ ts.isTypeParameterDeclaration(parent)) &&
302
+ parent.name === node
303
+ ) {
304
+ return false;
305
+ }
306
+ if (ts.isBindingElement(parent)) {
307
+ return false; // its name declares, its property name names a property
308
+ }
309
+ if (
310
+ (ts.isLabeledStatement(parent) ||
311
+ ts.isBreakStatement(parent) ||
312
+ ts.isContinueStatement(parent)) &&
313
+ parent.label === node
314
+ ) {
315
+ return false;
316
+ }
317
+ if (
318
+ (ts.isJsxOpeningElement(parent) ||
319
+ ts.isJsxSelfClosingElement(parent) ||
320
+ ts.isJsxClosingElement(parent)) &&
321
+ parent.tagName === node
322
+ ) {
323
+ return false; // an intrinsic element's name, or a host tag's (above)
324
+ }
325
+ return !(
326
+ ts.isQualifiedName(parent) ||
327
+ ts.isMetaProperty(parent) ||
328
+ ts.isJsxNamespacedName(parent)
329
+ );
330
+ }
331
+
332
+ // Whether a name stands in a type, which names types rather than values: all
333
+ // but what a `typeof` reads, and a class's `extends`.
334
+ function inTypePosition(
335
+ ts: typeof import("typescript"),
336
+ node: ts.Identifier,
337
+ ): boolean {
338
+ for (let at: ts.Node = node; at.parent !== undefined; at = at.parent) {
339
+ const parent = at.parent;
340
+ if (ts.isTypeQueryNode(parent)) {
341
+ return false;
342
+ }
343
+ if (
344
+ ts.isExpressionWithTypeArguments(parent) &&
345
+ ts.isHeritageClause(parent.parent) &&
346
+ ts.isClassLike(parent.parent.parent)
347
+ ) {
348
+ return false;
349
+ }
350
+ if (ts.isTypeNode(parent)) {
351
+ return true;
352
+ }
353
+ }
354
+ return false;
355
+ }
@@ -0,0 +1,29 @@
1
+ import type { SourceMapping } from "./buildMappings.js";
2
+ import type { Diagnostic } from "./diagnostics.js";
3
+ import { parseSourceText } from "./parseFile.js";
4
+ import { printVirtualCode } from "./printVirtualCode.js";
5
+ import { rewriteFile } from "./rewriteFile.js";
6
+
7
+ export interface Virtualized {
8
+ virtualCode: string;
9
+ mappings: SourceMapping[];
10
+ diagnostics: Diagnostic[];
11
+ }
12
+
13
+ export function virtualize(
14
+ ts: typeof import("typescript"),
15
+ filePath: string,
16
+ sourceText: string,
17
+ ): Virtualized {
18
+ const parsedFile = parseSourceText(ts, filePath, sourceText);
19
+
20
+ const rewrittenFile = rewriteFile(ts, parsedFile);
21
+
22
+ const { virtualCode, mappings } = printVirtualCode(
23
+ ts,
24
+ parsedFile,
25
+ rewrittenFile,
26
+ );
27
+
28
+ return { virtualCode, mappings, diagnostics: rewrittenFile.diagnostics };
29
+ }