@vesk/compiler 0.2.50-canary.9 → 0.2.50
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/abuse-checks.d.ts +7 -0
- package/dist/abuse-checks.d.ts.map +1 -0
- package/dist/abuse-checks.js +315 -0
- package/dist/errors.d.ts +39 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +78 -1
- package/dist/ir-generator.js +2 -2
- package/dist/scan.d.ts +15 -0
- package/dist/scan.d.ts.map +1 -1
- package/dist/scan.js +64 -0
- package/dist/server-jsgen.d.ts.map +1 -1
- package/dist/server-jsgen.js +167 -19
- package/dist/server-jsx-lowering.d.ts +16 -0
- package/dist/server-jsx-lowering.d.ts.map +1 -0
- package/dist/server-jsx-lowering.js +226 -0
- package/package.json +2 -2
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type ComponentIR } from '@vesk/compiler/src/ir';
|
|
2
|
+
/**
|
|
3
|
+
* Run the abuse checks for one component. Called after its body is built, so
|
|
4
|
+
* the tracked names are known.
|
|
5
|
+
*/
|
|
6
|
+
export declare function validateAbuse(comp: ComponentIR, file?: string, source?: string): void;
|
|
7
|
+
//# sourceMappingURL=abuse-checks.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"abuse-checks.d.ts","sourceRoot":"","sources":["../src/abuse-checks.ts"],"names":[],"mappings":"AA6BA,OAAO,EAQL,KAAK,WAAW,EACjB,MAAM,uBAAuB,CAAC;AAsS/B;;;GAGG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,WAAW,EAAE,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAMrF"}
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import { walk } from 'zimmerframe';
|
|
2
|
+
import { ClientBlock, DynamicBinding, OpaqueDynamicRegion, RuntimeStatement, ServerBlock, TrackDecl, StaticNode, } from '@vesk/compiler/src/ir';
|
|
3
|
+
import { VeskError, codeFrame } from '@vesk/compiler/src/errors';
|
|
4
|
+
/** Offset -> 1-based line/column, for a code frame in a diagnostic. */
|
|
5
|
+
function offsetToLineCol(source, offset) {
|
|
6
|
+
let line = 1;
|
|
7
|
+
let lineStart = 0;
|
|
8
|
+
for (let i = 0; i < offset && i < source.length; i++) {
|
|
9
|
+
if (source[i] === '\n') {
|
|
10
|
+
line++;
|
|
11
|
+
lineStart = i + 1;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return { line, column: offset - lineStart + 1 };
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Browser-only globals, with what to reach for instead. `navigator` and
|
|
18
|
+
* `location` are the dangerous ones: they do not throw on the server, they
|
|
19
|
+
* return undefined and the page renders wrong.
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* READS only. A bare read of one of these at statement level is unambiguous:
|
|
23
|
+
* it executes during SSR, either throwing (`window`) or silently returning the
|
|
24
|
+
* wrong thing (`navigator` exists in modern Node, so `navigator.onLine` renders
|
|
25
|
+
* as offline). Calls are NOT in this list: `setInterval`/`addEventListener`
|
|
26
|
+
* inside an effect or a hoisted statement are indistinguishable from a body
|
|
27
|
+
* statement at this point in the walk, and a diagnostic that fires on correct
|
|
28
|
+
* code is worse than none. Timer/listener misuse is a documented follow-up.
|
|
29
|
+
*/
|
|
30
|
+
const SSR_UNSAFE = {
|
|
31
|
+
window: 'the DOM',
|
|
32
|
+
document: 'the DOM',
|
|
33
|
+
localStorage: 'storage',
|
|
34
|
+
sessionStorage: 'storage',
|
|
35
|
+
navigator: 'the Network Information API (see getNetworkState)',
|
|
36
|
+
location: 'routing (see usePathname)',
|
|
37
|
+
history: 'routing (see useNavigate)',
|
|
38
|
+
matchMedia: 'a CSS media query',
|
|
39
|
+
};
|
|
40
|
+
function at(node, ctx) {
|
|
41
|
+
const opts = { inComponent: ctx.compName };
|
|
42
|
+
if (ctx.file)
|
|
43
|
+
opts.file = ctx.file;
|
|
44
|
+
if (ctx.source && typeof node.start === 'number') {
|
|
45
|
+
const { line, column } = offsetToLineCol(ctx.source, node.start);
|
|
46
|
+
opts.line = line;
|
|
47
|
+
opts.column = column;
|
|
48
|
+
opts.frame = codeFrame(ctx.source, line, column);
|
|
49
|
+
}
|
|
50
|
+
return opts;
|
|
51
|
+
}
|
|
52
|
+
/** Walk an expression, reporting SSR-unsafe globals and cell misuse. */
|
|
53
|
+
/** Calls whose bodies are emitted on the client only, so the DOM is legal there. */
|
|
54
|
+
const CLIENT_ONLY_CALLS = new Set(['on_destroy', 'effect', 'pre_effect', 'user_effect']);
|
|
55
|
+
/**
|
|
56
|
+
* The global named by a `typeof x !== 'undefined'` / `=== 'undefined'` test, or
|
|
57
|
+
* null. `&&` / `||` short-circuit, so the other side of the expression only runs
|
|
58
|
+
* when the global exists.
|
|
59
|
+
*/
|
|
60
|
+
function typeofGlobalIn(node) {
|
|
61
|
+
const isTypeofOf = (n) => n && n.type === 'UnaryExpression' && n.operator === 'typeof' && n.argument?.type === 'Identifier'
|
|
62
|
+
? String(n.argument.name)
|
|
63
|
+
: null;
|
|
64
|
+
if (!node)
|
|
65
|
+
return null;
|
|
66
|
+
const pair = (a, b) => {
|
|
67
|
+
const fromA = isTypeofOf(a);
|
|
68
|
+
if (fromA && b?.type === 'BinaryExpression' && b.operator === '!==')
|
|
69
|
+
return fromA;
|
|
70
|
+
const fromB = isTypeofOf(b);
|
|
71
|
+
if (fromB && a?.type === 'BinaryExpression' && a.operator === '!==')
|
|
72
|
+
return fromB;
|
|
73
|
+
return null;
|
|
74
|
+
};
|
|
75
|
+
if (node.type === 'LogicalExpression')
|
|
76
|
+
return pair(node.left, node.right);
|
|
77
|
+
if (node.type === 'ConditionalExpression') {
|
|
78
|
+
return pair(node.test, node.consequent) ?? pair(node.test, node.alternate);
|
|
79
|
+
}
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* The global a subexpression tests for existence with `typeof`, whether the test
|
|
84
|
+
* stands alone (`typeof x !== 'undefined'`), guards an `&&` chain, or selects a
|
|
85
|
+
* conditional branch. Anything evaluated only when that test passed may read the
|
|
86
|
+
* global safely.
|
|
87
|
+
*/
|
|
88
|
+
function guardNameOf(node) {
|
|
89
|
+
if (!node)
|
|
90
|
+
return null;
|
|
91
|
+
const direct = typeofGlobalIn(node);
|
|
92
|
+
if (direct)
|
|
93
|
+
return direct;
|
|
94
|
+
if (node.type === 'LogicalExpression')
|
|
95
|
+
return guardNameOf(node.left);
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Does this statement set up a client-only lifecycle call anywhere?
|
|
100
|
+
*
|
|
101
|
+
* `effect(() => { const h = setInterval(…); on_destroy(() => clearInterval(h)) })`
|
|
102
|
+
* can arrive as ONE hoisted statement, so a statement that mentions a
|
|
103
|
+
* client-only call is treated as client-only. That errs toward silence rather
|
|
104
|
+
* than a false positive, which is the right direction for a new diagnostic.
|
|
105
|
+
*/
|
|
106
|
+
function mentionsClientOnlyCall(ast) {
|
|
107
|
+
if (!ast)
|
|
108
|
+
return false;
|
|
109
|
+
let found = false;
|
|
110
|
+
try {
|
|
111
|
+
walk(ast, null, {
|
|
112
|
+
CallExpression(node) {
|
|
113
|
+
const callee = node.callee;
|
|
114
|
+
if (callee && callee.type === 'Identifier' && CLIENT_ONLY_CALLS.has(String(callee.name))) {
|
|
115
|
+
found = true;
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
return;
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
return found;
|
|
126
|
+
}
|
|
127
|
+
function checkExpression(ast, ctx, insideHandler) {
|
|
128
|
+
if (!ast)
|
|
129
|
+
return;
|
|
130
|
+
walk(ast, null, {
|
|
131
|
+
ConditionalExpression(node, context) {
|
|
132
|
+
// `test ? a : typeof x !== 'undefined' && x.use()` — the chosen branch is
|
|
133
|
+
// guarded, so a read of that global there is safe.
|
|
134
|
+
const guarded = guardNameOf(node.alternate) ?? guardNameOf(node.consequent);
|
|
135
|
+
if (!guarded)
|
|
136
|
+
return context.next();
|
|
137
|
+
ctx.guardedGlobals = new Set([...(ctx.guardedGlobals || []), guarded]);
|
|
138
|
+
return context.next();
|
|
139
|
+
},
|
|
140
|
+
LogicalExpression(node, context) {
|
|
141
|
+
// `typeof document !== 'undefined' && !!document.querySelector(…)` is the
|
|
142
|
+
// standard client-only guard, and the right side never evaluates on the
|
|
143
|
+
// server. Honour the short-circuit instead of flagging correct code (the
|
|
144
|
+
// docs app's error boundary uses exactly this shape).
|
|
145
|
+
const guarded = guardNameOf(node.left);
|
|
146
|
+
if (guarded) {
|
|
147
|
+
ctx.guardedGlobals = new Set([...(ctx.guardedGlobals || []), guarded]);
|
|
148
|
+
return context.next();
|
|
149
|
+
}
|
|
150
|
+
return context.next();
|
|
151
|
+
},
|
|
152
|
+
UnaryExpression(node, context) {
|
|
153
|
+
// `typeof window !== 'undefined'` is the CORRECT guard for a client-only
|
|
154
|
+
// value — the test-app's own broken-component fixture uses it. A `typeof`
|
|
155
|
+
// test never reads the value, so it must not be flagged.
|
|
156
|
+
if (node.operator === 'typeof')
|
|
157
|
+
return;
|
|
158
|
+
return context.next();
|
|
159
|
+
},
|
|
160
|
+
ArrowFunctionExpression(node, context) {
|
|
161
|
+
// A function body runs when it is CALLED, not when the render creates it.
|
|
162
|
+
// `const probe = () => document.querySelector(…)` called from a handler is
|
|
163
|
+
// correct code, so the walk stops here.
|
|
164
|
+
return;
|
|
165
|
+
},
|
|
166
|
+
FunctionExpression(node, context) {
|
|
167
|
+
return;
|
|
168
|
+
},
|
|
169
|
+
Identifier(node, context) {
|
|
170
|
+
if ((ctx.clientOnlyDepth || 0) > 0)
|
|
171
|
+
return context.next();
|
|
172
|
+
const parent = context.path.at(-1);
|
|
173
|
+
// A member's PROPERTY is a name, not a global: `foo.window` is fine.
|
|
174
|
+
if (parent && parent.type === 'MemberExpression' && parent.property === node && !parent.computed)
|
|
175
|
+
return context.next();
|
|
176
|
+
if (parent && parent.type === 'Property' && parent.key === node && !parent.computed && !parent.shorthand)
|
|
177
|
+
return context.next();
|
|
178
|
+
const name = String(node.name);
|
|
179
|
+
if (insideHandler)
|
|
180
|
+
return context.next();
|
|
181
|
+
if (ctx.guardedGlobals?.has(name))
|
|
182
|
+
return context.next();
|
|
183
|
+
if (name in SSR_UNSAFE) {
|
|
184
|
+
throw VeskError.ssrUnsafeGlobal(name, at(node, ctx));
|
|
185
|
+
}
|
|
186
|
+
return context.next();
|
|
187
|
+
},
|
|
188
|
+
CallExpression(node, context) {
|
|
189
|
+
const callee = node.callee;
|
|
190
|
+
// Depth is carried on the context object, not a module global: a global is
|
|
191
|
+
// never decremented, so one on_destroy call would silently disable every
|
|
192
|
+
// later check in the file.
|
|
193
|
+
const insideClientOnly = (ctx.clientOnlyDepth || 0) > 0;
|
|
194
|
+
if (callee && callee.type === 'Identifier') {
|
|
195
|
+
const name = String(callee.name);
|
|
196
|
+
if (insideHandler || insideClientOnly)
|
|
197
|
+
return context.next();
|
|
198
|
+
// on_destroy/effect bodies are emitted on the client only, so anything
|
|
199
|
+
// they touch is legal — and `context.skip()` keeps the walk out.
|
|
200
|
+
if (CLIENT_ONLY_CALLS.has(name)) {
|
|
201
|
+
ctx.clientOnlyDepth = (ctx.clientOnlyDepth || 0) + 1;
|
|
202
|
+
context.next();
|
|
203
|
+
ctx.clientOnlyDepth -= 1;
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (name in SSR_UNSAFE)
|
|
207
|
+
throw VeskError.ssrUnsafeGlobal(name, at(callee, ctx));
|
|
208
|
+
}
|
|
209
|
+
// Anything else (a nested arrow, a call argument) is still evaluated on
|
|
210
|
+
// the server when it runs in the body — unless we are inside a
|
|
211
|
+
// client-only call.
|
|
212
|
+
if (!insideHandler && !insideClientOnly)
|
|
213
|
+
checkExpression(callee ?? null, ctx, false);
|
|
214
|
+
return context.next();
|
|
215
|
+
},
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
/** The nodes whose expressions the server evaluates. */
|
|
219
|
+
function checkNode(node, ctx, insideHandler = false) {
|
|
220
|
+
if (!node || typeof node !== 'object')
|
|
221
|
+
return;
|
|
222
|
+
// Guarded globals are scoped to one top-level statement.
|
|
223
|
+
if (insideHandler === false && node instanceof RuntimeStatement)
|
|
224
|
+
ctx.guardedGlobals = new Set();
|
|
225
|
+
// `{#client}` never runs on the server; `{#server}` DOES (that is its whole
|
|
226
|
+
// purpose), so it is checked like any other body position.
|
|
227
|
+
if (node instanceof ClientBlock)
|
|
228
|
+
return;
|
|
229
|
+
if (node instanceof ServerBlock) {
|
|
230
|
+
for (const child of (node.children
|
|
231
|
+
|| node.body
|
|
232
|
+
|| [])) {
|
|
233
|
+
checkNode(child, ctx);
|
|
234
|
+
}
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
if (node instanceof RuntimeStatement) {
|
|
238
|
+
if (!insideHandler && !mentionsClientOnlyCall(node.ast))
|
|
239
|
+
checkExpression(node.ast, ctx, false);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
if (node instanceof TrackDecl) {
|
|
243
|
+
// `track(...)` is safe in the body; a track() inside a nested function is not,
|
|
244
|
+
// but that is a different failure (a raw syntax error) and the parser owns it.
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
if (node instanceof DynamicBinding) {
|
|
248
|
+
if (insideHandler)
|
|
249
|
+
return;
|
|
250
|
+
// An event handler is client-only by definition: it is dispatched by the
|
|
251
|
+
// browser, so `document.title = …` there is correct, not a bug.
|
|
252
|
+
const binding = node;
|
|
253
|
+
const target = String(binding.target || '');
|
|
254
|
+
if (target.startsWith('on') || binding.kind === 'event')
|
|
255
|
+
return;
|
|
256
|
+
checkExpression(node.expression?.ast ?? null, ctx, false);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
if (node instanceof OpaqueDynamicRegion) {
|
|
260
|
+
checkExpression(node.condition?.ast ?? null, ctx, false);
|
|
261
|
+
for (const child of (node.consequentNodes || []))
|
|
262
|
+
checkNode(child, ctx);
|
|
263
|
+
for (const child of (node.alternateNodes || []))
|
|
264
|
+
checkNode(child, ctx);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
if (node instanceof StaticNode) {
|
|
268
|
+
for (const attr of node.attributes || [])
|
|
269
|
+
void attr;
|
|
270
|
+
for (const child of node.children || [])
|
|
271
|
+
checkNode(child, ctx);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
// ComponentCall / MapRegion / prop slots / anything else: recurse over children
|
|
275
|
+
// and check their expressions, still in body position.
|
|
276
|
+
const record = node;
|
|
277
|
+
if (record.expression && typeof record.expression === 'object') {
|
|
278
|
+
checkExpression(record.expression.ast ?? null, ctx, false);
|
|
279
|
+
}
|
|
280
|
+
for (const key of Object.keys(record)) {
|
|
281
|
+
const value = record[key];
|
|
282
|
+
if (Array.isArray(value))
|
|
283
|
+
for (const item of value)
|
|
284
|
+
checkNode(item, ctx);
|
|
285
|
+
else if (value && typeof value === 'object' && value.constructor?.name?.endsWith('Node')) {
|
|
286
|
+
checkNode(value, ctx);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
/** The cell/value halves of every `track()` declaration in a component. */
|
|
291
|
+
function collectCells(comp) {
|
|
292
|
+
const cells = new Map();
|
|
293
|
+
const values = new Map();
|
|
294
|
+
for (const node of comp.body) {
|
|
295
|
+
if (!(node instanceof TrackDecl))
|
|
296
|
+
continue;
|
|
297
|
+
const cellName = node.rawName || node.name;
|
|
298
|
+
if (node.rawName)
|
|
299
|
+
cells.set(node.rawName, { value: node.name });
|
|
300
|
+
values.set(node.name, { cell: cellName });
|
|
301
|
+
}
|
|
302
|
+
return { cells, values };
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Run the abuse checks for one component. Called after its body is built, so
|
|
306
|
+
* the tracked names are known.
|
|
307
|
+
*/
|
|
308
|
+
export function validateAbuse(comp, file, source) {
|
|
309
|
+
// A `client` island renders on the server too — SSR runs the same body — so
|
|
310
|
+
// the same rules apply. What differs is `{#server}`/`{#client}`.
|
|
311
|
+
const { cells, values } = collectCells(comp);
|
|
312
|
+
const ctx = { compName: comp.name, file, source, cells, values };
|
|
313
|
+
for (const node of comp.body)
|
|
314
|
+
checkNode(node, ctx);
|
|
315
|
+
}
|
package/dist/errors.d.ts
CHANGED
|
@@ -9,6 +9,8 @@ export interface VeskErrorOptions {
|
|
|
9
9
|
nextSteps?: string[];
|
|
10
10
|
tip?: string;
|
|
11
11
|
code?: string;
|
|
12
|
+
/** Rendered code frame (see `codeFrame`). Never a V-code. */
|
|
13
|
+
frame?: string;
|
|
12
14
|
[key: string]: unknown;
|
|
13
15
|
}
|
|
14
16
|
export declare class VeskError extends Error {
|
|
@@ -19,7 +21,10 @@ export declare class VeskError extends Error {
|
|
|
19
21
|
suggestions: string[];
|
|
20
22
|
nextSteps: string[];
|
|
21
23
|
tip: string;
|
|
24
|
+
/** The V-diagnostic code, e.g. `V0501`. */
|
|
22
25
|
code?: string;
|
|
26
|
+
/** The rendered code frame. Used to be written into `code`, which silently replaced the V-code. */
|
|
27
|
+
frame?: string;
|
|
23
28
|
constructor(message: string, opts?: VeskErrorOptions);
|
|
24
29
|
static notFound(name: string, candidates?: string[], context?: VeskErrorOptions): VeskError;
|
|
25
30
|
/**
|
|
@@ -37,6 +42,40 @@ export declare class VeskError extends Error {
|
|
|
37
42
|
form?: 'nested' | 'value';
|
|
38
43
|
}): VeskError;
|
|
39
44
|
static classDecl(context?: VeskErrorOptions): VeskError;
|
|
45
|
+
/**
|
|
46
|
+
* A browser-only global used in code the SERVER evaluates.
|
|
47
|
+
*
|
|
48
|
+
* On the server these are either absent (`window is not defined`) or, worse,
|
|
49
|
+
* silently present-but-wrong (`navigator` exists in modern Node, so
|
|
50
|
+
* `navigator.onLine` is `undefined` and a page renders as if it were offline).
|
|
51
|
+
* A raw `ReferenceError` at request time is the worst possible report: it
|
|
52
|
+
* names neither the component nor the fix. This points at the line.
|
|
53
|
+
*/
|
|
54
|
+
static ssrUnsafeGlobal(name: string, context?: Partial<VeskErrorOptions> & {
|
|
55
|
+
inComponent?: string;
|
|
56
|
+
}): VeskError;
|
|
57
|
+
/**
|
|
58
|
+
* The second name from `track()` called as a function.
|
|
59
|
+
*
|
|
60
|
+
* `const &[count, setCount] = track(0)` returns a CELL, not a setter function, so
|
|
61
|
+
* `setCount(1)` throws `setCount is not a function` from generated code with
|
|
62
|
+
* no line of user code in the message. The cell is the writer: `.set(v)`.
|
|
63
|
+
*/
|
|
64
|
+
static cellCalledAsFunction(name: string, context?: Partial<VeskErrorOptions> & {
|
|
65
|
+
value?: string;
|
|
66
|
+
cell?: string;
|
|
67
|
+
}): VeskError;
|
|
68
|
+
/**
|
|
69
|
+
* `set(value, …)` used outside a rewritten expression.
|
|
70
|
+
*
|
|
71
|
+
* The compiler rewrites `set(count, v)` to `countCell.set(v)` inside JSX and
|
|
72
|
+
* event handlers — but a bare statement in the component body is NOT
|
|
73
|
+
* rewritten, so the runtime `set` receives the value and throws
|
|
74
|
+
* "Cannot read properties of undefined". Pass the CELL, not the value.
|
|
75
|
+
*/
|
|
76
|
+
static setWithValueName(name: string, context?: Partial<VeskErrorOptions> & {
|
|
77
|
+
cell?: string;
|
|
78
|
+
}): VeskError;
|
|
40
79
|
static serverBlockInClient(compName: string, context?: VeskErrorOptions): VeskError;
|
|
41
80
|
static clientBlockInServer(compName: string, context?: VeskErrorOptions): VeskError;
|
|
42
81
|
static componentNotFound(name: string, available?: string[], context?: VeskErrorOptions): VeskError;
|
package/dist/errors.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,wBAAgB,WAAW,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAoBxD;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,EAAE,WAAW,GAAE,MAAU,GAAG,MAAM,GAAG,IAAI,CAWrG;AAqCD,wBAAgB,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,SAAI,EAAE,KAAK,SAAI,GAAG,MAAM,CAkBrG;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAgBD,qBAAa,SAAU,SAAQ,KAAK;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,wBAAgB,WAAW,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAoBxD;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,EAAE,WAAW,GAAE,MAAU,GAAG,MAAM,GAAG,IAAI,CAWrG;AAqCD,wBAAgB,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,SAAI,EAAE,KAAK,SAAI,GAAG,MAAM,CAkBrG;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,6DAA6D;IAC7D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAgBD,qBAAa,SAAU,SAAQ,KAAK;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,GAAG,EAAE,MAAM,CAAC;IACZ,2CAA2C;IAC3C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,mGAAmG;IACnG,KAAK,CAAC,EAAE,MAAM,CAAC;gBAEH,OAAO,EAAE,MAAM,EAAE,IAAI,GAAE,gBAAqB;IAgBxD,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,GAAE,MAAM,EAAO,EAAE,OAAO,GAAE,gBAAqB,GAAG,SAAS;IA8BnG;;;;;;;;;;OAUG;IACH,MAAM,CAAC,kBAAkB,CAAC,OAAO,GAAE,gBAAgB,GAAG;QAAE,IAAI,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAA;KAAO,GAAG,SAAS;IA6BpG,MAAM,CAAC,SAAS,CAAC,OAAO,GAAE,gBAAqB,GAAG,SAAS;IAoB3D;;;;;;;;OAQG;IACH,MAAM,CAAC,eAAe,CACpB,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE,OAAO,CAAC,gBAAgB,CAAC,GAAG;QAAE,WAAW,CAAC,EAAE,MAAM,CAAA;KAAO,GACjE,SAAS;IAqBZ;;;;;;OAMG;IACH,MAAM,CAAC,oBAAoB,CACzB,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE,OAAO,CAAC,gBAAgB,CAAC,GAAG;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAO,GAC1E,SAAS;IAmBZ;;;;;;;OAOG;IACH,MAAM,CAAC,gBAAgB,CACrB,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE,OAAO,CAAC,gBAAgB,CAAC,GAAG;QAAE,IAAI,CAAC,EAAE,MAAM,CAAA;KAAO,GAC1D,SAAS;IAkBZ,MAAM,CAAC,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,GAAE,gBAAqB,GAAG,SAAS;IAmBvF,MAAM,CAAC,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,GAAE,gBAAqB,GAAG,SAAS;IAmBvF,MAAM,CAAC,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,GAAE,MAAM,EAAO,EAAE,OAAO,GAAE,gBAAqB,GAAG,SAAS;IAO3G,MAAM,CAAC,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,YAAY,GAAE,MAAM,EAAO,EAAE,OAAO,GAAE,gBAAqB,GAAG,SAAS;IAYvG,MAAM,CAAC,sBAAsB,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,GAAE,gBAAqB,GAAG,SAAS;IAmB/G,MAAM,CAAC,cAAc,CAAC,OAAO,GAAE,gBAAgB,GAAG;QAAE,IAAI,CAAC,EAAE,MAAM,CAAA;KAAO,GAAG,SAAS;IAmBpF,QAAQ,IAAI,MAAM;IA0BlB,MAAM,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;CAclC"}
|
package/dist/errors.js
CHANGED
|
@@ -107,7 +107,10 @@ export class VeskError extends Error {
|
|
|
107
107
|
suggestions;
|
|
108
108
|
nextSteps;
|
|
109
109
|
tip;
|
|
110
|
+
/** The V-diagnostic code, e.g. `V0501`. */
|
|
110
111
|
code;
|
|
112
|
+
/** The rendered code frame. Used to be written into `code`, which silently replaced the V-code. */
|
|
113
|
+
frame;
|
|
111
114
|
constructor(message, opts = {}) {
|
|
112
115
|
super(message);
|
|
113
116
|
this.name = 'VeskError';
|
|
@@ -117,8 +120,13 @@ export class VeskError extends Error {
|
|
|
117
120
|
this.suggestions = opts.suggestions || [];
|
|
118
121
|
this.nextSteps = opts.nextSteps || [];
|
|
119
122
|
this.tip = opts.tip || '';
|
|
120
|
-
|
|
123
|
+
// The diagnostic code owns `code`. The code frame used to be written there
|
|
124
|
+
// too, which silently replaced `V0501` with a wall of source text — the
|
|
125
|
+
// report lost the one thing that identifies it.
|
|
126
|
+
if (opts.code !== undefined && !opts.code.includes('\n'))
|
|
121
127
|
this.code = opts.code;
|
|
128
|
+
if (opts.frame !== undefined)
|
|
129
|
+
this.frame = opts.frame;
|
|
122
130
|
}
|
|
123
131
|
static notFound(name, candidates = [], context = {}) {
|
|
124
132
|
const allCandidates = [...new Set([...candidates, ...VESK_BUILTINS])];
|
|
@@ -202,6 +210,75 @@ export class VeskError extends Error {
|
|
|
202
210
|
tip: 'Use plain objects for data and factory functions for constructors inside .vsk files.',
|
|
203
211
|
});
|
|
204
212
|
}
|
|
213
|
+
/**
|
|
214
|
+
* A browser-only global used in code the SERVER evaluates.
|
|
215
|
+
*
|
|
216
|
+
* On the server these are either absent (`window is not defined`) or, worse,
|
|
217
|
+
* silently present-but-wrong (`navigator` exists in modern Node, so
|
|
218
|
+
* `navigator.onLine` is `undefined` and a page renders as if it were offline).
|
|
219
|
+
* A raw `ReferenceError` at request time is the worst possible report: it
|
|
220
|
+
* names neither the component nor the fix. This points at the line.
|
|
221
|
+
*/
|
|
222
|
+
static ssrUnsafeGlobal(name, context = {}) {
|
|
223
|
+
return new VeskError(`\`${name}\` is browser-only, and this component body runs during SSR.`, {
|
|
224
|
+
...context,
|
|
225
|
+
code: context.code || 'V0501',
|
|
226
|
+
suggestions: [
|
|
227
|
+
`Wrap the part that needs the DOM in a {#client} block.`,
|
|
228
|
+
`Move the code into an event handler — handlers never run on the server.`,
|
|
229
|
+
`Do the setup in on_destroy(), which the compiler only emits on the client.`,
|
|
230
|
+
],
|
|
231
|
+
nextSteps: [
|
|
232
|
+
`component ${context.inComponent ?? 'this component'}: wrap the \`${name}\` access in {#client}...{/client}.`,
|
|
233
|
+
`Or guard it: if (typeof ${name} !== 'undefined') { ... }`,
|
|
234
|
+
],
|
|
235
|
+
tip: 'SSR renders this component before the client exists. Reading a browser global here either throws during the request or (for `navigator`) silently returns undefined.',
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* The second name from `track()` called as a function.
|
|
240
|
+
*
|
|
241
|
+
* `const &[count, setCount] = track(0)` returns a CELL, not a setter function, so
|
|
242
|
+
* `setCount(1)` throws `setCount is not a function` from generated code with
|
|
243
|
+
* no line of user code in the message. The cell is the writer: `.set(v)`.
|
|
244
|
+
*/
|
|
245
|
+
static cellCalledAsFunction(name, context = {}) {
|
|
246
|
+
return new VeskError(`\`${name}\` is a cell, not a function — cells are written with \`.set()\`.`, {
|
|
247
|
+
...context,
|
|
248
|
+
code: context.code || 'V0502',
|
|
249
|
+
suggestions: [
|
|
250
|
+
`Use \`${name}.set(...)\` instead of \`${name}(...)\`.`,
|
|
251
|
+
`Or use the tracked spelling inside an event handler: set(${context.value ?? 'value'}, ...)`,
|
|
252
|
+
],
|
|
253
|
+
nextSteps: [
|
|
254
|
+
`const &[${context.value ?? 'value'}, ${name}] = track(0) → \`${name}.set(v)\``,
|
|
255
|
+
'The value read in markup is the first name; the second is the cell object.',
|
|
256
|
+
],
|
|
257
|
+
tip: `\`track()\` returns [value, cell]. \`${name}\` is the cell half.`,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* `set(value, …)` used outside a rewritten expression.
|
|
262
|
+
*
|
|
263
|
+
* The compiler rewrites `set(count, v)` to `countCell.set(v)` inside JSX and
|
|
264
|
+
* event handlers — but a bare statement in the component body is NOT
|
|
265
|
+
* rewritten, so the runtime `set` receives the value and throws
|
|
266
|
+
* "Cannot read properties of undefined". Pass the CELL, not the value.
|
|
267
|
+
*/
|
|
268
|
+
static setWithValueName(name, context = {}) {
|
|
269
|
+
return new VeskError(`\`set(${name}, …)\` needs the CELL, and \`${name}\` is the value half of track().`, {
|
|
270
|
+
...context,
|
|
271
|
+
code: context.code || 'V0503',
|
|
272
|
+
suggestions: [
|
|
273
|
+
`Use \`set(${context.cell ?? 'cell'}, …)\` or \`${context.cell ?? 'cell'}.set(…)\`.`,
|
|
274
|
+
`Or move the set() into an event handler, where the compiler rewrites it for you.`,
|
|
275
|
+
],
|
|
276
|
+
nextSteps: [
|
|
277
|
+
`const &[${name}, ${context.cell ?? 'cell'}] = track(0) → \`${context.cell ?? 'cell'}.set(v)\``,
|
|
278
|
+
],
|
|
279
|
+
tip: 'set(cell, value) is only rewritten inside JSX and handlers; in a body statement it reaches the runtime untouched.',
|
|
280
|
+
});
|
|
281
|
+
}
|
|
205
282
|
static serverBlockInClient(compName, context = {}) {
|
|
206
283
|
return new VeskError(`{#server} block found in client island "${compName}". Client islands render on both server and client, so {#server} blocks have no effect.`, {
|
|
207
284
|
...context,
|
package/dist/ir-generator.js
CHANGED
|
@@ -1524,7 +1524,7 @@ function validateBlocks(compName, isClient, body, file, source) {
|
|
|
1524
1524
|
const { line, column } = offsetToLineCol(source, pos);
|
|
1525
1525
|
opts.line = line;
|
|
1526
1526
|
opts.column = column;
|
|
1527
|
-
opts.
|
|
1527
|
+
opts.frame = codeFrame(source, line, column);
|
|
1528
1528
|
}
|
|
1529
1529
|
throw VeskError.serverBlockInClient(compName, opts);
|
|
1530
1530
|
}
|
|
@@ -1537,7 +1537,7 @@ function validateBlocks(compName, isClient, body, file, source) {
|
|
|
1537
1537
|
const { line, column } = offsetToLineCol(source, pos);
|
|
1538
1538
|
opts.line = line;
|
|
1539
1539
|
opts.column = column;
|
|
1540
|
-
opts.
|
|
1540
|
+
opts.frame = codeFrame(source, line, column);
|
|
1541
1541
|
}
|
|
1542
1542
|
throw VeskError.clientBlockInServer(compName, opts);
|
|
1543
1543
|
}
|
package/dist/scan.d.ts
CHANGED
|
@@ -93,6 +93,21 @@ export declare function hasTopLevelComma(text: string): boolean;
|
|
|
93
93
|
* and `for (... in ...)` headers apart from classic `for` loops.
|
|
94
94
|
*/
|
|
95
95
|
export declare function containsForOfIn(text: string): boolean;
|
|
96
|
+
/**
|
|
97
|
+
* Reports whether `ident` appears in `text` as a CALLED identifier — i.e.
|
|
98
|
+
* followed by an optional TS generic clause and then `(`.
|
|
99
|
+
*
|
|
100
|
+
* This is a character-loop tokenizer, not a text match: it steps over
|
|
101
|
+
* whitespace, `//` and block comments, and quoted / template strings, so
|
|
102
|
+
* `track(` appearing inside a string, a comment or a property access
|
|
103
|
+
* (`obj.track(...)`) never counts. Template literals are skipped whole —
|
|
104
|
+
* including their `${...}` holes — because a `track(` in an interpolation is
|
|
105
|
+
* not a reliable enough signal to inject a runtime binding for.
|
|
106
|
+
*
|
|
107
|
+
* Used to decide which runtime helpers a raw statement needs without
|
|
108
|
+
* requiring the caller to have been lowered into a TrackDecl first.
|
|
109
|
+
*/
|
|
110
|
+
export declare function callsIdentifier(text: string, ident: string): boolean;
|
|
96
111
|
/**
|
|
97
112
|
* Extracts the argument text of a `track<...>(...)` call from an init
|
|
98
113
|
* expression. Handles nested generics (`track<Map<string, number>>(...)`)
|
package/dist/scan.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scan.d.ts","sourceRoot":"","sources":["../src/scan.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAEpD;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAItD;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAErD;AAED,wBAAgB,YAAY,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAEhD;AAED,wBAAgB,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAE/C;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAOzE;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAWrD;AAED;;GAEG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAS5D;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAG9D;AAED;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAc9D;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAsB1D;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAY3D;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAmDlD;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAkBvE;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CA2BjE;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAgBtD;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAkBrD;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAYpD;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAOtD;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAcnE;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAqB9D;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAY3D;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAexD"}
|
|
1
|
+
{"version":3,"file":"scan.d.ts","sourceRoot":"","sources":["../src/scan.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAEpD;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAItD;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAErD;AAED,wBAAgB,YAAY,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAEhD;AAED,wBAAgB,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAE/C;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAOzE;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAWrD;AAED;;GAEG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAS5D;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAG9D;AAED;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAc9D;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAsB1D;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAY3D;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAmDlD;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAkBvE;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CA2BjE;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAgBtD;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAkBrD;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAwDpE;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAYpD;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAOtD;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAcnE;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAqB9D;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAY3D;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAexD"}
|
package/dist/scan.js
CHANGED
|
@@ -408,6 +408,70 @@ export function containsForOfIn(text) {
|
|
|
408
408
|
}
|
|
409
409
|
return false;
|
|
410
410
|
}
|
|
411
|
+
/**
|
|
412
|
+
* Reports whether `ident` appears in `text` as a CALLED identifier — i.e.
|
|
413
|
+
* followed by an optional TS generic clause and then `(`.
|
|
414
|
+
*
|
|
415
|
+
* This is a character-loop tokenizer, not a text match: it steps over
|
|
416
|
+
* whitespace, `//` and block comments, and quoted / template strings, so
|
|
417
|
+
* `track(` appearing inside a string, a comment or a property access
|
|
418
|
+
* (`obj.track(...)`) never counts. Template literals are skipped whole —
|
|
419
|
+
* including their `${...}` holes — because a `track(` in an interpolation is
|
|
420
|
+
* not a reliable enough signal to inject a runtime binding for.
|
|
421
|
+
*
|
|
422
|
+
* Used to decide which runtime helpers a raw statement needs without
|
|
423
|
+
* requiring the caller to have been lowered into a TrackDecl first.
|
|
424
|
+
*/
|
|
425
|
+
export function callsIdentifier(text, ident) {
|
|
426
|
+
let i = 0;
|
|
427
|
+
const len = text.length;
|
|
428
|
+
// True when the previous significant character means the next identifier
|
|
429
|
+
// is a member access (`obj.track`) rather than a free call.
|
|
430
|
+
let afterDot = false;
|
|
431
|
+
while (i < len) {
|
|
432
|
+
const ch = text[i];
|
|
433
|
+
if (isWhitespaceChar(ch)) {
|
|
434
|
+
i++;
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
const commentEnd = skipComment(text, i);
|
|
438
|
+
if (commentEnd !== i) {
|
|
439
|
+
i = commentEnd;
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
if (ch === '"' || ch === "'" || ch === '`') {
|
|
443
|
+
i = skipString(text, i);
|
|
444
|
+
afterDot = false;
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
if (ch === '.' || (ch === '?' && text[i + 1] === '.')) {
|
|
448
|
+
afterDot = true;
|
|
449
|
+
i += ch === '.' ? 1 : 2;
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
if (isIdentStartCode(ch.charCodeAt(0))) {
|
|
453
|
+
let j = i + 1;
|
|
454
|
+
while (j < len && isIdentCharCode(text.charCodeAt(j)))
|
|
455
|
+
j++;
|
|
456
|
+
const name = text.slice(i, j);
|
|
457
|
+
let k = skipWhitespace(text, j);
|
|
458
|
+
// A `name<T,>(...)` generic call — skip the balanced `<...>`.
|
|
459
|
+
if (text[k] === '<') {
|
|
460
|
+
k = skipTrackGeneric(text, k);
|
|
461
|
+
k = skipWhitespace(text, k);
|
|
462
|
+
}
|
|
463
|
+
if (name === ident && text[k] === '(' && !afterDot) {
|
|
464
|
+
return true;
|
|
465
|
+
}
|
|
466
|
+
afterDot = false;
|
|
467
|
+
i = j;
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
afterDot = false;
|
|
471
|
+
i++;
|
|
472
|
+
}
|
|
473
|
+
return false;
|
|
474
|
+
}
|
|
411
475
|
/**
|
|
412
476
|
* Extracts the argument text of a `track<...>(...)` call from an init
|
|
413
477
|
* expression. Handles nested generics (`track<Map<string, number>>(...)`)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server-jsgen.d.ts","sourceRoot":"","sources":["../src/server-jsgen.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,uBAAuB,CAAC;AAOzE,OAAO,EAAkG,KAAK,WAAW,EAAE,MAAM,mCAAmC,CAAC;
|
|
1
|
+
{"version":3,"file":"server-jsgen.d.ts","sourceRoot":"","sources":["../src/server-jsgen.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,uBAAuB,CAAC;AAOzE,OAAO,EAAkG,KAAK,WAAW,EAAE,MAAM,mCAAmC,CAAC;AA4DrK,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,aAAa,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,IAAI,EAAE,OAAO,GAAE,OAAe,EAAE,OAAO,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,GAAG,MAAM,CA4EjJ;AAyZD,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,WAAW,EAAE,aAAa,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,MAAM,CAwD1F;AAED,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,OAAO,CAAC;CAClB;AAuFD;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,GAAG,iBAAiB,EAAE,CAkCzE;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,cAAc,EAAE,OAAO,GAAG,GAAG,CAAC,MAAM,EAAE,QAAQ,CAAC,CAYhG"}
|
package/dist/server-jsgen.js
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
|
-
import { StaticNode, TextNode, DynamicBinding, OpaqueDynamicRegion, MapRegion, WhileLoop, SwitchBlock, TryCatch, ForLoop, TrackDecl, RuntimeStatement, ComponentRef, ComponentCall, ServerBlock, ClientBlock, HeadBlock, SlotNode, PropSlot, PropSlotRender, } from '@vesk/compiler/src/ir';
|
|
1
|
+
import { Expression, StaticNode, TextNode, DynamicBinding, OpaqueDynamicRegion, MapRegion, WhileLoop, SwitchBlock, TryCatch, ForLoop, TrackDecl, RuntimeStatement, ComponentRef, ComponentCall, ServerBlock, ClientBlock, HeadBlock, SlotNode, PropSlot, PropSlotRender, } from '@vesk/compiler/src/ir';
|
|
2
2
|
import { isStaticIR, collectTrackedNames, transformTracked, transformTrackedInit, semicolonizeStatement } from '@vesk/compiler/src/client-codegen';
|
|
3
|
-
import { unwrapTrackCall, stripTrackGeneric, hasTopLevelComma, skipWhitespace, findBalancedEnd, startsWithIdentifier } from '@vesk/compiler/src/scan';
|
|
3
|
+
import { unwrapTrackCall, stripTrackGeneric, hasTopLevelComma, skipWhitespace, findBalancedEnd, startsWithIdentifier, callsIdentifier } from '@vesk/compiler/src/scan';
|
|
4
4
|
import { escapeHtml, indent, exprJS, extractTopLevelNames, extractRuntimeNames, buildParamInit, __vskHydrate, __vskMarkerless, __vskImportedNames, setVskImportedNames, setVskForceClaim, takeVskForceClaim, nextVskId, } from '@vesk/compiler/src/server-utils';
|
|
5
5
|
import { localValueImportNames } from '@vesk/compiler/src/module-imports';
|
|
6
|
+
import { lowerJsxInExpression } from '@vesk/compiler/src/server-jsx-lowering';
|
|
7
|
+
import { print } from 'esrap';
|
|
8
|
+
import tsPrinter from 'esrap/languages/ts';
|
|
9
|
+
/** Print an already-lowered AST with the plain TS printer (no JSX remains). */
|
|
10
|
+
function printLoweredTs(ast) {
|
|
11
|
+
return print(ast, tsPrinter()).code;
|
|
12
|
+
}
|
|
6
13
|
import { vskImportValueNames } from '@vesk/compiler/src/vsk-imports';
|
|
7
14
|
// Hydrate-mode component-boundary wrapper. Each server-rendered component call
|
|
8
15
|
// is preceded by a `<!--vsk-->` marker and a single container element: the
|
|
@@ -107,8 +114,17 @@ export function irNodeToJS(node, importedNames, isAsync = false, tracked) {
|
|
|
107
114
|
`})();`,
|
|
108
115
|
].join('\n');
|
|
109
116
|
}
|
|
110
|
-
if (node instanceof RuntimeStatement)
|
|
117
|
+
if (node instanceof RuntimeStatement) {
|
|
118
|
+
const stmt = node;
|
|
119
|
+
const lowered = stmt.ast ? lowerJsxInExpression(stmt.ast) : null;
|
|
120
|
+
if (lowered && lowered !== stmt.ast && (tracked?.size ?? 0) === 0) {
|
|
121
|
+
return semicolonizeStatement(printLoweredTs(lowered));
|
|
122
|
+
}
|
|
123
|
+
if (lowered && lowered !== stmt.ast) {
|
|
124
|
+
return semicolonizeStatement(transformTracked({ ...node, ast: lowered }, tracked || new Map()));
|
|
125
|
+
}
|
|
111
126
|
return semicolonizeStatement(transformTracked(node, tracked || new Map()));
|
|
127
|
+
}
|
|
112
128
|
if (node instanceof SlotNode) {
|
|
113
129
|
// Markerless mode drops the slot boundary comments: SSR output is plain
|
|
114
130
|
// HTML and slot claims scope structurally (createLayoutSlot subject to the
|
|
@@ -133,7 +149,21 @@ function isEvent(target) {
|
|
|
133
149
|
return !!target && target.startsWith('on') && target.length > 2;
|
|
134
150
|
}
|
|
135
151
|
function exprJSX(node, tracked) {
|
|
136
|
-
|
|
152
|
+
// A function-valued child — `<For each={items}>{(item) => <li/>}</For>` —
|
|
153
|
+
// carries JSX inside an EXPRESSION, and the server has no IR node for it, so
|
|
154
|
+
// it used to be printed verbatim and the render threw "Unexpected token '<'".
|
|
155
|
+
// Lower it to the string the body emitter would have produced. Client output
|
|
156
|
+
// is untouched (a chunk goes through esbuild's tsx loader, so JSX is legal
|
|
157
|
+
// there).
|
|
158
|
+
const lowered = node.ast ? lowerJsxInExpression(node.ast) : null;
|
|
159
|
+
const didLower = lowered !== node.ast;
|
|
160
|
+
if (didLower && (tracked?.size ?? 0) === 0) {
|
|
161
|
+
// `transformTracked` returns the RAW source when there is nothing to
|
|
162
|
+
// rewrite, which would throw the lowering away. With no tracked names there
|
|
163
|
+
// are no rewrites to lose.
|
|
164
|
+
return exprJS(printLoweredTs(lowered));
|
|
165
|
+
}
|
|
166
|
+
return exprJS(transformTracked({ ...node, ast: lowered }, tracked || new Map()));
|
|
137
167
|
}
|
|
138
168
|
function staticNodeToJS(node, isAsync = false, tracked) {
|
|
139
169
|
const lines = [];
|
|
@@ -461,19 +491,35 @@ function componentCallToJS(node, importedNames, isAsync = false, tracked) {
|
|
|
461
491
|
}
|
|
462
492
|
}
|
|
463
493
|
if (regularChildren.length > 0) {
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
494
|
+
// A single function-valued child (`<For each={items}>{(item) => …}</For>`,
|
|
495
|
+
// or a reference to one) must stay a FUNCTION: `For` calls it per item.
|
|
496
|
+
// Wrapping it in the `(() => { const __out = []; … })()` text emitter made it
|
|
497
|
+
// return undefined, so For rendered an empty list.
|
|
498
|
+
const fnType = regularChildren.length === 1 && regularChildren[0] instanceof DynamicBinding && regularChildren[0].kind === 'text'
|
|
499
|
+
? regularChildren[0].expression.ast?.type
|
|
500
|
+
: null;
|
|
501
|
+
const isFnChild = fnType === 'ArrowFunctionExpression'
|
|
502
|
+
|| fnType === 'FunctionExpression'
|
|
503
|
+
|| fnType === 'Identifier'
|
|
504
|
+
|| fnType === 'MemberExpression';
|
|
505
|
+
if (isFnChild) {
|
|
506
|
+
propsEntries.push(`children: ${exprJSX(regularChildren[0].expression, tracked)}`);
|
|
469
507
|
}
|
|
470
|
-
|
|
471
|
-
const
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
508
|
+
else {
|
|
509
|
+
const childLines = [];
|
|
510
|
+
for (const child of regularChildren) {
|
|
511
|
+
const code = irNodeToJS(child, importedNames, isAsync, tracked);
|
|
512
|
+
if (code)
|
|
513
|
+
childLines.push(code);
|
|
514
|
+
}
|
|
515
|
+
if (childLines.length > 0) {
|
|
516
|
+
const childrenVar = `__ch${nextVskId()}`;
|
|
517
|
+
propsEntries.push(`children: ${childrenVar}`);
|
|
518
|
+
lines.push(`const ${childrenVar} = ${isAsync ? 'await (async ' : '('}() => {`);
|
|
519
|
+
lines.push(`const __out = [];`);
|
|
520
|
+
lines.push(indent(childLines.join('\n')));
|
|
521
|
+
lines.push(`return __out.join(''); })();`);
|
|
522
|
+
}
|
|
477
523
|
}
|
|
478
524
|
}
|
|
479
525
|
const propsObj = `{ ${propsEntries.join(', ')} }`;
|
|
@@ -503,10 +549,10 @@ function componentCallToJS(node, importedNames, isAsync = false, tracked) {
|
|
|
503
549
|
}
|
|
504
550
|
const callExpr = `${awaitKw}${calleeVar}(${propsObj}, __registry, (${calleeVar}.__veskScope || __vesk))`;
|
|
505
551
|
if (__vskHydrate && !__vskMarkerless) {
|
|
506
|
-
lines.push(`__out.push(${JSON.stringify(componentMarker(compName))} + (${callExpr}
|
|
552
|
+
lines.push(`__out.push(${JSON.stringify(componentMarker(compName))} + __flat(${callExpr}));`);
|
|
507
553
|
}
|
|
508
554
|
else {
|
|
509
|
-
lines.push(`__out.push(${callExpr}
|
|
555
|
+
lines.push(`__out.push(__flat(${callExpr}));`);
|
|
510
556
|
}
|
|
511
557
|
return lines.join('\n');
|
|
512
558
|
}
|
|
@@ -524,6 +570,9 @@ export function generateFunctionBody(comp, importedNames) {
|
|
|
524
570
|
lines.push(`const __styleText = (v) => { if (typeof v === 'string') return v; if (v && typeof v === 'object') { let s = ''; for (const k in v) { const x = v[k]; if (x == null || x === false) continue; s += k.replace(/[A-Z]/g, (m) => '-' + m.toLowerCase()) + ':' + x + ';'; } return s; } return String(v); };`);
|
|
525
571
|
lines.push(`const __attr = (n, v) => (v == null || v === false) ? '' : ' ' + n + '="' + __escape(n === 'style' ? __styleText(v) : String(v)) + '"';`);
|
|
526
572
|
lines.push(`const raw = (s) => s == null ? '' : String(s);`);
|
|
573
|
+
// A component may return an ARRAY of strings (`<For>` maps its children), and
|
|
574
|
+
// `Array#toString` joins the rows with commas — `<li>a</li>,<li>b</li>`.
|
|
575
|
+
lines.push(`const __flat = (v) => Array.isArray(v) ? v.join('') : (v == null ? '' : String(v));`);
|
|
527
576
|
lines.push(`const __tk = globalThis.__vsk_ssr_token || '';`);
|
|
528
577
|
if (asyncMode) {
|
|
529
578
|
lines.push(`const __pk = __tk ? '__vsk_ssr_promises_' + __tk : '__vsk_ssr_promises';`);
|
|
@@ -563,6 +612,105 @@ export function generateFunctionBody(comp, importedNames) {
|
|
|
563
612
|
__currentCompName = '';
|
|
564
613
|
return lines.join('\n');
|
|
565
614
|
}
|
|
615
|
+
/**
|
|
616
|
+
* Whether a component body needs `track` / `get` / `set` destructured into its
|
|
617
|
+
* `__vesk` scope.
|
|
618
|
+
*
|
|
619
|
+
* A `TrackDecl` (`const &[n] = track(0)`) always needs them. A PLAIN
|
|
620
|
+
* `const n = track(0)` does not lower to a TrackDecl — it stays a raw
|
|
621
|
+
* `RuntimeStatement` whose source still calls `track` — and used to reach the
|
|
622
|
+
* server renderer unbound, taking the whole page down with
|
|
623
|
+
* "track is not defined". The client codegen imports these names
|
|
624
|
+
* unconditionally, so the server has to recognise the plain form too or the
|
|
625
|
+
* two halves of a file disagree about what is in scope.
|
|
626
|
+
*/
|
|
627
|
+
function bodyNeedsRuntimeCells(nodes) {
|
|
628
|
+
for (const node of nodes) {
|
|
629
|
+
if (node instanceof TrackDecl)
|
|
630
|
+
return true;
|
|
631
|
+
// Raw statement / expression text: the only place a plain `track(...)`
|
|
632
|
+
// call can appear outside a lowered node.
|
|
633
|
+
if (node instanceof RuntimeStatement && callsIdentifier(node.raw, 'track'))
|
|
634
|
+
return true;
|
|
635
|
+
if (node instanceof Expression && callsIdentifier(node.raw, 'track'))
|
|
636
|
+
return true;
|
|
637
|
+
if (node instanceof ForLoop) {
|
|
638
|
+
if (callsIdentifier(node.init, 'track'))
|
|
639
|
+
return true;
|
|
640
|
+
if (callsIdentifier(node.update, 'track'))
|
|
641
|
+
return true;
|
|
642
|
+
if (callsIdentifier(node.condition.raw, 'track'))
|
|
643
|
+
return true;
|
|
644
|
+
if (bodyNeedsRuntimeCells(node.bodyTemplate))
|
|
645
|
+
return true;
|
|
646
|
+
}
|
|
647
|
+
if (node instanceof StaticNode) {
|
|
648
|
+
for (const attr of node.attributes) {
|
|
649
|
+
if (callsIdentifier(attr.value, 'track'))
|
|
650
|
+
return true;
|
|
651
|
+
}
|
|
652
|
+
if (bodyNeedsRuntimeCells(node.children))
|
|
653
|
+
return true;
|
|
654
|
+
}
|
|
655
|
+
if (node instanceof ComponentCall) {
|
|
656
|
+
for (const prop of node.props) {
|
|
657
|
+
if (callsIdentifier(prop.value.raw, 'track'))
|
|
658
|
+
return true;
|
|
659
|
+
}
|
|
660
|
+
for (const spread of node.spreadProps) {
|
|
661
|
+
if (callsIdentifier(spread.raw, 'track'))
|
|
662
|
+
return true;
|
|
663
|
+
}
|
|
664
|
+
if (bodyNeedsRuntimeCells(node.children))
|
|
665
|
+
return true;
|
|
666
|
+
}
|
|
667
|
+
if (node instanceof ServerBlock || node instanceof ClientBlock || node instanceof HeadBlock) {
|
|
668
|
+
if (bodyNeedsRuntimeCells(node.children))
|
|
669
|
+
return true;
|
|
670
|
+
}
|
|
671
|
+
if (node instanceof PropSlot && bodyNeedsRuntimeCells(node.body))
|
|
672
|
+
return true;
|
|
673
|
+
if (node instanceof MapRegion) {
|
|
674
|
+
if (callsIdentifier(node.expression.raw, 'track'))
|
|
675
|
+
return true;
|
|
676
|
+
if (bodyNeedsRuntimeCells(node.bodyTemplate))
|
|
677
|
+
return true;
|
|
678
|
+
if (bodyNeedsRuntimeCells(node.alternateNodes))
|
|
679
|
+
return true;
|
|
680
|
+
}
|
|
681
|
+
if (node instanceof OpaqueDynamicRegion) {
|
|
682
|
+
if (callsIdentifier(node.condition.raw, 'track'))
|
|
683
|
+
return true;
|
|
684
|
+
if (bodyNeedsRuntimeCells(node.consequentNodes))
|
|
685
|
+
return true;
|
|
686
|
+
if (bodyNeedsRuntimeCells(node.alternateNodes))
|
|
687
|
+
return true;
|
|
688
|
+
}
|
|
689
|
+
if (node instanceof WhileLoop) {
|
|
690
|
+
if (callsIdentifier(node.condition.raw, 'track'))
|
|
691
|
+
return true;
|
|
692
|
+
if (bodyNeedsRuntimeCells(node.bodyTemplate))
|
|
693
|
+
return true;
|
|
694
|
+
}
|
|
695
|
+
if (node instanceof SwitchBlock) {
|
|
696
|
+
if (callsIdentifier(node.discriminant.raw, 'track'))
|
|
697
|
+
return true;
|
|
698
|
+
for (const c of node.cases) {
|
|
699
|
+
if (c.test && callsIdentifier(c.test.raw, 'track'))
|
|
700
|
+
return true;
|
|
701
|
+
if (bodyNeedsRuntimeCells(c.body))
|
|
702
|
+
return true;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
if (node instanceof TryCatch) {
|
|
706
|
+
if (bodyNeedsRuntimeCells(node.bodyTemplate))
|
|
707
|
+
return true;
|
|
708
|
+
if (bodyNeedsRuntimeCells(node.catchBody))
|
|
709
|
+
return true;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
return false;
|
|
713
|
+
}
|
|
566
714
|
/**
|
|
567
715
|
* Builds the compiled body source for every component in an IR root. Used by
|
|
568
716
|
* both `buildComponentMap` (live functions) and `precompile.ts` (AOT SSR plans
|
|
@@ -579,7 +727,7 @@ export function buildComponentEntries(irRoot) {
|
|
|
579
727
|
const componentNames = new Set(irRoot.components.map((c) => c.name));
|
|
580
728
|
const topValueNames = topNames.filter((n) => !componentNames.has(n));
|
|
581
729
|
const importedNames = new Set([...runtimeNames, ...localValueNames, ...topValueNames]);
|
|
582
|
-
const hasTracked = irRoot.components.some((c) => c.body
|
|
730
|
+
const hasTracked = irRoot.components.some((c) => bodyNeedsRuntimeCells(c.body));
|
|
583
731
|
const extraNames = hasTracked ? ['get', 'set', 'track'] : [];
|
|
584
732
|
// Values exported by a `.vsk` module (`export const MAX = 10`) are hoisted
|
|
585
733
|
// into this file's `__vesk` and must be destructured into component scope.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** True when `node` contains JSX anywhere. */
|
|
2
|
+
export declare function containsJsx(node: unknown, depth?: number): boolean;
|
|
3
|
+
declare const RAW = "__raw";
|
|
4
|
+
/**
|
|
5
|
+
* JSX text semantics, matching what the compiler emits for StaticNode text:
|
|
6
|
+
* lines are trimmed, blank lines vanish, and interior newlines collapse to a
|
|
7
|
+
* single space. Getting this wrong is visible as stray whitespace in SSR HTML.
|
|
8
|
+
*/
|
|
9
|
+
export declare function normalizeJsxText(value: string): string;
|
|
10
|
+
/**
|
|
11
|
+
* Rewrite every JSX node inside `ast` into the string it would have rendered.
|
|
12
|
+
* Returns the original node when there is no JSX, so the common path is free.
|
|
13
|
+
*/
|
|
14
|
+
export declare function lowerJsxInExpression(ast: unknown): unknown;
|
|
15
|
+
export { RAW as RAW_HELPER };
|
|
16
|
+
//# sourceMappingURL=server-jsx-lowering.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server-jsx-lowering.d.ts","sourceRoot":"","sources":["../src/server-jsx-lowering.ts"],"names":[],"mappings":"AAwBA,8CAA8C;AAC9C,wBAAgB,WAAW,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,SAAI,GAAG,OAAO,CAa7D;AAKD,QAAA,MAAM,GAAG,UAAU,CAAC;AAEpB;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAiBtD;AAiLD;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAG1D;AAED,OAAO,EAAE,GAAG,IAAI,UAAU,EAAE,CAAC"}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/** True when `node` contains JSX anywhere. */
|
|
2
|
+
export function containsJsx(node, depth = 0) {
|
|
3
|
+
if (!node || typeof node !== 'object' || depth > 40)
|
|
4
|
+
return false;
|
|
5
|
+
if (Array.isArray(node)) {
|
|
6
|
+
for (const item of node)
|
|
7
|
+
if (containsJsx(item, depth + 1))
|
|
8
|
+
return true;
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
const type = node.type;
|
|
12
|
+
if (type === 'JSXElement' || type === 'JSXFragment')
|
|
13
|
+
return true;
|
|
14
|
+
for (const key of Object.keys(node)) {
|
|
15
|
+
const value = node[key];
|
|
16
|
+
if (value && typeof value === 'object' && containsJsx(value, depth + 1))
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
/** The helper names the generated server body already defines. */
|
|
22
|
+
const ESCAPE = '__escape';
|
|
23
|
+
const ATTR = '__attr';
|
|
24
|
+
const RAW = '__raw';
|
|
25
|
+
/**
|
|
26
|
+
* JSX text semantics, matching what the compiler emits for StaticNode text:
|
|
27
|
+
* lines are trimmed, blank lines vanish, and interior newlines collapse to a
|
|
28
|
+
* single space. Getting this wrong is visible as stray whitespace in SSR HTML.
|
|
29
|
+
*/
|
|
30
|
+
export function normalizeJsxText(value) {
|
|
31
|
+
const lines = value.split(/\r\n|\n|\r/);
|
|
32
|
+
let lastNonEmpty = -1;
|
|
33
|
+
for (let i = 0; i < lines.length; i++) {
|
|
34
|
+
if (/[^ \t]/.test(lines[i]))
|
|
35
|
+
lastNonEmpty = i;
|
|
36
|
+
}
|
|
37
|
+
let out = '';
|
|
38
|
+
for (let i = 0; i < lines.length; i++) {
|
|
39
|
+
let line = lines[i].replace(/\t/g, ' ');
|
|
40
|
+
if (i !== 0)
|
|
41
|
+
line = line.replace(/^ +/, '');
|
|
42
|
+
if (i !== lines.length - 1)
|
|
43
|
+
line = line.replace(/ +$/, '');
|
|
44
|
+
if (line) {
|
|
45
|
+
if (i !== lastNonEmpty)
|
|
46
|
+
line += ' ';
|
|
47
|
+
out += line;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
function lower(node, ctx) {
|
|
53
|
+
if (Array.isArray(node))
|
|
54
|
+
return node.map((n) => lower(n, ctx));
|
|
55
|
+
if (!node || typeof node !== 'object')
|
|
56
|
+
return node;
|
|
57
|
+
const n = node;
|
|
58
|
+
if (n.type === 'JSXElement') {
|
|
59
|
+
const opening = n.openingElement;
|
|
60
|
+
const name = jsxTagName(opening.name);
|
|
61
|
+
const attrs = lowerAttributes(opening.attributes, ctx);
|
|
62
|
+
const children = lowerChildren(n.children, ctx);
|
|
63
|
+
// statics[0] opens the tag, attributes interleave, then '>', then children,
|
|
64
|
+
// then the closing tag. Getting the '>' placement wrong silently produced
|
|
65
|
+
// `<lia</li>` — a tag that never closes and swallows the next element.
|
|
66
|
+
const statics = [`<${name}`, ...attrs.statics.slice(1)];
|
|
67
|
+
statics[statics.length - 1] += '>';
|
|
68
|
+
const quasis = [...statics, `</${name}>`];
|
|
69
|
+
const expressions = [...attrs.exprs, ...children];
|
|
70
|
+
return template(quasis, expressions);
|
|
71
|
+
}
|
|
72
|
+
if (n.type === 'JSXFragment') {
|
|
73
|
+
return template(['', ''], lowerChildren(n.children, ctx));
|
|
74
|
+
}
|
|
75
|
+
// Any other node: rebuild it with lowered children.
|
|
76
|
+
const out = {};
|
|
77
|
+
for (const key of Object.keys(n)) {
|
|
78
|
+
if (key === 'type' || key === 'start' || key === 'end' || key === 'loc' || key === 'range') {
|
|
79
|
+
out[key] = n[key];
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const value = n[key];
|
|
83
|
+
out[key] = value && typeof value === 'object' ? lower(value, ctx) : value;
|
|
84
|
+
}
|
|
85
|
+
return out;
|
|
86
|
+
}
|
|
87
|
+
function jsxTagName(nameNode) {
|
|
88
|
+
const n = nameNode;
|
|
89
|
+
if (!n)
|
|
90
|
+
return 'div';
|
|
91
|
+
if (n.type === 'JSXIdentifier')
|
|
92
|
+
return String(n.name);
|
|
93
|
+
if (n.type === 'JSXMemberExpression') {
|
|
94
|
+
const object = jsxTagName(n.object);
|
|
95
|
+
const property = n.property;
|
|
96
|
+
return `${object}.${String(property?.name)}`;
|
|
97
|
+
}
|
|
98
|
+
if (n.type === 'JSXNamespacedName') {
|
|
99
|
+
const ns = n.namespace;
|
|
100
|
+
const name = n.name;
|
|
101
|
+
return `${String(ns?.name)}:${String(name?.name)}`;
|
|
102
|
+
}
|
|
103
|
+
return 'div';
|
|
104
|
+
}
|
|
105
|
+
function lowerAttributes(attrs, ctx) {
|
|
106
|
+
const statics = [''];
|
|
107
|
+
const exprs = [];
|
|
108
|
+
const push = (text, expr) => {
|
|
109
|
+
if (expr === undefined) {
|
|
110
|
+
statics[statics.length - 1] += text;
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
statics[statics.length - 1] += text;
|
|
114
|
+
statics.push('');
|
|
115
|
+
exprs.push(expr);
|
|
116
|
+
};
|
|
117
|
+
for (const attr of attrs || []) {
|
|
118
|
+
const a = attr;
|
|
119
|
+
if (a.type === 'JSXSpreadAttribute') {
|
|
120
|
+
// A spread inside a function-valued child is not lowered; dropping it
|
|
121
|
+
// would be silently wrong, so it is reported instead.
|
|
122
|
+
console.error('[vesk] a spread attribute in a JSX expression is not supported yet (dropped):', String(a.argument ?? '').slice(0, 60));
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
const name = jsxAttrName(a.name);
|
|
126
|
+
const value = a.value;
|
|
127
|
+
if (value === null || value === undefined) {
|
|
128
|
+
push(` ${name}`);
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (value.type === 'StringLiteral') {
|
|
132
|
+
push(` ${name}="${escapeAttrText(String(value.value))}"`);
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (value.type === 'JSXExpressionContainer') {
|
|
136
|
+
const inner = value.expression;
|
|
137
|
+
if (!inner || inner.type === 'JSXEmptyExpression') {
|
|
138
|
+
push(` ${name}="true"`);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
push(` ${name}="`, callNode(ATTR, [literal(name), coerceString(lower(inner, ctx))]));
|
|
142
|
+
push('"');
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
push(` ${name}="`, callNode(ATTR, [literal(name), coerceString(lower(value, ctx))]));
|
|
146
|
+
push('"');
|
|
147
|
+
}
|
|
148
|
+
return { statics, exprs };
|
|
149
|
+
}
|
|
150
|
+
function jsxAttrName(nameNode) {
|
|
151
|
+
const n = nameNode;
|
|
152
|
+
if (!n)
|
|
153
|
+
return 'data';
|
|
154
|
+
if (n.type === 'JSXIdentifier')
|
|
155
|
+
return String(n.name);
|
|
156
|
+
return jsxTagName(n);
|
|
157
|
+
}
|
|
158
|
+
function lowerChildren(children, ctx) {
|
|
159
|
+
const out = [];
|
|
160
|
+
for (const child of children || []) {
|
|
161
|
+
const c = child;
|
|
162
|
+
if (c.type === 'JSXText') {
|
|
163
|
+
const text = normalizeJsxText(String(c.value ?? ''));
|
|
164
|
+
if (text)
|
|
165
|
+
out.push(literal(text));
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (c.type === 'JSXExpressionContainer') {
|
|
169
|
+
const inner = c.expression;
|
|
170
|
+
if (!inner || inner.type === 'JSXEmptyExpression')
|
|
171
|
+
continue;
|
|
172
|
+
const lowered = lower(inner, ctx);
|
|
173
|
+
out.push(coerceString(lowered));
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
const lowered = lower(c, ctx);
|
|
177
|
+
out.push(coerceString(lowered));
|
|
178
|
+
}
|
|
179
|
+
return out;
|
|
180
|
+
}
|
|
181
|
+
/** `{v}` in HTML output must be escaped, exactly as the body emitter does. */
|
|
182
|
+
function coerceString(node) {
|
|
183
|
+
if (!node || typeof node !== 'object')
|
|
184
|
+
return callNode(ESCAPE, [literal(String(node))]);
|
|
185
|
+
const n = node;
|
|
186
|
+
// Already a template literal we built: its parts are static text, but any
|
|
187
|
+
// expression inside came from a nested container that escaped itself.
|
|
188
|
+
if (n.type === 'TemplateLiteral')
|
|
189
|
+
return n;
|
|
190
|
+
if (n.type === 'Literal' || n.type === 'StringLiteral')
|
|
191
|
+
return callNode(ESCAPE, [node]);
|
|
192
|
+
return callNode(ESCAPE, [callNode('String', [node])]);
|
|
193
|
+
}
|
|
194
|
+
/** Assemble a template literal: `statics.length === expressions.length + 1`. */
|
|
195
|
+
function template(statics, expressions) {
|
|
196
|
+
const quasis = statics.map((raw, i) => ({
|
|
197
|
+
type: 'TemplateElement',
|
|
198
|
+
value: { raw },
|
|
199
|
+
tail: i === statics.length - 1,
|
|
200
|
+
}));
|
|
201
|
+
return { type: 'TemplateLiteral', quasis, expressions };
|
|
202
|
+
}
|
|
203
|
+
function literal(value) {
|
|
204
|
+
return { type: 'Literal', value };
|
|
205
|
+
}
|
|
206
|
+
function callNode(callee, args) {
|
|
207
|
+
return {
|
|
208
|
+
type: 'CallExpression',
|
|
209
|
+
callee: { type: 'Identifier', name: callee },
|
|
210
|
+
arguments: args,
|
|
211
|
+
optional: false,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
function escapeAttrText(value) {
|
|
215
|
+
return value.split('&').join('&').split('<').join('<').split('>').join('>').split('"').join('"');
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Rewrite every JSX node inside `ast` into the string it would have rendered.
|
|
219
|
+
* Returns the original node when there is no JSX, so the common path is free.
|
|
220
|
+
*/
|
|
221
|
+
export function lowerJsxInExpression(ast) {
|
|
222
|
+
if (!containsJsx(ast))
|
|
223
|
+
return ast;
|
|
224
|
+
return lower(ast, { track: false });
|
|
225
|
+
}
|
|
226
|
+
export { RAW as RAW_HELPER };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vesk/compiler",
|
|
3
|
-
"version": "0.2.50
|
|
3
|
+
"version": "0.2.50",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Vesk compiler — lexer, parser, semantic analysis, IR, codegen",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"@sveltejs/acorn-typescript": "^1.0.0",
|
|
23
|
-
"@vesk/types": "^0.2.50
|
|
23
|
+
"@vesk/types": "^0.2.50",
|
|
24
24
|
"acorn": "^8.15.0",
|
|
25
25
|
"esrap": "^2.3.0",
|
|
26
26
|
"is-reference": "^3.0.0",
|