@vesk/compiler 0.2.50-canary.11 → 0.2.50-canary.12

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.
@@ -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;
@@ -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;gBAEF,OAAO,EAAE,MAAM,EAAE,IAAI,GAAE,gBAAqB;IAYxD,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,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"}
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"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"}
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
- if (opts.code !== undefined)
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,
@@ -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.code = codeFrame(source, line, column);
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.code = codeFrame(source, line, column);
1540
+ opts.frame = codeFrame(source, line, column);
1541
1541
  }
1542
1542
  throw VeskError.clientBlockInServer(compName, opts);
1543
1543
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vesk/compiler",
3
- "version": "0.2.50-canary.11",
3
+ "version": "0.2.50-canary.12",
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-canary.11",
23
+ "@vesk/types": "^0.2.50-canary.12",
24
24
  "acorn": "^8.15.0",
25
25
  "esrap": "^2.3.0",
26
26
  "is-reference": "^3.0.0",