@vesk/compiler 0.2.50-canary.9 → 0.2.51

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
  /**
@@ -33,10 +38,64 @@ export declare class VeskError extends Error {
33
38
  * - `value` — `ns.max` read as a value. Component tags resolve; plain value
34
39
  * reads do not, because nothing binds `ns` at runtime.
35
40
  */
41
+ /**
42
+ * An imported `.ts`/`.js` module could not be parsed.
43
+ *
44
+ * This used to be silent, and that is the whole point of the diagnostic. A
45
+ * parse failure meant the module's RAW, untranspiled source was substituted,
46
+ * so TypeScript syntax reached `new Function` and the module blew up much
47
+ * later with a bare `Unexpected token ':'` — attributed to no file, at a
48
+ * point in the build far from the code that caused it. It also poisoned the
49
+ * whole shared module graph: one unparseable module left its exports
50
+ * undefined, so unrelated consumers failed with `X is not iterable`.
51
+ *
52
+ * Two real constructs hit this: a parameter annotation that survived the
53
+ * stripper only when the parameter also had a default value, and a generic
54
+ * arrow's type parameter list, which the JSX tokenizer cannot read at all.
55
+ * Both are fixed; this error exists so the NEXT such gap is reported at the
56
+ * right file with the right line instead of surfacing three modules later.
57
+ */
58
+ static moduleParseFailed(context?: VeskErrorOptions & {
59
+ reason?: string;
60
+ }): VeskError;
36
61
  static vskNamespaceMember(context?: VeskErrorOptions & {
37
62
  form?: 'nested' | 'value';
38
63
  }): VeskError;
39
64
  static classDecl(context?: VeskErrorOptions): VeskError;
65
+ /**
66
+ * A browser-only global used in code the SERVER evaluates.
67
+ *
68
+ * On the server these are either absent (`window is not defined`) or, worse,
69
+ * silently present-but-wrong (`navigator` exists in modern Node, so
70
+ * `navigator.onLine` is `undefined` and a page renders as if it were offline).
71
+ * A raw `ReferenceError` at request time is the worst possible report: it
72
+ * names neither the component nor the fix. This points at the line.
73
+ */
74
+ static ssrUnsafeGlobal(name: string, context?: Partial<VeskErrorOptions> & {
75
+ inComponent?: string;
76
+ }): VeskError;
77
+ /**
78
+ * The second name from `track()` called as a function.
79
+ *
80
+ * `const &[count, setCount] = track(0)` returns a CELL, not a setter function, so
81
+ * `setCount(1)` throws `setCount is not a function` from generated code with
82
+ * no line of user code in the message. The cell is the writer: `.set(v)`.
83
+ */
84
+ static cellCalledAsFunction(name: string, context?: Partial<VeskErrorOptions> & {
85
+ value?: string;
86
+ cell?: string;
87
+ }): VeskError;
88
+ /**
89
+ * `set(value, …)` used outside a rewritten expression.
90
+ *
91
+ * The compiler rewrites `set(count, v)` to `countCell.set(v)` inside JSX and
92
+ * event handlers — but a bare statement in the component body is NOT
93
+ * rewritten, so the runtime `set` receives the value and throws
94
+ * "Cannot read properties of undefined". Pass the CELL, not the value.
95
+ */
96
+ static setWithValueName(name: string, context?: Partial<VeskErrorOptions> & {
97
+ cell?: string;
98
+ }): VeskError;
40
99
  static serverBlockInClient(compName: string, context?: VeskErrorOptions): VeskError;
41
100
  static clientBlockInServer(compName: string, context?: VeskErrorOptions): VeskError;
42
101
  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":"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;;;;;;;;;;;;;;;;OAgBG;IACH,MAAM,CAAC,iBAAiB,CAAC,OAAO,GAAE,gBAAgB,GAAG;QAAE,MAAM,CAAC,EAAE,MAAM,CAAA;KAAO,GAAG,SAAS;IAoBzF,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
