@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
package/src/index.ts ADDED
@@ -0,0 +1,20 @@
1
+ export type { SourceMapping } from "./buildMappings.js";
2
+ export type { CodeInformation } from "./CodeInformation.js";
3
+ export type { Diagnostic } from "./diagnostics.js";
4
+ export type { EmittedScript } from "./emitScript.js";
5
+ export { emitScripts, type EmittedScriptAt } from "./emitScripts.js";
6
+ export { flattenScripts } from "./flattenScripts.js";
7
+ export type { ClientScript, ParsedFile, Splice } from "./parseFile.js";
8
+ export { parseSourceFile, parseSourceText } from "./parseFile.js";
9
+ export { type ResolvedParam, resolveBindings } from "./resolveBindings.js";
10
+ export type { Plugin } from "./compileScript.js";
11
+ export {
12
+ type CompileModuleOptions,
13
+ compileModule,
14
+ pluginsFrom,
15
+ } from "./compileModule.js";
16
+ export { type TransformOptions, transform } from "./transform.js";
17
+ export { transpile } from "./transpile.js";
18
+ export { mangle, unmangle } from "./unmangle.js";
19
+ export type { Virtualized } from "./virtualize.js";
20
+ export { virtualize } from "./virtualize.js";
@@ -0,0 +1,15 @@
1
+ // Whether a tag names a component rather than one of the target's elements.
2
+ //
3
+ // TypeScript's own rule, spelled out because it does not publish it (`ts`'s
4
+ // `isIntrinsicJsxName` is internal): a name starting lowercase, or holding a
5
+ // `-` or a `:`, is the target's, and anything else is a value in scope. Written
6
+ // here so a tag reads the same to the compiler as it does to the checker, which
7
+ // applies this rule to the virtual code.
8
+ export function isComponentTag(tagName: string): boolean {
9
+ const first = tagName.charCodeAt(0);
10
+ const intrinsic =
11
+ (first >= 0x61 /* a */ && first <= 0x7a) /* z */ ||
12
+ tagName.includes("-") ||
13
+ tagName.includes(":");
14
+ return !intrinsic;
15
+ }
@@ -0,0 +1,18 @@
1
+ import type ts from "typescript";
2
+
3
+ /** <receiver>.<method>(...args) */
4
+ export function call(
5
+ ts: typeof import("typescript"),
6
+ receiver: string,
7
+ method: string,
8
+ args: ts.Expression[],
9
+ ): ts.CallExpression {
10
+ return ts.factory.createCallExpression(
11
+ ts.factory.createPropertyAccessExpression(
12
+ ts.factory.createIdentifier(receiver),
13
+ method,
14
+ ),
15
+ undefined,
16
+ args,
17
+ );
18
+ }
@@ -0,0 +1,331 @@
1
+ import type ts from "typescript";
2
+ import type { SourceRange } from "./SourceRange.js";
3
+
4
+ export interface ParsedFile {
5
+ sourceFile: ts.SourceFile;
6
+ scripts: ClientScript[];
7
+ }
8
+
9
+ export interface ClientScript {
10
+ sourceFile: ts.SourceFile;
11
+ sourceNode: ts.TaggedTemplateExpression;
12
+ textWithPlaceholders: string;
13
+ fileWithPlaceholders: ts.SourceFile;
14
+ splices: { [placeholder: string]: Splice };
15
+
16
+ toSourceRange: (node: ts.Node) => SourceRange;
17
+ // an offset in the placeholder text, in the host file
18
+ toSourceOffset: (offset: number) => number;
19
+ }
20
+
21
+ // A host value read into a client script: braced — a `${...}` template
22
+ // span — or unbraced, the `$x` shorthand, sugar for splicing the host
23
+ // binding named without the sigil (`$x` reads as `${x}`). Both stand in the
24
+ // placeholder text as an identifier — the spelling that keys the splice
25
+ // dictionary and the script's `params` (`key`) — and evaluate a host
26
+ // expression (`expression`).
27
+ //
28
+ // What the text spells, and only that. A component tag names a host binding
29
+ // too, but no sigil says so — which tags do is scope resolution's to answer —
30
+ // so the rewrite hands those to the script itself (see `rewriteScript`).
31
+ export type Splice = BracedSplice | UnbracedSplice;
32
+
33
+ export interface BracedSplice {
34
+ kind: "braced";
35
+ // the template span's host expression
36
+ expression: ts.Expression;
37
+ // the param's key — the `$0splice<n>` identifier standing in
38
+ // `textWithPlaceholders`, which also keys the splice dictionary
39
+ key: string;
40
+ // client scripts nested in the host expression
41
+ scripts: ClientScript[];
42
+ // where the script reads it, in the placeholder text
43
+ refs: ts.Identifier[];
44
+ }
45
+
46
+ export interface UnbracedSplice {
47
+ kind: "unbraced";
48
+ // the host binding the shorthand names (synthesized, e.g. `x` for `$x`)
49
+ expression: ts.Identifier;
50
+ // the param's key — the `$x` spelling the shorthand stands as in the
51
+ // placeholder text, which also keys the splice dictionary
52
+ key: string;
53
+ // a shorthand names a single binding, so it nests no scripts
54
+ scripts: [];
55
+ // where the script reads it, in the placeholder text
56
+ refs: ts.Identifier[];
57
+ }
58
+
59
+ export function parseSourceText(
60
+ ts: typeof import("typescript"),
61
+ filePath: string,
62
+ sourceText: string,
63
+ ): ParsedFile {
64
+ const sourceFile = ts.createSourceFile(
65
+ filePath,
66
+ sourceText,
67
+ ts.ScriptTarget.Latest,
68
+ true,
69
+ );
70
+ return parseSourceFile(ts, sourceFile);
71
+ }
72
+
73
+ export function parseSourceFile(
74
+ ts: typeof import("typescript"),
75
+ sourceFile: ts.SourceFile,
76
+ ): ParsedFile {
77
+ const scripts = getDirectScripts(ts, null, sourceFile);
78
+ return { sourceFile, scripts };
79
+ }
80
+
81
+ function getDirectScripts(
82
+ ts: typeof import("typescript"),
83
+ parent: Splice | null,
84
+ sourceFile: ts.SourceFile,
85
+ ): ClientScript[] {
86
+ const node: ts.Node = parent ? parent.expression : sourceFile;
87
+ const taggedTemplates: ts.TaggedTemplateExpression[] = [];
88
+
89
+ const visit = (current: ts.Node): void => {
90
+ if (
91
+ ts.isTaggedTemplateExpression(current) &&
92
+ ts.isIdentifier(current.tag) &&
93
+ current.tag.text === "cs"
94
+ ) {
95
+ taggedTemplates.push(current);
96
+ } else {
97
+ current.forEachChild(visit);
98
+ }
99
+ };
100
+ visit(node);
101
+
102
+ const scripts: ClientScript[] = [];
103
+
104
+ taggedTemplates.forEach((taggedTemplate) => {
105
+ const { textWithPlaceholders, mappings } = toTextWithPlaceholders(
106
+ ts,
107
+ taggedTemplate,
108
+ sourceFile,
109
+ );
110
+ const fileWithPlaceholders = ts.createSourceFile(
111
+ sourceFile.fileName,
112
+ textWithPlaceholders,
113
+ ts.ScriptTarget.Latest,
114
+ true,
115
+ );
116
+ // The dictionary's insertion order is evaluation order: emitted as the
117
+ // script's `params` array literal, the splices' host expressions run
118
+ // first to last when the `cs` expression itself evaluates — left to
119
+ // right in source order, like a real template literal's spans.
120
+ const splices = getDirectSplices(
121
+ ts,
122
+ taggedTemplate,
123
+ sourceFile,
124
+ fileWithPlaceholders,
125
+ );
126
+ const toSourceRange = (node: ts.Node): SourceRange => ({
127
+ start: toSourceOffset(mappings, node.getStart(fileWithPlaceholders)),
128
+ end: toSourceOffset(mappings, node.getEnd()),
129
+ });
130
+ scripts.push({
131
+ sourceFile,
132
+ sourceNode: taggedTemplate,
133
+ textWithPlaceholders,
134
+ fileWithPlaceholders,
135
+ toSourceRange,
136
+ toSourceOffset: (offset) => toSourceOffset(mappings, offset),
137
+ splices,
138
+ });
139
+ });
140
+
141
+ return scripts;
142
+ }
143
+
144
+ // A splice per first reference in the placeholder text, in source order, and
145
+ // every reference to it: a `$0splice<n>` placeholder resolves to its template
146
+ // span's braced splice, and any other `$x` identifier mints an unbraced
147
+ // splice, deduplicated by spelling. Property, attribute and declaration names
148
+ // are not references (a `$`-prefixed declaration is rejected at rewrite time),
149
+ // and nested scripts are already placeholders in this text, so the walk scans
150
+ // only the script's own body.
151
+ function getDirectSplices(
152
+ ts: typeof import("typescript"),
153
+ taggedTemplate: ts.TaggedTemplateExpression,
154
+ sourceFile: ts.SourceFile,
155
+ fileWithPlaceholders: ts.SourceFile,
156
+ ): { [placeholder: string]: Splice } {
157
+ // No prototype: a script's `valueOf` or `constructor` is no splice.
158
+ const splices: { [placeholder: string]: Splice } = Object.create(null);
159
+
160
+ const template = taggedTemplate.template;
161
+ const spans = ts.isTemplateExpression(template) ? template.templateSpans : [];
162
+
163
+ const visit = (node: ts.Node): void => {
164
+ if (ts.isPropertyAccessExpression(node)) {
165
+ visit(node.expression); // the name is not a reference
166
+ return;
167
+ }
168
+ if (ts.isPropertyAssignment(node)) {
169
+ if (ts.isComputedPropertyName(node.name)) {
170
+ visit(node.name);
171
+ }
172
+ visit(node.initializer);
173
+ return;
174
+ }
175
+ if (
176
+ ts.isVariableDeclaration(node) ||
177
+ ts.isParameter(node) ||
178
+ ts.isBindingElement(node)
179
+ ) {
180
+ if (node.initializer) {
181
+ visit(node.initializer);
182
+ }
183
+ return;
184
+ }
185
+ if (ts.isJsxAttribute(node)) {
186
+ if (node.initializer) {
187
+ visit(node.initializer); // the name is not a reference
188
+ }
189
+ return;
190
+ }
191
+ if (ts.isCatchClause(node)) {
192
+ visit(node.block); // the catch binding is not a reference
193
+ return;
194
+ }
195
+ if (ts.isIdentifier(node) && splices[node.text] != null) {
196
+ splices[node.text]!.refs.push(node);
197
+ return;
198
+ }
199
+ if (
200
+ ts.isIdentifier(node) &&
201
+ node.text.startsWith("$") &&
202
+ node.text.length > 1
203
+ ) {
204
+ const key = node.text;
205
+ if (key.startsWith("$0splice")) {
206
+ // The numeric suffix indexes the template's spans. A `$0splice`
207
+ // spelling that resolves to no span (`$0spliceFoo`, an out-of-range
208
+ // index) is no splice at all — and no host binding either, since a
209
+ // name can't start with `$0`.
210
+ const index = Number(key.slice("$0splice".length));
211
+ const span = spans[index];
212
+ if (span != null) {
213
+ const splice: BracedSplice = {
214
+ kind: "braced",
215
+ expression: span.expression,
216
+ key,
217
+ scripts: [],
218
+ refs: [node],
219
+ };
220
+ splices[key] = splice;
221
+ splice.scripts = getDirectScripts(ts, splice, sourceFile);
222
+ }
223
+ } else {
224
+ splices[key] = {
225
+ kind: "unbraced",
226
+ expression: ts.factory.createIdentifier(key.slice(1)),
227
+ key,
228
+ scripts: [],
229
+ refs: [node],
230
+ };
231
+ }
232
+ return;
233
+ }
234
+ node.forEachChild(visit);
235
+ };
236
+ visit(fileWithPlaceholders);
237
+
238
+ return splices;
239
+ }
240
+
241
+ interface OffsetMapping {
242
+ placeholderStart: number;
243
+ length: number;
244
+ sourceStart: number;
245
+ verbatim: boolean;
246
+ }
247
+
248
+ function toTextWithPlaceholders(
249
+ ts: typeof import("typescript"),
250
+ taggedTemplate: ts.TaggedTemplateExpression,
251
+ sourceFile: ts.SourceFile,
252
+ ): {
253
+ textWithPlaceholders: string;
254
+ mappings: OffsetMapping[];
255
+ } {
256
+ const sourceText = sourceFile.text;
257
+ const template = taggedTemplate.template;
258
+
259
+ const start = template.getStart(sourceFile) + 1; // past `
260
+ const end = template.getEnd() - 1; // before `
261
+
262
+ let textWithPlaceholders = "";
263
+ const mappings: OffsetMapping[] = [];
264
+
265
+ if (ts.isNoSubstitutionTemplateLiteral(template)) {
266
+ textWithPlaceholders = sourceText.slice(start, end);
267
+ mappings.push({
268
+ placeholderStart: 0,
269
+ length: textWithPlaceholders.length,
270
+ sourceStart: start,
271
+ verbatim: true,
272
+ });
273
+ } else {
274
+ let chunkStart = start;
275
+
276
+ template.templateSpans.forEach((span, index) => {
277
+ const placeholder = `$0splice${index}`;
278
+ const dollarBrace = span.expression.getFullStart() - 2; // before ${
279
+ const chunk = sourceText.slice(chunkStart, dollarBrace);
280
+ mappings.push({
281
+ placeholderStart: textWithPlaceholders.length,
282
+ length: chunk.length,
283
+ sourceStart: chunkStart,
284
+ verbatim: true,
285
+ });
286
+ textWithPlaceholders += chunk;
287
+ mappings.push({
288
+ placeholderStart: textWithPlaceholders.length,
289
+ length: placeholder.length,
290
+ sourceStart: dollarBrace,
291
+ verbatim: false,
292
+ });
293
+ textWithPlaceholders += placeholder;
294
+ chunkStart = span.literal.getStart(sourceFile) + 1; // past }
295
+ });
296
+
297
+ const tail = sourceText.slice(chunkStart, end);
298
+ mappings.push({
299
+ placeholderStart: textWithPlaceholders.length,
300
+ length: tail.length,
301
+ sourceStart: chunkStart,
302
+ verbatim: true,
303
+ });
304
+ textWithPlaceholders += tail;
305
+ }
306
+
307
+ return { textWithPlaceholders, mappings };
308
+ }
309
+
310
+ function toSourceOffset(mappings: OffsetMapping[], pos: number): number {
311
+ for (const mapping of mappings) {
312
+ if (
313
+ mapping.verbatim &&
314
+ pos >= mapping.placeholderStart &&
315
+ pos <= mapping.placeholderStart + mapping.length
316
+ ) {
317
+ return mapping.sourceStart + (pos - mapping.placeholderStart);
318
+ }
319
+ }
320
+ for (const mapping of mappings) {
321
+ if (
322
+ !mapping.verbatim &&
323
+ pos >= mapping.placeholderStart &&
324
+ pos <= mapping.placeholderStart + mapping.length
325
+ ) {
326
+ return mapping.sourceStart;
327
+ }
328
+ }
329
+ const last = mappings[mappings.length - 1];
330
+ return last.sourceStart + last.length;
331
+ }
@@ -0,0 +1,93 @@
1
+ import type ts from "typescript";
2
+ import { buildMappings, type SourceMapping } from "./buildMappings.js";
3
+ import type { ClientScript, ParsedFile, Splice } from "./parseFile.js";
4
+ import type { RewrittenFile } from "./rewriteFile.js";
5
+ import { type Segment, segmentsToString } from "./segmentsToString.js";
6
+ import { virtualScript } from "./virtualScript.js";
7
+
8
+ interface VirtualizedFile {
9
+ virtualCode: string;
10
+ mappings: SourceMapping[];
11
+ }
12
+
13
+ export function printVirtualCode(
14
+ ts: typeof import("typescript"),
15
+ parsedFile: ParsedFile,
16
+ rewrittenFile: RewrittenFile,
17
+ ): VirtualizedFile {
18
+ const { sourceFile } = parsedFile;
19
+ const segments: Segment[] = [];
20
+
21
+ let cursor = 0;
22
+ for (const script of parsedFile.scripts) {
23
+ const start = script.sourceNode.getStart(sourceFile);
24
+ segments.push(...renderVerbatim(sourceFile.text, cursor, start - cursor));
25
+ segments.push(...renderScript(ts, sourceFile, rewrittenFile, script));
26
+ cursor = script.sourceNode.getEnd();
27
+ }
28
+ segments.push(
29
+ ...renderVerbatim(sourceFile.text, cursor, sourceFile.text.length - cursor),
30
+ );
31
+
32
+ return {
33
+ virtualCode: segmentsToString(segments),
34
+ mappings: buildMappings(segments),
35
+ };
36
+ }
37
+
38
+ function renderScript(
39
+ ts: typeof import("typescript"),
40
+ sourceFile: ts.SourceFile,
41
+ rewrittenFile: RewrittenFile,
42
+ script: ClientScript,
43
+ ): Segment[] {
44
+ const rewritten = rewrittenFile.scripts.get(script.sourceNode);
45
+ if (rewritten === undefined) {
46
+ return [];
47
+ }
48
+ // Left as written, its errors already said: `never` fits wherever it
49
+ // stands, so nothing else is reported for it.
50
+ if (rewritten.leftAsWritten) {
51
+ return ["cs.lift(undefined as never)"];
52
+ }
53
+ return virtualScript(
54
+ ts,
55
+ script,
56
+ rewrittenFile.bindings,
57
+ rewritten.awaits,
58
+ (splice) => renderSplice(ts, sourceFile, rewrittenFile, splice),
59
+ );
60
+ }
61
+
62
+ function renderSplice(
63
+ ts: typeof import("typescript"),
64
+ sourceFile: ts.SourceFile,
65
+ rewrittenFile: RewrittenFile,
66
+ splice: Splice,
67
+ ): Segment[] {
68
+ const segments: Segment[] = [];
69
+ const expression = splice.expression;
70
+ const end = expression.getEnd();
71
+
72
+ let cursor = expression.getStart(sourceFile);
73
+ for (const nested of splice.scripts) {
74
+ const start = nested.sourceNode.getStart(sourceFile);
75
+ segments.push(...renderVerbatim(sourceFile.text, cursor, start - cursor));
76
+ segments.push(...renderScript(ts, sourceFile, rewrittenFile, nested));
77
+ cursor = nested.sourceNode.getEnd();
78
+ }
79
+ segments.push(...renderVerbatim(sourceFile.text, cursor, end - cursor));
80
+
81
+ return segments;
82
+ }
83
+
84
+ function renderVerbatim(
85
+ sourceText: string,
86
+ start: number,
87
+ length: number,
88
+ ): Segment[] {
89
+ if (length <= 0) {
90
+ return [];
91
+ }
92
+ return [[sourceText.slice(start, start + length), undefined, start, length]];
93
+ }