@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.
@@ -7,6 +7,7 @@ import { walk } from 'zimmerframe';
7
7
  import { parse } from '@vesk/compiler/src/parser';
8
8
  import { stripTsTypes, hasTsSyntax, isTypeOnlyStatement } from '@vesk/compiler/src/strip-ts';
9
9
  import { importModuleTarget } from '@vesk/compiler/src/tokens';
10
+ import { VeskError } from '@vesk/compiler/src/errors';
10
11
  // =============================================================
11
12
  // SSR module-value imports.
12
13
  //
@@ -246,6 +247,12 @@ export function loadSsrModule(absPath) {
246
247
  // transitive closure on every cold load (tens of seconds dev, too slow to
247
248
  // ship in prod).
248
249
  const src = readSsrSource(absPath);
250
+ // Report here, before any fallback can run. `nativeRequireFallback` and
251
+ // the raw-source substitution both "succeed" in the sense that they
252
+ // return something, which is how an unparseable module used to keep
253
+ // going and fail somewhere else entirely.
254
+ if (src?.parseError)
255
+ throw src.parseError;
249
256
  const body = (src?.ast?.body || []);
250
257
  if (src && isPureReexportBarrel(body)) {
251
258
  exportsObj = createLazyBarrelExports(buildBarrelSpec(body), dirname(absPath));
@@ -365,14 +372,31 @@ export function readSsrSource(absPath) {
365
372
  return null;
366
373
  }
367
374
  const raw = cachedRaw;
368
- let ast = null;
369
375
  try {
370
- ast = parse(raw, { filename: absPath });
371
- }
372
- catch {
373
- ast = null;
376
+ return { raw, ast: parse(raw, { filename: absPath }) };
377
+ }
378
+ catch (e) {
379
+ // A parse failure is a BUILD FAILURE, not a fallback. It used to return
380
+ // `ast: null` silently, and the caller then substituted the raw source — so
381
+ // the file's TypeScript syntax reached `new Function` and surfaced as a
382
+ // bare `Unexpected token ':'` in whichever module imported it, with no
383
+ // file, no line, and (because the module's exports came back undefined) a
384
+ // trail of unrelated `X is not iterable` failures behind it.
385
+ const err = e;
386
+ let parseError;
387
+ if (err instanceof VeskError) {
388
+ // `parse()` already produced a located diagnostic for this file. Keep its
389
+ // message, frame and position — they are strictly better than a wrapper's
390
+ // — and add the code so the failure is identifiable in build logs.
391
+ parseError = err;
392
+ if (err.code === undefined)
393
+ err.code = 'V0901';
394
+ }
395
+ else {
396
+ parseError = VeskError.moduleParseFailed({ file: absPath, reason: e?.message });
397
+ }
398
+ return { raw, ast: null, parseError };
374
399
  }
375
- return { raw, ast };
376
400
  }
377
401
  /** True when every statement in a module body is a re-export (or a type-only /
378
402
  * directive statement). Such "pure barrels" carry no computation of their own,
@@ -481,7 +505,11 @@ function createLazyBarrelExports(spec, dir) {
481
505
  const loadSource = (source) => {
482
506
  let target = targets.get(source);
483
507
  if (!target) {
484
- target = resolveSsrModule(source, dir) ?? '';
508
+ // Compiler-owned sources resolve through their package `exports` map, not
509
+ // the project's tsconfig `paths` (see `createModuleRequire`).
510
+ target = (isCompilerOwnedTarget(source)
511
+ ? resolveCompilerOwnedModule(source)
512
+ : resolveSsrModule(source, dir)) ?? '';
485
513
  if (!target)
486
514
  return null;
487
515
  targets.set(source, target);
@@ -590,6 +618,11 @@ function evaluateModuleFile(absPath, exportsObj, src) {
590
618
  let ast;
591
619
  if (src) {
592
620
  ast = src.ast;
621
+ // Report the parse failure at the file that caused it rather than letting
622
+ // the raw source reach `new Function` and fail as a SyntaxError nobody can
623
+ // trace back.
624
+ if (!ast && src.parseError)
625
+ throw src.parseError;
593
626
  }
594
627
  else {
595
628
  try {
@@ -684,8 +717,59 @@ function scanUnsupportedNode(node, inFunction = false) {
684
717
  return null;
685
718
  }
686
719
  /** Recursive `require` used inside evaluated modules, rooted at their dir. */
720
+ /**
721
+ * Resolve a compiler-owned (`@vesk/*`) specifier WITHOUT consulting tsconfig
722
+ * `paths`. The package's own `exports` map is the authority for its own
723
+ * subpaths — and it points at built JavaScript.
724
+ */
725
+ function resolveCompilerOwnedModule(specifier) {
726
+ const viaExports = resolveViaPackageExports(specifier, process.cwd());
727
+ if (viaExports)
728
+ return viaExports;
729
+ const viaModuleField = resolveViaPackageModuleField(specifier, process.cwd());
730
+ if (viaModuleField)
731
+ return viaModuleField;
732
+ const native = nativeResolve(specifier, process.cwd());
733
+ if (native && !isBuiltinPath(native))
734
+ return native;
735
+ return null;
736
+ }
687
737
  function createModuleRequire(fromDir) {
688
738
  return (specifier) => {
739
+ // `@vesk/*` is compiler-owned, and it is BUILT JavaScript by the time it
740
+ // ships (`@vesk/runtime` → `dist/index-client.js`). It must not go through
741
+ // this loader at all, which parses with the `.vsk` grammar.
742
+ //
743
+ // Both halves of that were wrong, and together they took down any module
744
+ // that imported BOTH a `@vesk/*` package and a relative module:
745
+ //
746
+ // 1. Resolution used the project's tsconfig `paths`, which map
747
+ // `@vesk/runtime/*` to `packages/runtime/src/*`, while the package's own
748
+ // `exports` map sends the same specifier to `dist/*.js`. The alias won,
749
+ // so the loader was handed raw TypeScript.
750
+ // 2. The built runtime entry is itself a re-export barrel, so it became a
751
+ // lazy Proxy. Reading any name resolved a source file and parsed it
752
+ // with the `.vsk` grammar — which rejects `component` as a reserved
753
+ // word, so even `dist/ripple-runtime.js` failed.
754
+ //
755
+ // The resulting throw escaped the proxy read and propagated out of the
756
+ // IMPORTING module's evaluation, so every one of its exports came back
757
+ // `undefined` and any `track(...)` cell in it threw
758
+ // `Cannot read properties of undefined (reading 'get')`.
759
+ //
760
+ // Native `require` is the right loader here: these packages ship plain JS
761
+ // with a real `exports` map, exactly what Node is for. The bare-only and
762
+ // relative-only cases each happened to recover via `nativeRequireFallback`,
763
+ // which is why each looked fine in isolation and only the combination broke.
764
+ if (isCompilerOwnedTarget(specifier)) {
765
+ const resolved = resolveCompilerOwnedModule(specifier);
766
+ if (resolved) {
767
+ const native = nativeRequireFallback(resolved);
768
+ if (native)
769
+ return native;
770
+ }
771
+ throw new Error(`Cannot load module '${specifier}'`);
772
+ }
689
773
  const resolved = resolveSsrModule(specifier, fromDir);
690
774
  if (!resolved)
691
775
  throw new Error(`Cannot find module '${specifier}'`);
@@ -1454,6 +1538,11 @@ export function collectModuleBundled(absPath, collector) {
1454
1538
  else {
1455
1539
  const src = readSsrSource(absPath);
1456
1540
  if (!src || !src.ast) {
1541
+ // A module we could not parse is a hard failure. Substituting the raw
1542
+ // source (the historical behavior) pushes every type annotation in this
1543
+ // file into `new Function` and blames some unrelated module later.
1544
+ if (src?.parseError)
1545
+ throw src.parseError;
1457
1546
  const raw = src ? src.raw : '// unreadable during build\nmodule.exports = {};';
1458
1547
  collector.modules.push({ key, code: raw, dir, deps: {} });
1459
1548
  return key;
package/dist/parser.d.ts CHANGED
@@ -33,6 +33,35 @@ export declare function preprocessForClauses(source: string): {
33
33
  code: string;
34
34
  annotations: VeskAnnotation[];
35
35
  };
36
+ /**
37
+ * Blanks the type parameter list of a GENERIC ARROW function:
38
+ *
39
+ * const read = <T,>(key: string, fallback: T): T => fallback;
40
+ *
41
+ * The list is type-only information the TS stripper deletes anyway, and it is
42
+ * unrepresentable in the tokenizer this parser uses: a `<` in expression
43
+ * position is read as `jsxTagStart`, and inside it the tokenizer emits
44
+ * `jsxName` / `jsxTagEnd` — never type tokens. So the construct could not be
45
+ * parsed at all, while `function f<T>(…)` and `async <T,>(…)` both could.
46
+ *
47
+ * That failure was silent rather than loud. `readSsrSource` treats a parse
48
+ * error as "not a module we understand" and substitutes the RAW, untranspiled
49
+ * source, so the parse error surfaced much later as
50
+ * `Unexpected token ':'` while the emitted module was being evaluated.
51
+ *
52
+ * Blanked IN PLACE — same character length, so every source offset, and
53
+ * therefore every AST node position, is preserved. This runs alongside the
54
+ * `{#clause}` and specifier-export passes for the same reason.
55
+ *
56
+ * The disambiguation is structural, not textual: the `>` must be followed by the
57
+ * arrow's PARAMETER list and then (after an optional return type) by `=>`. That
58
+ * is what separates `const f = <T,>(k: T): T => k` from the JSX
59
+ * `const a = <div>(hi)</div>`, where the `>` is also followed by `(` but never
60
+ * by `=>`. Type arguments (`f<T>(x)`) and comparisons fail the same test, so
61
+ * they are left untouched. A `<` inside a string, template or comment is
62
+ * skipped by the lexical scan before any of this runs.
63
+ */
64
+ export declare function blankGenericArrowTypeParams(source: string): string;
36
65
  export declare function createBaseParser(): typeof acorn.Parser;
37
66
  /**
38
67
  * Parse GENERATED JavaScript with plain acorn — no TypeScript plugin, no
@@ -1 +1 @@
1
- {"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../src/parser.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAOtC,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,YAAY,CAAC;IACnB,8CAA8C;IAC9C,QAAQ,EAAE,MAAM,CAAC;IACjB,gEAAgE;IAChE,WAAW,EAAE,MAAM,CAAC;IACpB,8CAA8C;IAC9C,SAAS,EAAE,MAAM,CAAC;IAClB,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5B,sDAAsD;IACtD,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AA0BD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,cAAc,EAAE,CAAA;CAAE,CA4LpG;AAED,wBAAgB,gBAAgB,IAAI,OAAO,KAAK,CAAC,MAAM,CAEtD;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI,CAM/D;AAED,wBAAgB,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,GAAE,YAAiB,GAAG,OAAO,CAuEzE"}
1
+ {"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../src/parser.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAOtC,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,YAAY,CAAC;IACnB,8CAA8C;IAC9C,QAAQ,EAAE,MAAM,CAAC;IACjB,gEAAgE;IAChE,WAAW,EAAE,MAAM,CAAC;IACpB,8CAA8C;IAC9C,SAAS,EAAE,MAAM,CAAC;IAClB,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5B,sDAAsD;IACtD,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AA0BD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,cAAc,EAAE,CAAA;CAAE,CA4LpG;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAwFlE;AA2DD,wBAAgB,gBAAgB,IAAI,OAAO,KAAK,CAAC,MAAM,CAEtD;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI,CAM/D;AAED,wBAAgB,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,GAAE,YAAiB,GAAG,OAAO,CAuEzE"}
package/dist/parser.js CHANGED
@@ -253,6 +253,226 @@ export function preprocessForClauses(source) {
253
253
  }
254
254
  return { code: chars.join(''), annotations };
255
255
  }
256
+ /**
257
+ * Blanks the type parameter list of a GENERIC ARROW function:
258
+ *
259
+ * const read = <T,>(key: string, fallback: T): T => fallback;
260
+ *
261
+ * The list is type-only information the TS stripper deletes anyway, and it is
262
+ * unrepresentable in the tokenizer this parser uses: a `<` in expression
263
+ * position is read as `jsxTagStart`, and inside it the tokenizer emits
264
+ * `jsxName` / `jsxTagEnd` — never type tokens. So the construct could not be
265
+ * parsed at all, while `function f<T>(…)` and `async <T,>(…)` both could.
266
+ *
267
+ * That failure was silent rather than loud. `readSsrSource` treats a parse
268
+ * error as "not a module we understand" and substitutes the RAW, untranspiled
269
+ * source, so the parse error surfaced much later as
270
+ * `Unexpected token ':'` while the emitted module was being evaluated.
271
+ *
272
+ * Blanked IN PLACE — same character length, so every source offset, and
273
+ * therefore every AST node position, is preserved. This runs alongside the
274
+ * `{#clause}` and specifier-export passes for the same reason.
275
+ *
276
+ * The disambiguation is structural, not textual: the `>` must be followed by the
277
+ * arrow's PARAMETER list and then (after an optional return type) by `=>`. That
278
+ * is what separates `const f = <T,>(k: T): T => k` from the JSX
279
+ * `const a = <div>(hi)</div>`, where the `>` is also followed by `(` but never
280
+ * by `=>`. Type arguments (`f<T>(x)`) and comparisons fail the same test, so
281
+ * they are left untouched. A `<` inside a string, template or comment is
282
+ * skipped by the lexical scan before any of this runs.
283
+ */
284
+ export function blankGenericArrowTypeParams(source) {
285
+ let changed = false;
286
+ const chars = source.split('');
287
+ let i = 0;
288
+ const isSpace = (ch) => isSpaceChar(ch);
289
+ while (i < source.length) {
290
+ const ch = source[i];
291
+ // Skip strings, templates and comments — the same lexical discipline the
292
+ // clause pass uses, so a `<T,>` inside a string or comment is never touched.
293
+ if (ch === '"' || ch === "'" || ch === '`') {
294
+ const quote = ch;
295
+ let j = i + 1;
296
+ while (j < source.length) {
297
+ if (source[j] === '\\') {
298
+ j += 2;
299
+ continue;
300
+ }
301
+ if (source[j] === quote) {
302
+ j++;
303
+ break;
304
+ }
305
+ if (quote === '`' && source[j] === '$' && source[j + 1] === '{') {
306
+ let depth = 1;
307
+ j += 2;
308
+ while (j < source.length && depth > 0) {
309
+ if (source[j] === '{')
310
+ depth++;
311
+ else if (source[j] === '}')
312
+ depth--;
313
+ j++;
314
+ }
315
+ continue;
316
+ }
317
+ j++;
318
+ }
319
+ i = j;
320
+ continue;
321
+ }
322
+ if (ch === '/' && source[i + 1] === '/') {
323
+ while (i < source.length && source[i] !== '\n')
324
+ i++;
325
+ continue;
326
+ }
327
+ if (ch === '/' && source[i + 1] === '*') {
328
+ i += 2;
329
+ while (i < source.length && !(source[i] === '*' && source[i + 1] === '/'))
330
+ i++;
331
+ i += 2;
332
+ continue;
333
+ }
334
+ if (ch !== '<') {
335
+ i++;
336
+ continue;
337
+ }
338
+ // A candidate `<` in expression position: find the matching `>`, tracking
339
+ // angle depth and skipping bracketed groups (a `>` inside `(…)`/`[…]`/`{…}`
340
+ // is a comparison or a nested type argument, never the terminator).
341
+ let j = i + 1;
342
+ let angle = 1;
343
+ let end = -1;
344
+ let bail = false;
345
+ while (j < source.length) {
346
+ const c = source[j];
347
+ if (c === '<') {
348
+ angle++;
349
+ j++;
350
+ continue;
351
+ }
352
+ if (c === '>') {
353
+ angle--;
354
+ j++;
355
+ if (angle === 0) {
356
+ end = j;
357
+ break;
358
+ }
359
+ continue;
360
+ }
361
+ if (c === '(' || c === '[' || c === '{') {
362
+ const close = c === '(' ? ')' : c === '[' ? ']' : '}';
363
+ let depth = 0;
364
+ while (j < source.length) {
365
+ const d = source[j];
366
+ if (d === c)
367
+ depth++;
368
+ else if (d === close) {
369
+ depth--;
370
+ if (depth === 0) {
371
+ j++;
372
+ break;
373
+ }
374
+ }
375
+ j++;
376
+ }
377
+ continue;
378
+ }
379
+ if (c === ';' || c === '\n') {
380
+ bail = true;
381
+ break;
382
+ }
383
+ j++;
384
+ }
385
+ if (bail || end < 0) {
386
+ i++;
387
+ continue;
388
+ }
389
+ // Must be an arrow: `>` then a parameter list, then an optional return
390
+ // type, then `=>`. Scanning to the `=>` is what distinguishes this from a
391
+ // JSX element that happens to be followed by a parenthesized text run.
392
+ if (!isArrowParameterList(source, end)) {
393
+ i = end;
394
+ continue;
395
+ }
396
+ for (let p = i; p < end; p++) {
397
+ if (chars[p] !== '\n')
398
+ chars[p] = ' ';
399
+ }
400
+ changed = true;
401
+ i = end;
402
+ }
403
+ return changed ? chars.join('') : source;
404
+ }
405
+ /**
406
+ * True when `source` continues from `from` with `( … )` — optionally followed
407
+ * by a `: ReturnType` — and then `=>`. Balances brackets so the arrow inside a
408
+ * default value (`(cb = () => 1) => …`) is not mistaken for the terminator.
409
+ */
410
+ function isArrowParameterList(source, from) {
411
+ let i = from;
412
+ while (i < source.length && isSpaceChar(source[i]))
413
+ i++;
414
+ if (source[i] !== '(')
415
+ return false;
416
+ let depth = 0;
417
+ while (i < source.length) {
418
+ const ch = source[i];
419
+ if (ch === '(' || ch === '[' || ch === '{') {
420
+ depth++;
421
+ i++;
422
+ continue;
423
+ }
424
+ if (ch === ')' || ch === ']' || ch === '}') {
425
+ depth--;
426
+ i++;
427
+ if (depth === 0)
428
+ break;
429
+ continue;
430
+ }
431
+ i++;
432
+ }
433
+ if (depth !== 0)
434
+ return false;
435
+ while (i < source.length && isSpaceChar(source[i]))
436
+ i++;
437
+ // Optional return type: `: T` up to the `=>` at depth zero.
438
+ if (source[i] === ':') {
439
+ i++;
440
+ let tdepth = 0;
441
+ while (i < source.length) {
442
+ const ch = source[i];
443
+ if (ch === '(' || ch === '[' || ch === '{') {
444
+ tdepth++;
445
+ i++;
446
+ continue;
447
+ }
448
+ if (ch === ')' || ch === ']' || ch === '}') {
449
+ if (tdepth === 0)
450
+ return false;
451
+ tdepth--;
452
+ i++;
453
+ continue;
454
+ }
455
+ if (tdepth === 0 && ch === '=' && source[i + 1] === '>')
456
+ return true;
457
+ // A statement boundary ends the scan ONLY at depth zero. An object
458
+ // LITERAL type is full of them and is perfectly legal inside a return
459
+ // annotation: `{ roles: R[]; changed: boolean }` has a `;` (and, when it
460
+ // wraps, a newline) inside the braces. Bailing on those is what made
461
+ // `<R extends { id: string }>(a: R[], b: R[]): { roles: R[]; changed:
462
+ // boolean } => a` the one generic-arrow shape that still failed — it needs
463
+ // a generic arrow AND a braced constraint AND a return annotation, which
464
+ // is why dropping any ONE of the three parsed cleanly.
465
+ if (tdepth === 0 && (ch === ';' || ch === '\n'))
466
+ return false;
467
+ i++;
468
+ }
469
+ return false;
470
+ }
471
+ return source[i] === '=' && source[i + 1] === '>';
472
+ }
473
+ function isSpaceChar(ch) {
474
+ return ch === ' ' || ch === '\t' || ch === '\n' || ch === '\r' || ch === '\f' || ch === '\v';
475
+ }
256
476
  export function createBaseParser() {
257
477
  return acorn.Parser.extend(tsPlugin({}), VeskParserPlugin());
258
478
  }
@@ -283,7 +503,7 @@ export function parseGeneratedJs(source) {
283
503
  }
284
504
  export function parse(source, options = {}) {
285
505
  const ParserClass = createBaseParser();
286
- const { code, annotations } = preprocessForClauses(blankComments(source));
506
+ const { code, annotations } = preprocessForClauses(blankGenericArrowTypeParams(blankComments(source)));
287
507
  // Acorn validates that every `export { X }` names a real top-level binding.
288
508
  // A `.vsk` component is a registry entry, not a binding, so acorn rejects
289
509
  // `export { MyComponent }`. Remove only those specifier lists — one whose
@@ -1 +1 @@
1
- {"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAM1D,MAAM,WAAW,WAAW;IAC1B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,SAAS,EAAE,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAChC;AAED;;;;;;GAMG;AACH,4DAA4D;AAC5D,wBAAgB,eAAe,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAajD;AAED,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,MAAM,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAqB3F;AAkDD,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAUnE;AAwED,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,GAAE,WAAgB,GAAG,SAAS,EAAE,CAKjF;AAMD,wBAAgB,cAAc,CAAC,aAAa,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CA2BzE;AA6HD,wBAAgB,cAAc,CAAC,IAAI,EAAE,SAAS,EAAE,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAgBrE;AAED,MAAM,WAAW,oBAAoB;IACnC,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,SAAS,EAAE,EAAE,OAAO,GAAE,oBAAyB,GAAG,MAAM,CAoCnG;AAoBD,wBAAgB,QAAQ,CAAC,IAAI,EAAE,SAAS,EAAE,EAAE,QAAQ,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAmGhF"}
1
+ {"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAM1D,MAAM,WAAW,WAAW;IAC1B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,SAAS,EAAE,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAChC;AAED;;;;;;GAMG;AACH,4DAA4D;AAC5D,wBAAgB,eAAe,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAajD;AAED,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,MAAM,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAqB3F;AAkDD,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAUnE;AAwED,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,GAAE,WAAgB,GAAG,SAAS,EAAE,CAKjF;AAMD,wBAAgB,cAAc,CAAC,aAAa,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CA2BzE;AA6HD,wBAAgB,cAAc,CAAC,IAAI,EAAE,SAAS,EAAE,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAgBrE;AAED,MAAM,WAAW,oBAAoB;IACnC,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,SAAS,EAAE,EAAE,OAAO,GAAE,oBAAyB,GAAG,MAAM,CAoCnG;AAoBD,wBAAgB,QAAQ,CAAC,IAAI,EAAE,SAAS,EAAE,EAAE,QAAQ,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAkHhF"}
package/dist/router.js CHANGED
@@ -520,6 +520,21 @@ export function matchUrl(tree, pathname) {
520
520
  pushWithStandalone(node);
521
521
  return true;
522
522
  }
523
+ // A group can also BE the page at this position: `app/(public)/page.vsk`
524
+ // serves `/`. The recursion above only ever considers the group's
525
+ // CHILDREN, and at `/` none of them match (they all carry a path
526
+ // segment), so the group was skipped entirely and the match fell
527
+ // through to the page-less root node — which produced a 404 for the
528
+ // zero-segment path while every nested route (`/about`, `/login`)
529
+ // matched fine.
530
+ //
531
+ // A group is transparent for PREFIX matching, and it has to be
532
+ // transparent here too: a group with no page at this position is not a
533
+ // match, a group WITH one is.
534
+ if (partIndex >= parts.length && node.page && node.fullPath === '/' + parts.join('/')) {
535
+ pushWithStandalone(node);
536
+ return true;
537
+ }
523
538
  continue;
524
539
  }
525
540
  if (node.fullPath === '/') {
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>>(...)`)
@@ -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;AAoDrK,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,CAgEjJ;AA4XD,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,WAAW,EAAE,aAAa,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,MAAM,CAqD1F;AAED,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,OAAO,CAAC;CAClB;AAED;;;;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"}
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;AA6DrK,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;AAobD,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"}