- 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])];
@@ -161,6 +169,39 @@ export class VeskError extends Error {
161
169
  * - `value` — `ns.max` read as a value. Component tags resolve; plain value
162
170
  * reads do not, because nothing binds `ns` at runtime.
163
171
  */
172
+ /**
173
+ * An imported `.ts`/`.js` module could not be parsed.
174
+ *
175
+ * This used to be silent, and that is the whole point of the diagnostic. A
176
+ * parse failure meant the module's RAW, untranspiled source was substituted,
177
+ * so TypeScript syntax reached `new Function` and the module blew up much
178
+ * later with a bare `Unexpected token ':'` — attributed to no file, at a
179
+ * point in the build far from the code that caused it. It also poisoned the
180
+ * whole shared module graph: one unparseable module left its exports
181
+ * undefined, so unrelated consumers failed with `X is not iterable`.
182
+ *
183
+ * Two real constructs hit this: a parameter annotation that survived the
184
+ * stripper only when the parameter also had a default value, and a generic
185
+ * arrow's type parameter list, which the JSX tokenizer cannot read at all.
186
+ * Both are fixed; this error exists so the NEXT such gap is reported at the
187
+ * right file with the right line instead of surfacing three modules later.
188
+ */
189
+ static moduleParseFailed(context = {}) {
190
+ return new VeskError(`Could not parse an imported module${context.reason ? `: ${context.reason}` : '.'}`, {
191
+ ...context,
192
+ code: context.code || 'V0901',
193
+ suggestions: [
194
+ 'Check the line the ^ marker points at — it is the construct the parser stopped on.',
195
+ 'If it is TypeScript syntax the stripper does not handle, simplifying it here fixes every module that imports this file.',
196
+ 'This is a bug in the compiler, not in your code, if the construct is ordinary TypeScript — please report it with the source line.',
197
+ ],
198
+ nextSteps: [
199
+ 'A module that fails to parse is substituted with its raw source, so every type annotation it contains becomes a runtime SyntaxError.',
200
+ 'The failure is reported here, at the offending file and line, rather than as an `Unexpected token` in whichever module happened to import it.',
201
+ ],
202
+ tip: 'Ordinary TypeScript should parse; if this fires on valid TS, the parser is missing a construct.',
203
+ });
204
+ }
164
205
  static vskNamespaceMember(context = {}) {
165
206
  const nested = context.form === 'nested';
166
207
  return new VeskError(nested
@@ -202,6 +243,75 @@ export class VeskError extends Error {
202
243
  tip: 'Use plain objects for data and factory functions for constructors inside .vsk files.',
203
244
  });
204
245
  }
246
+ /**
247
+ * A browser-only global used in code the SERVER evaluates.
248
+ *
249
+ * On the server these are either absent (`window is not defined`) or, worse,
250
+ * silently present-but-wrong (`navigator` exists in modern Node, so
251
+ * `navigator.onLine` is `undefined` and a page renders as if it were offline).
252
+ * A raw `ReferenceError` at request time is the worst possible report: it
253
+ * names neither the component nor the fix. This points at the line.
254
+ */
255
+ static ssrUnsafeGlobal(name, context = {}) {
256
+ return new VeskError(`\`${name}\` is browser-only, and this component body runs during SSR.`, {
257
+ ...context,
258
+ code: context.code || 'V0501',
259
+ suggestions: [
260
+ `Wrap the part that needs the DOM in a {#client} block.`,
261
+ `Move the code into an event handler — handlers never run on the server.`,
262
+ `Do the setup in on_destroy(), which the compiler only emits on the client.`,
263
+ ],
264
+ nextSteps: [
265
+ `component ${context.inComponent ?? 'this component'}: wrap the \`${name}\` access in {#client}...{/client}.`,
266
+ `Or guard it: if (typeof ${name} !== 'undefined') { ... }`,
267
+ ],
268
+ 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.',
269
+ });
270
+ }
271
+ /**
272
+ * The second name from `track()` called as a function.
273
+ *
274
+ * `const &[count, setCount] = track(0)` returns a CELL, not a setter function, so
275
+ * `setCount(1)` throws `setCount is not a function` from generated code with
276
+ * no line of user code in the message. The cell is the writer: `.set(v)`.
277
+ */
278
+ static cellCalledAsFunction(name, context = {}) {
279
+ return new VeskError(`\`${name}\` is a cell, not a function — cells are written with \`.set()\`.`, {
280
+ ...context,
281
+ code: context.code || 'V0502',
282
+ suggestions: [
283
+ `Use \`${name}.set(...)\` instead of \`${name}(...)\`.`,
284
+ `Or use the tracked spelling inside an event handler: set(${context.value ?? 'value'}, ...)`,
285
+ ],
286
+ nextSteps: [
287
+ `const &[${context.value ?? 'value'}, ${name}] = track(0) → \`${name}.set(v)\``,
288
+ 'The value read in markup is the first name; the second is the cell object.',
289
+ ],
290
+ tip: `\`track()\` returns [value, cell]. \`${name}\` is the cell half.`,
291
+ });
292
+ }
293
+ /**
294
+ * `set(value, …)` used outside a rewritten expression.
295
+ *
296
+ * The compiler rewrites `set(count, v)` to `countCell.set(v)` inside JSX and
297
+ * event handlers — but a bare statement in the component body is NOT
298
+ * rewritten, so the runtime `set` receives the value and throws
299
+ * "Cannot read properties of undefined". Pass the CELL, not the value.
300
+ */
301
+ static setWithValueName(name, context = {}) {
302
+ return new VeskError(`\`set(${name}, …)\` needs the CELL, and \`${name}\` is the value half of track().`, {
303
+ ...context,
304
+ code: context.code || 'V0503',
305
+ suggestions: [
306
+ `Use \`set(${context.cell ?? 'cell'}, …)\` or \`${context.cell ?? 'cell'}.set(…)\`.`,
307
+ `Or move the set() into an event handler, where the compiler rewrites it for you.`,
308
+ ],
309
+ nextSteps: [
310
+ `const &[${name}, ${context.cell ?? 'cell'}] = track(0) → \`${context.cell ?? 'cell'}.set(v)\``,
311
+ ],
312
+ tip: 'set(cell, value) is only rewritten inside JSX and handlers; in a body statement it reaches the runtime untouched.',
313
+ });
314
+ }
205
315
  static serverBlockInClient(compName, context = {}) {
206
316
  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
317
  ...context,
@@ -1 +1 @@
1
- {"version":3,"file":"ir-generator.d.ts","sourceRoot":"","sources":["../src/ir-generator.ts"],"names":[],"mappings":"AACA,OAAO,EACL,MAAM,EAsBP,MAAM,uBAAuB,CAAC;AA2T/B,wBAAgB,YAAY,CAAC,MAAM,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,GAAG,CAAC;IAAC,cAAc,CAAC,EAAE,GAAG,CAAC;IAAC,UAAU,CAAC,EAAE,GAAG,EAAE,CAAA;CAAE,CAAC,GAAG,SAAS,GAAG,IAAI,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAkCnL;AA6nCD,wBAAgB,UAAU,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAqT9E"}
1
+ {"version":3,"file":"ir-generator.d.ts","sourceRoot":"","sources":["../src/ir-generator.ts"],"names":[],"mappings":"AACA,OAAO,EACL,MAAM,EAsBP,MAAM,uBAAuB,CAAC;AA2T/B,wBAAgB,YAAY,CAAC,MAAM,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,GAAG,CAAC;IAAC,cAAc,CAAC,EAAE,GAAG,CAAC;IAAC,UAAU,CAAC,EAAE,GAAG,EAAE,CAAA;CAAE,CAAC,GAAG,SAAS,GAAG,IAAI,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAkCnL;AAo+CD,wBAAgB,UAAU,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAmV9E"}