@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.
- package/LICENSE +21 -0
- package/README.md +25 -2
- package/dist/CodeInformation.d.ts +8 -0
- package/dist/CodeInformation.js +2 -0
- package/dist/CodeInformation.js.map +1 -0
- package/dist/SourceRange.d.ts +4 -0
- package/dist/SourceRange.js +2 -0
- package/dist/SourceRange.js.map +1 -0
- package/dist/buildMappings.d.ts +10 -0
- package/dist/buildMappings.js +21 -0
- package/dist/buildMappings.js.map +1 -0
- package/dist/compileModule.d.ts +25 -0
- package/dist/compileModule.js +78 -0
- package/dist/compileModule.js.map +1 -0
- package/dist/compileScript.d.ts +24 -0
- package/dist/compileScript.js +66 -0
- package/dist/compileScript.js.map +1 -0
- package/dist/diagnostics.d.ts +8 -0
- package/dist/diagnostics.js +2 -0
- package/dist/diagnostics.js.map +1 -0
- package/dist/emitScript.d.ts +52 -0
- package/dist/emitScript.js +201 -0
- package/dist/emitScript.js.map +1 -0
- package/dist/emitScripts.d.ts +10 -0
- package/dist/emitScripts.js +23 -0
- package/dist/emitScripts.js.map +1 -0
- package/dist/flattenScripts.d.ts +2 -0
- package/dist/flattenScripts.js +10 -0
- package/dist/flattenScripts.js.map +1 -0
- package/dist/hashText.d.ts +1 -0
- package/dist/hashText.js +18 -0
- package/dist/hashText.js.map +1 -0
- package/dist/hostElements.d.ts +3 -0
- package/dist/hostElements.js +28 -0
- package/dist/hostElements.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/isComponentTag.d.ts +1 -0
- package/dist/isComponentTag.js +15 -0
- package/dist/isComponentTag.js.map +1 -0
- package/dist/nodeFactory.d.ts +3 -0
- package/dist/nodeFactory.js +5 -0
- package/dist/nodeFactory.js.map +1 -0
- package/dist/parseFile.d.ts +34 -0
- package/dist/parseFile.js +200 -0
- package/dist/parseFile.js.map +1 -0
- package/dist/printVirtualCode.d.ts +9 -0
- package/dist/printVirtualCode.js +52 -0
- package/dist/printVirtualCode.js.map +1 -0
- package/dist/resolveBindings.d.ts +76 -0
- package/dist/resolveBindings.js +240 -0
- package/dist/resolveBindings.js.map +1 -0
- package/dist/rewriteFile.d.ts +12 -0
- package/dist/rewriteFile.js +22 -0
- package/dist/rewriteFile.js.map +1 -0
- package/dist/rewriteScript.d.ts +27 -0
- package/dist/rewriteScript.js +268 -0
- package/dist/rewriteScript.js.map +1 -0
- package/dist/segmentsToString.d.ts +9 -0
- package/dist/segmentsToString.js +4 -0
- package/dist/segmentsToString.js.map +1 -0
- package/dist/tagRoot.d.ts +2 -0
- package/dist/tagRoot.js +10 -0
- package/dist/tagRoot.js.map +1 -0
- package/dist/transform.d.ts +17 -0
- package/dist/transform.js +61 -0
- package/dist/transform.js.map +1 -0
- package/dist/transpile.d.ts +27 -0
- package/dist/transpile.js +41 -0
- package/dist/transpile.js.map +1 -0
- package/dist/unmangle.d.ts +2 -0
- package/dist/unmangle.js +9 -0
- package/dist/unmangle.js.map +1 -0
- package/dist/virtualScript.d.ts +22 -0
- package/dist/virtualScript.js +305 -0
- package/dist/virtualScript.js.map +1 -0
- package/dist/virtualize.d.ts +8 -0
- package/dist/virtualize.js +10 -0
- package/dist/virtualize.js.map +1 -0
- package/package.json +58 -3
- package/src/CodeInformation.ts +14 -0
- package/src/SourceRange.ts +4 -0
- package/src/buildMappings.ts +31 -0
- package/src/compileModule.ts +132 -0
- package/src/compileScript.ts +97 -0
- package/src/diagnostics.ts +9 -0
- package/src/emitScript.ts +297 -0
- package/src/emitScripts.ts +39 -0
- package/src/flattenScripts.ts +13 -0
- package/src/hashText.ts +17 -0
- package/src/hostElements.ts +34 -0
- package/src/index.ts +20 -0
- package/src/isComponentTag.ts +15 -0
- package/src/nodeFactory.ts +18 -0
- package/src/parseFile.ts +331 -0
- package/src/printVirtualCode.ts +93 -0
- package/src/resolveBindings.ts +417 -0
- package/src/rewriteFile.ts +52 -0
- package/src/rewriteScript.ts +413 -0
- package/src/segmentsToString.ts +16 -0
- package/src/tagRoot.ts +14 -0
- package/src/transform.ts +96 -0
- package/src/transpile.ts +49 -0
- package/src/unmangle.ts +10 -0
- package/src/virtualScript.ts +355 -0
- package/src/virtualize.ts +29 -0
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
import type ts from "typescript";
|
|
2
|
+
import type { CompiledScript } from "./compileScript.js";
|
|
3
|
+
import type { Diagnostic } from "./diagnostics.js";
|
|
4
|
+
import {
|
|
5
|
+
type EmittedScript,
|
|
6
|
+
emitScript,
|
|
7
|
+
paramName,
|
|
8
|
+
scriptEdits,
|
|
9
|
+
} from "./emitScript.js";
|
|
10
|
+
import { isComponentTag } from "./isComponentTag.js";
|
|
11
|
+
import { call } from "./nodeFactory.js";
|
|
12
|
+
import type { ClientScript } from "./parseFile.js";
|
|
13
|
+
import type { BindingResolution, ResolvedParam } from "./resolveBindings.js";
|
|
14
|
+
import type { SourceRange } from "./SourceRange.js";
|
|
15
|
+
import { tagRoot } from "./tagRoot.js";
|
|
16
|
+
|
|
17
|
+
/** What `cs.create` is handed for a script: which it is, and what it runs. */
|
|
18
|
+
export interface RuntimeScript {
|
|
19
|
+
id: string;
|
|
20
|
+
// the module's `params`, an array literal of literals
|
|
21
|
+
params: ts.Expression;
|
|
22
|
+
// the script's `args`, an array literal of host expressions
|
|
23
|
+
args: ts.Expression;
|
|
24
|
+
emitted: EmittedScript;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* A script's module, declared once at the top of its host file, every run of
|
|
29
|
+
* the script sharing it: `const $module0 = { id, code, … };`.
|
|
30
|
+
*/
|
|
31
|
+
export function moduleDeclaration(
|
|
32
|
+
ts: typeof import("typescript"),
|
|
33
|
+
name: string,
|
|
34
|
+
{ id, params }: RuntimeScript,
|
|
35
|
+
{ code, map, dependencies }: CompiledScript,
|
|
36
|
+
): ts.VariableStatement {
|
|
37
|
+
const f = ts.factory;
|
|
38
|
+
const string = (text: string) => f.createStringLiteral(text);
|
|
39
|
+
return f.createVariableStatement(
|
|
40
|
+
undefined,
|
|
41
|
+
f.createVariableDeclarationList(
|
|
42
|
+
[
|
|
43
|
+
f.createVariableDeclaration(
|
|
44
|
+
name,
|
|
45
|
+
undefined,
|
|
46
|
+
undefined,
|
|
47
|
+
f.createObjectLiteralExpression(
|
|
48
|
+
[
|
|
49
|
+
f.createPropertyAssignment("id", string(id)),
|
|
50
|
+
f.createPropertyAssignment("code", string(code)),
|
|
51
|
+
f.createPropertyAssignment("map", string(map)),
|
|
52
|
+
f.createPropertyAssignment(
|
|
53
|
+
"dependencies",
|
|
54
|
+
f.createArrayLiteralExpression(dependencies.map(string)),
|
|
55
|
+
),
|
|
56
|
+
f.createPropertyAssignment("params", params),
|
|
57
|
+
],
|
|
58
|
+
true,
|
|
59
|
+
),
|
|
60
|
+
),
|
|
61
|
+
],
|
|
62
|
+
ts.NodeFlags.Const,
|
|
63
|
+
),
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** A script as the client gets it, `cs.create($module0, [...args])`. */
|
|
68
|
+
export function createCall(
|
|
69
|
+
ts: typeof import("typescript"),
|
|
70
|
+
moduleName: string,
|
|
71
|
+
{ args }: RuntimeScript,
|
|
72
|
+
): ts.Expression {
|
|
73
|
+
return call(ts, "cs", "create", [
|
|
74
|
+
ts.factory.createIdentifier(moduleName),
|
|
75
|
+
args,
|
|
76
|
+
]);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface RewrittenScript {
|
|
80
|
+
// what the client runs (null where the script did not parse, which is left
|
|
81
|
+
// as written)
|
|
82
|
+
runtime: RuntimeScript | null;
|
|
83
|
+
// whether its splices `await`, so the wrapper it is checked in does too
|
|
84
|
+
awaits: boolean;
|
|
85
|
+
// left as written: checked as nothing, its errors already said
|
|
86
|
+
leftAsWritten: boolean;
|
|
87
|
+
diagnostics: Diagnostic[];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function rewriteScript(
|
|
91
|
+
ts: typeof import("typescript"),
|
|
92
|
+
clientScript: ClientScript,
|
|
93
|
+
fileHash: string,
|
|
94
|
+
bindings: BindingResolution,
|
|
95
|
+
params: readonly ResolvedParam[] = [],
|
|
96
|
+
): RewrittenScript {
|
|
97
|
+
const { sourceFile, sourceNode, fileWithPlaceholders } = clientScript;
|
|
98
|
+
|
|
99
|
+
// A script that can't be rewritten is left as written at runtime, and
|
|
100
|
+
// checked as nothing, so nothing else is reported for it.
|
|
101
|
+
const leftAsWritten = (diagnostics: Diagnostic[]): RewrittenScript => ({
|
|
102
|
+
runtime: null,
|
|
103
|
+
awaits: false,
|
|
104
|
+
leftAsWritten: true,
|
|
105
|
+
diagnostics,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const scriptRange: SourceRange = {
|
|
109
|
+
start: sourceNode.getStart(sourceFile),
|
|
110
|
+
end: sourceNode.getEnd(),
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// A script that doesn't parse is reported as TypeScript's parser read it.
|
|
114
|
+
const parseErrors = parseDiagnostics(fileWithPlaceholders);
|
|
115
|
+
if (parseErrors.length > 0) {
|
|
116
|
+
return leftAsWritten(
|
|
117
|
+
parseErrors.map((diagnostic) => ({
|
|
118
|
+
range: {
|
|
119
|
+
start: clientScript.toSourceOffset(diagnostic.start!),
|
|
120
|
+
end: clientScript.toSourceOffset(
|
|
121
|
+
diagnostic.start! + diagnostic.length!,
|
|
122
|
+
),
|
|
123
|
+
},
|
|
124
|
+
message: ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n"),
|
|
125
|
+
category: ts.DiagnosticCategory.Error,
|
|
126
|
+
code: diagnostic.code,
|
|
127
|
+
})),
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// A script is one expression or one block: statements go in braces.
|
|
132
|
+
const [statement] = fileWithPlaceholders.statements;
|
|
133
|
+
if (
|
|
134
|
+
fileWithPlaceholders.statements.length !== 1 ||
|
|
135
|
+
!(ts.isExpressionStatement(statement!) || ts.isBlock(statement!))
|
|
136
|
+
) {
|
|
137
|
+
return leftAsWritten([
|
|
138
|
+
{
|
|
139
|
+
range: scriptRange,
|
|
140
|
+
message:
|
|
141
|
+
"A `cs` client script is one expression or one block: write " +
|
|
142
|
+
"statements in braces, e.g. cs`{ a(); b(); }`.",
|
|
143
|
+
category: ts.DiagnosticCategory.Error,
|
|
144
|
+
code: 0,
|
|
145
|
+
},
|
|
146
|
+
]);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// A `${…}` written where the script has text rather than code is no splice.
|
|
150
|
+
const unspliced = unsplicedSpans(ts, clientScript);
|
|
151
|
+
if (unspliced.length > 0) {
|
|
152
|
+
return leftAsWritten(unspliced);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Nor is a script that writes what it can't: emitting it may not be
|
|
156
|
+
// possible, as a declared `$` name is a splice, not a name.
|
|
157
|
+
const refused = refusals(ts, clientScript, bindings);
|
|
158
|
+
if (refused.length > 0) {
|
|
159
|
+
return leftAsWritten(refused);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Which script this is (see `ClientModule.id`): its line from 1, and its
|
|
163
|
+
// column from 0.
|
|
164
|
+
const { line, character } = sourceFile.getLineAndCharacterOfPosition(
|
|
165
|
+
scriptRange.start,
|
|
166
|
+
);
|
|
167
|
+
const id = `${fileHash}:${line + 1}:${character}`;
|
|
168
|
+
|
|
169
|
+
// The function's parameters, as `ClientModule.params` describes them, and
|
|
170
|
+
// what a run passes: for each splice and tag, the host expression written
|
|
171
|
+
// there.
|
|
172
|
+
const objectLiteral = (properties: Record<string, ts.Expression>) =>
|
|
173
|
+
ts.factory.createObjectLiteralExpression(
|
|
174
|
+
Object.entries(properties).map(([name, value]) =>
|
|
175
|
+
ts.factory.createPropertyAssignment(name, value),
|
|
176
|
+
),
|
|
177
|
+
false,
|
|
178
|
+
);
|
|
179
|
+
const string = (text: string) => ts.factory.createStringLiteral(text);
|
|
180
|
+
const paramsLiteral = ts.factory.createArrayLiteralExpression(
|
|
181
|
+
params.map((param) => {
|
|
182
|
+
switch (param.kind) {
|
|
183
|
+
case "splice":
|
|
184
|
+
return objectLiteral({
|
|
185
|
+
kind: string("splice"),
|
|
186
|
+
bindings: ts.factory.createArrayLiteralExpression(
|
|
187
|
+
param.bindings.map(string),
|
|
188
|
+
false,
|
|
189
|
+
),
|
|
190
|
+
});
|
|
191
|
+
case "capture":
|
|
192
|
+
return objectLiteral({
|
|
193
|
+
kind: string("capture"),
|
|
194
|
+
key: string(param.key),
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}),
|
|
198
|
+
false,
|
|
199
|
+
);
|
|
200
|
+
const argsLiteral = ts.factory.createArrayLiteralExpression(
|
|
201
|
+
params.flatMap((param) =>
|
|
202
|
+
param.kind === "capture"
|
|
203
|
+
? []
|
|
204
|
+
: [clientScript.splices[param.key]!.expression],
|
|
205
|
+
),
|
|
206
|
+
false,
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
// A splice that awaits is the host's `await`, where the template is: where
|
|
210
|
+
// the host may await, the script's function is async, and awaited there;
|
|
211
|
+
// elsewhere it isn't, so the splice's own `await` is refused where written.
|
|
212
|
+
const awaits =
|
|
213
|
+
mayAwait(ts, sourceNode) &&
|
|
214
|
+
Object.values(clientScript.splices).some(
|
|
215
|
+
(splice) => splice.kind === "braced" && hasAwait(ts, splice.expression),
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
const emitted = emitScript(
|
|
219
|
+
ts,
|
|
220
|
+
clientScript,
|
|
221
|
+
params.map(paramName),
|
|
222
|
+
scriptEdits(clientScript, bindings, params),
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
runtime: { id, params: paramsLiteral, args: argsLiteral, emitted },
|
|
227
|
+
awaits,
|
|
228
|
+
leftAsWritten: false,
|
|
229
|
+
diagnostics: [],
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Whether host code at `node` may `await`: in an async function, or at a
|
|
234
|
+
// module's top level.
|
|
235
|
+
function mayAwait(ts: typeof import("typescript"), node: ts.Node): boolean {
|
|
236
|
+
for (let parent = node.parent; parent; parent = parent.parent) {
|
|
237
|
+
if (ts.isFunctionLike(parent)) {
|
|
238
|
+
return (
|
|
239
|
+
ts
|
|
240
|
+
.getModifiers(parent as ts.FunctionLikeDeclaration)
|
|
241
|
+
?.some((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword) ??
|
|
242
|
+
false
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// An `await` in host code, but not one inside a function it holds, which is
|
|
250
|
+
// that function's own, nor in a nested script, which is checked as its own.
|
|
251
|
+
function hasAwait(ts: typeof import("typescript"), node: ts.Node): boolean {
|
|
252
|
+
if (ts.isAwaitExpression(node)) {
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
255
|
+
if (ts.isFunctionLike(node) || ts.isTaggedTemplateExpression(node)) {
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
return ts.forEachChild(node, (child) => hasAwait(ts, child)) ?? false;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Each `${…}` that isn't a splice: written in text (JSX text, a string, a
|
|
262
|
+
// comment) rather than code, where nothing reads its placeholder.
|
|
263
|
+
function unsplicedSpans(
|
|
264
|
+
ts: typeof import("typescript"),
|
|
265
|
+
{ sourceFile, sourceNode, splices }: ClientScript,
|
|
266
|
+
): Diagnostic[] {
|
|
267
|
+
const spans = ts.isTemplateExpression(sourceNode.template)
|
|
268
|
+
? sourceNode.template.templateSpans
|
|
269
|
+
: [];
|
|
270
|
+
return spans
|
|
271
|
+
.filter((_, index) => splices[`$0splice${index}`] === undefined)
|
|
272
|
+
.map((span) => ({
|
|
273
|
+
range: {
|
|
274
|
+
start: span.expression.getFullStart() - 2, // at `${`
|
|
275
|
+
end: span.literal.getStart(sourceFile) + 1, // past `}`
|
|
276
|
+
},
|
|
277
|
+
message:
|
|
278
|
+
"This `${…}` is in text, not code, so it isn't spliced. As an " +
|
|
279
|
+
"element's child, write it in braces: `{${…}}`.",
|
|
280
|
+
category: ts.DiagnosticCategory.Error,
|
|
281
|
+
code: 0,
|
|
282
|
+
}));
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// What a script may not write though TypeScript would read it: a name it
|
|
286
|
+
// declares where it can't, a tag naming what it doesn't declare, and a splice
|
|
287
|
+
// it can't spell unbraced.
|
|
288
|
+
function refusals(
|
|
289
|
+
ts: typeof import("typescript"),
|
|
290
|
+
script: ClientScript,
|
|
291
|
+
bindings: BindingResolution,
|
|
292
|
+
): Diagnostic[] {
|
|
293
|
+
const diagnostics: Diagnostic[] = [];
|
|
294
|
+
const refuse = (node: ts.Node, message: string): void => {
|
|
295
|
+
diagnostics.push({
|
|
296
|
+
range: script.toSourceRange(node),
|
|
297
|
+
message,
|
|
298
|
+
category: ts.DiagnosticCategory.Error,
|
|
299
|
+
code: 0,
|
|
300
|
+
});
|
|
301
|
+
};
|
|
302
|
+
const visit = (node: ts.Node): void => {
|
|
303
|
+
const name =
|
|
304
|
+
(ts.isVariableDeclaration(node) ||
|
|
305
|
+
ts.isBindingElement(node) ||
|
|
306
|
+
ts.isParameter(node) ||
|
|
307
|
+
ts.isFunctionDeclaration(node) ||
|
|
308
|
+
ts.isClassDeclaration(node)) &&
|
|
309
|
+
node.name !== undefined &&
|
|
310
|
+
ts.isIdentifier(node.name)
|
|
311
|
+
? node.name
|
|
312
|
+
: undefined;
|
|
313
|
+
if (name !== undefined) {
|
|
314
|
+
// `$`-prefixed names splice host bindings, so a client script can't
|
|
315
|
+
// declare one: the declaration would shadow the shorthand.
|
|
316
|
+
if (name.text.startsWith("$")) {
|
|
317
|
+
refuse(
|
|
318
|
+
name,
|
|
319
|
+
"`$`-prefixed names are reserved for unbraced splices in a `cs` " +
|
|
320
|
+
"client script.",
|
|
321
|
+
);
|
|
322
|
+
} else {
|
|
323
|
+
// Checked here rather than left to TypeScript: the virtual code binds
|
|
324
|
+
// renamed names, so its strict-mode checks never see these, and
|
|
325
|
+
// without strict mode it wouldn't check them at all.
|
|
326
|
+
const reason = strictReason(name.text);
|
|
327
|
+
if (reason !== null) {
|
|
328
|
+
refuse(
|
|
329
|
+
name,
|
|
330
|
+
`\`${name.text}\` is not allowed as a ${
|
|
331
|
+
ts.isParameter(node) ? "parameter" : "variable declaration"
|
|
332
|
+
} name: ${reason}.`,
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
// A component tag is the script's own or a host value spliced, `<$Card>`:
|
|
338
|
+
// the virtual code is checked in the host file, where an unmarked `<Card>`
|
|
339
|
+
// would quietly read the host's.
|
|
340
|
+
if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) {
|
|
341
|
+
// What the tag starts with: `Card`, or `ui` in `<ui.Card>`.
|
|
342
|
+
const root = tagRoot(ts, node.tagName);
|
|
343
|
+
const written = node.tagName.getText(script.fileWithPlaceholders);
|
|
344
|
+
if (root?.text.startsWith("$0splice")) {
|
|
345
|
+
refuse(
|
|
346
|
+
root,
|
|
347
|
+
"A tag splices a host value by its name, e.g. `<$Card>`, not " +
|
|
348
|
+
"with `${…}`.",
|
|
349
|
+
);
|
|
350
|
+
} else if (
|
|
351
|
+
root !== undefined &&
|
|
352
|
+
!root.text.startsWith("$") &&
|
|
353
|
+
// a name alone is a component's where it starts uppercase; a member
|
|
354
|
+
// of anything is one
|
|
355
|
+
(root !== node.tagName || isComponentTag(root.text)) &&
|
|
356
|
+
!bindings.has(root)
|
|
357
|
+
) {
|
|
358
|
+
refuse(
|
|
359
|
+
root,
|
|
360
|
+
`\`<${written}>\` names nothing this script declares. A host value ` +
|
|
361
|
+
`used as a tag is spliced: \`<$${written}>\`.`,
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
node.forEachChild(visit);
|
|
366
|
+
};
|
|
367
|
+
visit(script.fileWithPlaceholders);
|
|
368
|
+
// A `$`-prefixed host binding has no shorthand: `$$x` stacks sigils
|
|
369
|
+
// unreadably, so it splices braced.
|
|
370
|
+
for (const splice of Object.values(script.splices)) {
|
|
371
|
+
if (splice.kind === "unbraced" && splice.expression.text.startsWith("$")) {
|
|
372
|
+
for (const ref of splice.refs) {
|
|
373
|
+
refuse(
|
|
374
|
+
ref,
|
|
375
|
+
`Can't splice \`${splice.expression.text}\` unbraced: a ` +
|
|
376
|
+
"`$`-prefixed host binding splices with braces, e.g. " +
|
|
377
|
+
`\`\${${splice.expression.text}}\`.`,
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return diagnostics;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Why strict mode forbids binding a name, or null where it doesn't.
|
|
386
|
+
function strictReason(name: string): string | null {
|
|
387
|
+
switch (name) {
|
|
388
|
+
case "eval":
|
|
389
|
+
case "arguments":
|
|
390
|
+
return "strict mode forbids binding it";
|
|
391
|
+
case "let":
|
|
392
|
+
case "static":
|
|
393
|
+
case "yield":
|
|
394
|
+
case "implements":
|
|
395
|
+
case "interface":
|
|
396
|
+
case "package":
|
|
397
|
+
case "private":
|
|
398
|
+
case "protected":
|
|
399
|
+
case "public":
|
|
400
|
+
return "it is reserved in strict mode";
|
|
401
|
+
case "await":
|
|
402
|
+
return "it is reserved in module code";
|
|
403
|
+
default:
|
|
404
|
+
return null;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// A file's parse errors. Internal to TypeScript, which reports them only
|
|
409
|
+
// through a program; a script has no program of its own.
|
|
410
|
+
function parseDiagnostics(file: ts.SourceFile): readonly ts.Diagnostic[] {
|
|
411
|
+
return (file as unknown as { parseDiagnostics: ts.Diagnostic[] })
|
|
412
|
+
.parseDiagnostics;
|
|
413
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { CodeInformation } from "./CodeInformation.js";
|
|
2
|
+
|
|
3
|
+
export type Segment =
|
|
4
|
+
| string
|
|
5
|
+
| [
|
|
6
|
+
text: string,
|
|
7
|
+
source: string | undefined,
|
|
8
|
+
sourceOffset: number,
|
|
9
|
+
length: number,
|
|
10
|
+
// the editor behavior of the mapped node the text was attributed to
|
|
11
|
+
data?: CodeInformation,
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
export function segmentsToString<T extends Segment>(segments: T[]) {
|
|
15
|
+
return segments.map((s) => (typeof s === "string" ? s : s[0])).join("");
|
|
16
|
+
}
|
package/src/tagRoot.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type ts from "typescript";
|
|
2
|
+
|
|
3
|
+
// The name a tag starts with: `Card` in `<Card>`, `Animated` in
|
|
4
|
+
// `<Animated.View>`; none where it starts with `this`.
|
|
5
|
+
export function tagRoot(
|
|
6
|
+
ts: typeof import("typescript"),
|
|
7
|
+
tagName: ts.JsxTagNameExpression,
|
|
8
|
+
): ts.Identifier | undefined {
|
|
9
|
+
let at: ts.Node = tagName;
|
|
10
|
+
while (ts.isPropertyAccessExpression(at)) {
|
|
11
|
+
at = at.expression;
|
|
12
|
+
}
|
|
13
|
+
return ts.isIdentifier(at) ? at : undefined;
|
|
14
|
+
}
|
package/src/transform.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type ts from "typescript";
|
|
2
|
+
import { compileScript, type Plugin } from "./compileScript.js";
|
|
3
|
+
import { parseSourceFile } from "./parseFile.js";
|
|
4
|
+
import { rewriteFile } from "./rewriteFile.js";
|
|
5
|
+
import { createCall, moduleDeclaration } from "./rewriteScript.js";
|
|
6
|
+
|
|
7
|
+
export interface TransformOptions {
|
|
8
|
+
/**
|
|
9
|
+
* What a script's source map names its host file, given TypeScript's name for
|
|
10
|
+
* it: for a build whose file names are paths on its own machine. By default,
|
|
11
|
+
* TypeScript's name.
|
|
12
|
+
*/
|
|
13
|
+
readonly sourceName?: (fileName: string) => string;
|
|
14
|
+
/**
|
|
15
|
+
* The framework's compile steps, run in series over each script: Solid's
|
|
16
|
+
* adapter's `solid()`. None by default, which leaves a script's JSX for a
|
|
17
|
+
* bundle's own plugins.
|
|
18
|
+
*/
|
|
19
|
+
readonly plugins?: readonly Plugin[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function transform(
|
|
23
|
+
ts: typeof import("typescript"),
|
|
24
|
+
addDiagnostic?: (diagnostic: ts.Diagnostic) => void,
|
|
25
|
+
{ sourceName = (fileName) => fileName, plugins = [] }: TransformOptions = {},
|
|
26
|
+
): ts.TransformerFactory<ts.SourceFile> {
|
|
27
|
+
return (context) => (sourceFile) => {
|
|
28
|
+
const parsedFile = parseSourceFile(ts, sourceFile);
|
|
29
|
+
if (parsedFile.scripts.length === 0) {
|
|
30
|
+
return sourceFile;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const rewrittenFile = rewriteFile(ts, parsedFile);
|
|
34
|
+
|
|
35
|
+
if (addDiagnostic) {
|
|
36
|
+
for (const diagnostic of rewrittenFile.diagnostics) {
|
|
37
|
+
addDiagnostic({
|
|
38
|
+
file: sourceFile,
|
|
39
|
+
start: diagnostic.range.start,
|
|
40
|
+
length: diagnostic.range.end - diagnostic.range.start,
|
|
41
|
+
messageText: diagnostic.message,
|
|
42
|
+
category: diagnostic.category,
|
|
43
|
+
code: diagnostic.code,
|
|
44
|
+
source: "backtick",
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Each script as the client gets it, its module declared once for every
|
|
50
|
+
// run of it; one that didn't parse is left as written.
|
|
51
|
+
const name = sourceName(sourceFile.fileName);
|
|
52
|
+
const modules: ts.Statement[] = [];
|
|
53
|
+
const byStart = new Map<number, ts.Node>();
|
|
54
|
+
for (const [template, { runtime }] of rewrittenFile.scripts) {
|
|
55
|
+
if (runtime !== null) {
|
|
56
|
+
const compiled = compileScript(ts, runtime.emitted, name, plugins);
|
|
57
|
+
const moduleName = `$module${modules.length}`;
|
|
58
|
+
modules.push(moduleDeclaration(ts, moduleName, runtime, compiled));
|
|
59
|
+
byStart.set(
|
|
60
|
+
template.getStart(rewrittenFile.sourceFile),
|
|
61
|
+
createCall(ts, moduleName, runtime),
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const visit: ts.Visitor = (node) => {
|
|
67
|
+
if (
|
|
68
|
+
ts.isTaggedTemplateExpression(node) &&
|
|
69
|
+
ts.isIdentifier(node.tag) &&
|
|
70
|
+
node.tag.text === "cs" &&
|
|
71
|
+
node.pos >= 0
|
|
72
|
+
) {
|
|
73
|
+
const runtime = byStart.get(node.getStart(rewrittenFile.sourceFile));
|
|
74
|
+
if (runtime) {
|
|
75
|
+
return ts.visitEachChild(runtime, visit, context);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return ts.visitEachChild(node, visit, context);
|
|
79
|
+
};
|
|
80
|
+
const visited = ts.visitNode(
|
|
81
|
+
sourceFile,
|
|
82
|
+
visit,
|
|
83
|
+
ts.isSourceFile,
|
|
84
|
+
) as ts.SourceFile;
|
|
85
|
+
// After the imports, before anything that could run a script.
|
|
86
|
+
const imports = visited.statements.findIndex(
|
|
87
|
+
(statement) => !ts.isImportDeclaration(statement),
|
|
88
|
+
);
|
|
89
|
+
const at = imports === -1 ? visited.statements.length : imports;
|
|
90
|
+
return context.factory.updateSourceFile(visited, [
|
|
91
|
+
...visited.statements.slice(0, at),
|
|
92
|
+
...modules,
|
|
93
|
+
...visited.statements.slice(at),
|
|
94
|
+
]);
|
|
95
|
+
};
|
|
96
|
+
}
|
package/src/transpile.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type ts from "typescript";
|
|
2
|
+
import { type TransformOptions, transform } from "./transform.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* One standalone file, run through the transform.
|
|
6
|
+
*
|
|
7
|
+
* The options are here because a file standing alone has nobody to ask for
|
|
8
|
+
* them. Everywhere a project is being built there is a `tsconfig.json` and the
|
|
9
|
+
* `tsc` and Bun plugins read the user's; this is the other case — one source,
|
|
10
|
+
* no project — which is what both an e2e fixture and a playground example are.
|
|
11
|
+
* They have to be compiled the same way for either to say anything about the
|
|
12
|
+
* other, and one exported function is how that stays true.
|
|
13
|
+
*
|
|
14
|
+
* `verbatimModuleSyntax` is the load-bearing option. Without it TypeScript
|
|
15
|
+
* elides an import the source does not appear to use — `state`, where the
|
|
16
|
+
* source says `$state` inside a template — while the `params` the transform
|
|
17
|
+
* writes still names it, and what comes out throws `state is not defined`.
|
|
18
|
+
*
|
|
19
|
+
* What comes back is a module: the imports are still imports, because the
|
|
20
|
+
* transform writes references to them that TypeScript has not seen and so
|
|
21
|
+
* cannot rewrite. A caller that needs something runnable without a module
|
|
22
|
+
* loader transpiles this again, to `require`/`exports`, in a second pass.
|
|
23
|
+
*
|
|
24
|
+
* `jsxImportSource` is the caller's, and the only one that is: which target a
|
|
25
|
+
* file draws with is not something a compiler knows, and a default here would
|
|
26
|
+
* be this package naming one target's package for every file it ever sees.
|
|
27
|
+
*/
|
|
28
|
+
export function transpile(
|
|
29
|
+
ts: typeof import("typescript"),
|
|
30
|
+
fileName: string,
|
|
31
|
+
sourceText: string,
|
|
32
|
+
jsxImportSource: string,
|
|
33
|
+
addDiagnostic?: (diagnostic: ts.Diagnostic) => void,
|
|
34
|
+
options?: TransformOptions,
|
|
35
|
+
): string {
|
|
36
|
+
const { outputText } = ts.transpileModule(sourceText, {
|
|
37
|
+
fileName,
|
|
38
|
+
compilerOptions: {
|
|
39
|
+
target: ts.ScriptTarget.ESNext,
|
|
40
|
+
module: ts.ModuleKind.ESNext,
|
|
41
|
+
jsx: ts.JsxEmit.ReactJSX,
|
|
42
|
+
jsxImportSource,
|
|
43
|
+
sourceMap: false,
|
|
44
|
+
verbatimModuleSyntax: true,
|
|
45
|
+
},
|
|
46
|
+
transformers: { before: [transform(ts, addDiagnostic, options)] },
|
|
47
|
+
});
|
|
48
|
+
return outputText;
|
|
49
|
+
}
|
package/src/unmangle.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// The prefix the compiler adds to client-script identifiers when virtualizing.
|
|
2
|
+
const CS_PREFIX = "__cs_";
|
|
3
|
+
|
|
4
|
+
export function mangle(identifier: string): string {
|
|
5
|
+
return `${CS_PREFIX}${identifier}`;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function unmangle(text: string): string {
|
|
9
|
+
return text.replaceAll(CS_PREFIX, "");
|
|
10
|
+
}
|