@vesk/compiler 0.2.50 → 0.2.52

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.
@@ -433,6 +433,64 @@ function jsxTagDepth(nameNode) {
433
433
  // `import * as ns from './x.vsk'`.
434
434
  let currentVskNamespaceLocals = null;
435
435
  let currentSourceFile;
436
+ /**
437
+ * Names bound to a component VALUE in the component's OWN body (a parameter or
438
+ * a `const`/`let`/`var`/function declared inside it) that a JSX tag may
439
+ * legally resolve to.
440
+ *
441
+ * A JSX tag was historically registry-only: `<Icon />` looked the name up in
442
+ * the component registry and threw "was not found" when it missed, even though
443
+ * `const Icon = item.icon` had bound a perfectly good component value one line
444
+ * above. That made the single most natural way to write a dynamic-icon nav
445
+ * list (`const Icon = item.icon; <li><Icon size={14} /></li>`) a 500 for the
446
+ * WHOLE route — and the error text ("declare it with the `component` keyword")
447
+ * named the one thing the author had already done correctly.
448
+ *
449
+ * Such a name is emitted with a `calleeExpr`, routing it through the SAME code
450
+ * path that already works for `<it.icon />` — the callee is invoked directly
451
+ * instead of resolved as a name.
452
+ *
453
+ * Deliberately EXCLUDES imported names and top-level file values: those
454
+ * already resolve through the `importedNames` path in both codegens, and
455
+ * giving them a callee there would take self-claiming runtime components
456
+ * (`Link`, `Form`, `Md`) off the path that reserves the caller's walker
457
+ * correctly. Registry resolution also keeps precedence: a name that is a
458
+ * declared `component` still wins.
459
+ */
460
+ let currentComponentValues = null;
461
+ /** File-wide name sets backing `isComponentValueName`. */
462
+ let currentFileComponentNames = null;
463
+ let currentFileValueBindings = null;
464
+ let currentImportedNames = null;
465
+ let currentComponentCallHelpers = null;
466
+ /** A helper body expression that is exactly one component call, or null. */
467
+ function unwrapHelperBody(node) {
468
+ let body = node;
469
+ if (body?.type === 'ArrowFunctionExpression' || body?.type === 'FunctionExpression')
470
+ body = body.body;
471
+ if (body?.type === 'BlockStatement') {
472
+ const stmts = (body.body || []).filter((s) => s.type !== 'EmptyStatement');
473
+ if (stmts.length !== 1)
474
+ return null;
475
+ body = stmts[0];
476
+ }
477
+ if (body?.type === 'ReturnStatement')
478
+ body = body.argument;
479
+ if (!body)
480
+ return null;
481
+ // Shape check only — deliberately NOT `componentCallInExpression`, which
482
+ // consults the per-component name sets. Helpers are collected once for the
483
+ // whole file, before any component's sets are installed, and whether a
484
+ // callee resolves to a component is settled at the call site instead.
485
+ if (body.type !== 'CallExpression')
486
+ return null;
487
+ if (body.callee?.type !== 'Identifier')
488
+ return null;
489
+ const args = body.arguments || [];
490
+ if (args.length === 0 || args[0]?.type !== 'ObjectExpression')
491
+ return null;
492
+ return body;
493
+ }
436
494
  /**
437
495
  * Local names bound by `import * as ns from './x.vsk'` that are NOT shadowed by
438
496
  * any other binding in the file. A `.vsk` namespace is only special when the
@@ -489,6 +547,61 @@ function markShadowedBindings(node, namespaceLocals) {
489
547
  }
490
548
  }
491
549
  /** Names a single binding-bearing node introduces. */
550
+ /**
551
+ * Every top-level VALUE binding in the file (excluding `component`
552
+ * declarations — those are registry entries, not values, and resolve by name).
553
+ * Pre-scanned rather than read out of `topLevelCode`, which fills in as the
554
+ * top-level loop runs and would therefore miss a binding declared after the
555
+ * component currently being compiled.
556
+ */
557
+ function collectFileValueBindings(ast) {
558
+ const names = new Set();
559
+ for (const node of (ast.body || [])) {
560
+ if (node.type === 'ComponentDeclaration')
561
+ continue;
562
+ if (node.type === 'ImportDeclaration')
563
+ continue;
564
+ const target = node.type === 'ExportNamedDeclaration' || node.type === 'ExportDefaultDeclaration'
565
+ ? node.declaration
566
+ : node;
567
+ if (!target)
568
+ continue;
569
+ if (target.type === 'VariableDeclaration') {
570
+ for (const d of target.declarations || [])
571
+ for (const n of declaredBindingNames(d))
572
+ names.add(n);
573
+ continue;
574
+ }
575
+ for (const n of declaredBindingNames(target))
576
+ names.add(n);
577
+ }
578
+ return names;
579
+ }
580
+ /**
581
+ * Every value binding a tag inside one component body could resolve to —
582
+ * the component's own parameters and body-local declarations only.
583
+ */
584
+ function collectComponentValueNames(compNode) {
585
+ const names = new Set();
586
+ if (!compNode)
587
+ return names;
588
+ for (const p of compNode.params || [])
589
+ for (const n of declaredBindingNames(p))
590
+ names.add(n);
591
+ for (const stmt of (compNode.body?.body || [])) {
592
+ // A body statement is the DECLARATION (`const Icon = …`), and
593
+ // `declaredBindingNames` reads declarators, so unwrap first.
594
+ if (stmt.type === 'VariableDeclaration') {
595
+ for (const d of stmt.declarations || [])
596
+ for (const n of declaredBindingNames(d))
597
+ names.add(n);
598
+ continue;
599
+ }
600
+ for (const n of declaredBindingNames(stmt))
601
+ names.add(n);
602
+ }
603
+ return names;
604
+ }
492
605
  function declaredBindingNames(node) {
493
606
  const out = [];
494
607
  const pushPattern = (id) => {
@@ -582,9 +695,227 @@ function findNamespaceValueUse(node, locals) {
582
695
  }
583
696
  return null;
584
697
  }
698
+ /**
699
+ * Build the helper table from the file's top-level const/function bindings.
700
+ * Only single-expression component-call bodies with plain identifier params
701
+ * are recorded — see `ComponentCallHelper` for why the rest are excluded.
702
+ */
703
+ function collectComponentCallHelpers(source, ast) {
704
+ const out = new Map();
705
+ for (const node of (ast.body || [])) {
706
+ const decl = node.type === 'ExportNamedDeclaration' || node.type === 'ExportDefaultDeclaration'
707
+ ? node.declaration
708
+ : node;
709
+ if (!decl)
710
+ continue;
711
+ let name = null;
712
+ let fn = null;
713
+ if (decl.type === 'VariableDeclaration' && decl.declarations?.length === 1) {
714
+ const d = decl.declarations[0];
715
+ if (d.id?.type === 'Identifier') {
716
+ name = d.id.name;
717
+ fn = d.init;
718
+ }
719
+ }
720
+ else if (decl.type === 'FunctionDeclaration' && decl.id?.name) {
721
+ name = decl.id.name;
722
+ fn = decl;
723
+ }
724
+ if (!name || !fn)
725
+ continue;
726
+ if (fn.type !== 'ArrowFunctionExpression' && fn.type !== 'FunctionExpression' && fn.type !== 'FunctionDeclaration')
727
+ continue;
728
+ // A rest/default/destructured parameter cannot be substituted by a simple
729
+ // identifier match, so such helpers are left as ordinary calls.
730
+ const params = fn.params || [];
731
+ if (params.some((p) => p.type !== 'Identifier'))
732
+ continue;
733
+ const body = unwrapHelperBody(fn);
734
+ if (body === null)
735
+ continue;
736
+ const args = body.arguments || [];
737
+ const propsObj = args[0];
738
+ const props = [];
739
+ const spreadProps = [];
740
+ const slots = [];
741
+ for (const prop of (propsObj.properties || [])) {
742
+ if (prop.type === 'SpreadElement') {
743
+ spreadProps.push(prop.argument);
744
+ continue;
745
+ }
746
+ const pname = prop.key.type === 'Identifier' ? prop.key.name : prop.key.value;
747
+ if (prop.value.type === 'JSXElement' || prop.value.type === 'JSXFragment') {
748
+ slots.push({ name: pname, nodes: exprToIR(source, prop.value) });
749
+ continue;
750
+ }
751
+ props.push({ name: pname, value: prop.value });
752
+ }
753
+ const paramNames = params.map((p) => p.name);
754
+ out.set(name, { props, spreadProps, slots, paramNames, componentName: body.callee.name });
755
+ }
756
+ return out;
757
+ }
758
+ /**
759
+ * Replace every identifier in `node` that matches a helper parameter with the
760
+ * argument passed at the call site. Identifier-only and AST-level (the compiler
761
+ * never rewrites source by pattern), and it returns null if the tree holds
762
+ * anything it cannot prove is a value — e.g. a nested function that closes over
763
+ * the parameter, where substituting would change what runs.
764
+ */
765
+ function substituteHelperParams(node, bindings) {
766
+ if (!node || typeof node !== 'object')
767
+ return node;
768
+ if (Array.isArray(node)) {
769
+ const out = [];
770
+ for (const child of node) {
771
+ const next = substituteHelperParams(child, bindings);
772
+ if (next === null)
773
+ return null;
774
+ out.push(next);
775
+ }
776
+ return out;
777
+ }
778
+ if (typeof node.type !== 'string')
779
+ return node;
780
+ // A nested function body runs later and may capture the parameter; leave the
781
+ // whole helper alone rather than risk changing its meaning.
782
+ if (node.type === 'ArrowFunctionExpression' || node.type === 'FunctionExpression')
783
+ return null;
784
+ if (node.type === 'Identifier' && bindings.has(node.name)) {
785
+ const bound = bindings.get(node.name);
786
+ return bound === null ? null : bound;
787
+ }
788
+ const out = {};
789
+ for (const key of Object.keys(node)) {
790
+ if (key === 'loc' || key === 'range' || key === 'start' || key === 'end') {
791
+ out[key] = node[key];
792
+ continue;
793
+ }
794
+ const next = substituteHelperParams(node[key], bindings);
795
+ if (next === null)
796
+ return null;
797
+ out[key] = next;
798
+ }
799
+ return out;
800
+ }
801
+ /** Lower a call to a `ComponentCallHelper` into a real `ComponentCall`. */
802
+ function helperCallToIR(source, helper, args) {
803
+ // The rendered component is the one INSIDE the helper, not the helper itself.
804
+ const compName = helper.componentName;
805
+ // First argument is the props object INSIDE the helper; the caller's
806
+ // arguments fill the helper's parameters from index 0.
807
+ const bindings = new Map();
808
+ helper.paramNames.forEach((param, i) => bindings.set(param, args[i] ?? null));
809
+ for (const bound of bindings.values())
810
+ if (bound === null)
811
+ return null;
812
+ const props = [];
813
+ for (const p of helper.props) {
814
+ const substituted = substituteHelperParams(p.value, bindings);
815
+ if (substituted === null)
816
+ return null;
817
+ props.push({ name: p.name, value: toExpression(source, substituted) });
818
+ }
819
+ const spreadProps = [];
820
+ for (const sp of helper.spreadProps) {
821
+ const substituted = substituteHelperParams(sp, bindings);
822
+ if (substituted === null)
823
+ return null;
824
+ spreadProps.push(toExpression(source, substituted));
825
+ }
826
+ return [new ComponentCall(compName, props, helper.slots.flatMap((s) => [new PropSlot(s.name, s.nodes)]), spreadProps, -1, componentValueCallee(compName))];
827
+ }
585
828
  function isHTMLTag(name) {
586
829
  return name.length > 0 && name[0] === name[0].toLowerCase();
587
830
  }
831
+ /**
832
+ * The `calleeExpr` for a bare-identifier JSX tag, or null when the name must
833
+ * resolve through the component registry.
834
+ *
835
+ * Null is the default and the reason every existing tag is untouched: only a
836
+ * name bound to a value in this component's scope gets a callee, so
837
+ * `component Icon {}` / imported components keep registry semantics (with
838
+ * their "not found" diagnostic), while `const Icon = item.icon` — which used to
839
+ * throw and take the whole route down with it — resolves to the value it is.
840
+ */
841
+ function componentValueCallee(tagName) {
842
+ if (!currentComponentValues || !currentComponentValues.has(tagName))
843
+ return null;
844
+ return tagName;
845
+ }
846
+ /**
847
+ * A component invoked as a function in expression position, or null.
848
+ *
849
+ * The callee must be a plain identifier that names a component — an import, a
850
+ * top-level file value, a body-local binding, or a declared `component` — and
851
+ * the first argument an object literal, the shape `Terminal({ size: 14 })` has.
852
+ *
853
+ * This is a WIDER name test than `componentValueCallee`, and deliberately so:
854
+ * a tag only needs a callee for names nothing else can resolve, but a CALL has
855
+ * no other lowering at all. The first argument must be an object literal
856
+ * because that is what distinguishes a component invocation from an ordinary
857
+ * call on a same-named helper, and the callee must be a known component name
858
+ * for the same reason — `doThing(x)` on a local helper stays an ordinary call.
859
+ */
860
+ function componentCallInExpression(expr) {
861
+ if (!expr || expr.type !== 'CallExpression')
862
+ return null;
863
+ const callee = expr.callee;
864
+ if (!callee || callee.type !== 'Identifier')
865
+ return null;
866
+ if (!isComponentValueName(callee.name))
867
+ return null;
868
+ const args = expr.arguments || [];
869
+ if (args.length === 0 || !args[0] || args[0].type !== 'ObjectExpression')
870
+ return null;
871
+ return expr;
872
+ }
873
+ /**
874
+ * Every name in the file that denotes a component: declared `component`s,
875
+ * imports, top-level file values, and the current component's body-local
876
+ * bindings. Used only to recognize a component call — never to give a tag a
877
+ * callee (that is `componentValueCallee`'s narrower job).
878
+ */
879
+ function isComponentValueName(name) {
880
+ if (currentFileComponentNames?.has(name))
881
+ return true;
882
+ if (currentComponentValues?.has(name))
883
+ return true;
884
+ return currentFileValueBindings?.has(name) || currentImportedNames?.has(name) || false;
885
+ }
886
+ /**
887
+ * Lower `Comp({ a: 1, ...rest })` to a `ComponentCall`. Props/spreads map
888
+ * straight onto the component-call channels; a slot-shaped value (`children`)
889
+ * is routed through `PropSlot` so named content keeps working when a component
890
+ * is invoked as a function rather than used as a tag.
891
+ */
892
+ function componentCallToIR(source, expr) {
893
+ const calleeName = expr.callee.name;
894
+ const args = expr.arguments || [];
895
+ const propsArg = args[0];
896
+ const props = [];
897
+ const spreadProps = [];
898
+ const slots = [];
899
+ for (const prop of (propsArg.properties || [])) {
900
+ if (prop.type === 'SpreadElement') {
901
+ spreadProps.push(toExpression(source, prop.argument));
902
+ continue;
903
+ }
904
+ const name = prop.key.type === 'Identifier' ? prop.key.name : prop.key.value;
905
+ if (prop.value.type === 'JSXElement' || prop.value.type === 'JSXFragment') {
906
+ slots.push(new PropSlot(name, exprToIR(source, prop.value)));
907
+ continue;
908
+ }
909
+ props.push({ name, value: toExpression(source, prop.value) });
910
+ }
911
+ // Extra positional arguments are the component ABI (`props`, registry,
912
+ // walker) that only the compiler emits; a caller's own extra args are not
913
+ // something to forward, and silently dropping them would hide a mistake.
914
+ const children = args.length > 1
915
+ ? [new RuntimeStatement(`/* [vesk] extra arguments to a component call are ignored */ ${getSource(source, args[1])}`)]
916
+ : [];
917
+ return [new ComponentCall(calleeName, props, [...children, ...slots], spreadProps, expr.start ?? -1, componentValueCallee(calleeName))];
918
+ }
588
919
  function isMapCall(expr) {
589
920
  return (expr.type === 'CallExpression' &&
590
921
  expr.callee.type === 'MemberExpression' &&
@@ -926,7 +1257,10 @@ function processJSXChildren(source, children) {
926
1257
  continue;
927
1258
  }
928
1259
  }
929
- result.push(new DynamicBinding(toExpression(source, expr)));
1260
+ // Routed through exprToIR so a component invoked as a function (and a
1261
+ // module-scope helper returning one) becomes a real component render
1262
+ // rather than text that gets escaped into visible markup.
1263
+ result.push(...exprToIR(source, expr));
930
1264
  i++;
931
1265
  }
932
1266
  else if (child.type === 'JSXElement') {
@@ -990,6 +1324,30 @@ function exprToIR(source, expr) {
990
1324
  }
991
1325
  if (expr.type === 'ParenthesizedExpression')
992
1326
  return exprToIR(source, expr.expression);
1327
+ // A component INVOKED as a function in expression position
1328
+ // (`{Terminal({ size: 14 })}`, or any branch of a ternary/`&&`) used to
1329
+ // degrade to a text binding, and the server escaped the component's HTML
1330
+ // string into visible page text — the worst failure mode of the three,
1331
+ // because it looks like content. `{cond ? Terminal({ size: 14 }) : null}`
1332
+ // was worse still: the escaped markup rendered as nothing at all, so a
1333
+ // missing icon shipped silently.
1334
+ //
1335
+ // A component's return value is markup, not text, so the call must be a real
1336
+ // component render. It is lowered to the SAME `ComponentCall` node a `<Tag />`
1337
+ // would produce, which means it inherits the hydration marker, the claim
1338
+ // path, and the fragment flattening that the tag form already relies on —
1339
+ // rather than needing a second, parallel implementation.
1340
+ if (componentCallInExpression(expr) !== null)
1341
+ return componentCallToIR(source, expr);
1342
+ // A module-scope helper that just returns a component call is the same
1343
+ // component render one level removed — inline it (see
1344
+ // `ComponentCallHelper`) rather than let its HTML be escaped into the page.
1345
+ if (expr?.type === 'CallExpression' && currentComponentCallHelpers?.has(expr.callee?.name)) {
1346
+ const helper = currentComponentCallHelpers.get(expr.callee.name);
1347
+ const inlined = helperCallToIR(source, helper, expr.arguments || []);
1348
+ if (inlined !== null)
1349
+ return inlined;
1350
+ }
993
1351
  // Nested conditionals/`&&` with JSX branches become nested dynamic regions
994
1352
  // so `a ? <X/> : b ? <Y/> : <Z/>` compiles recursively instead of degrading
995
1353
  // to a raw text binding.
@@ -1093,12 +1451,12 @@ function processJSXElement(source, element) {
1093
1451
  }
1094
1452
  if (!isHTMLTag(tagName) && selfClosing) {
1095
1453
  const { props, spreadProps, slots } = extractProps(source, element);
1096
- return [new ComponentCall(tagName, props, slots.map((s) => new PropSlot(s.name, s.nodes)), spreadProps, element.start)];
1454
+ return [new ComponentCall(tagName, props, slots.map((s) => new PropSlot(s.name, s.nodes)), spreadProps, element.start, componentValueCallee(tagName))];
1097
1455
  }
1098
1456
  if (!isHTMLTag(tagName)) {
1099
1457
  const { props, spreadProps, slots } = extractProps(source, element);
1100
1458
  const children = processJSXChildren(source, element.children || []);
1101
- return [new ComponentCall(tagName, props, [...children, ...slots.map((s) => new PropSlot(s.name, s.nodes))], spreadProps, element.start)];
1459
+ return [new ComponentCall(tagName, props, [...children, ...slots.map((s) => new PropSlot(s.name, s.nodes))], spreadProps, element.start, componentValueCallee(tagName))];
1102
1460
  }
1103
1461
  const attributes = element.openingElement.attributes
1104
1462
  .filter((attr) => attr.type !== 'JSXSpreadAttribute')
@@ -1635,6 +1993,20 @@ export function generateIR(ast, source, filename) {
1635
1993
  const vskNamespaceLocals = collectVskNamespaceLocals(ast);
1636
1994
  const prevNamespaceLocals = currentVskNamespaceLocals;
1637
1995
  const prevSourceFile = currentSourceFile;
1996
+ // Pre-scan the two name sets the per-component value resolution needs, so
1997
+ // order in the file does not matter.
1998
+ const fileValueBindings = collectFileValueBindings(ast);
1999
+ currentComponentCallHelpers = collectComponentCallHelpers(source, ast);
2000
+ const prevFileComponentNames = currentFileComponentNames;
2001
+ const prevFileValueBindings = currentFileValueBindings;
2002
+ const prevImportedNames = currentImportedNames;
2003
+ const fileComponentNames = new Set((ast.body || [])
2004
+ .map((n) => (n.type === 'ExportNamedDeclaration' || n.type === 'ExportDefaultDeclaration' ? n.declaration : n))
2005
+ .filter((n) => n?.type === 'ComponentDeclaration')
2006
+ .map((n) => n.id.name));
2007
+ currentFileComponentNames = fileComponentNames;
2008
+ currentFileValueBindings = fileValueBindings;
2009
+ currentImportedNames = importedNames;
1638
2010
  currentVskNamespaceLocals = vskNamespaceLocals;
1639
2011
  currentSourceFile = file;
1640
2012
  try {
@@ -1774,76 +2146,87 @@ export function generateIR(ast, source, filename) {
1774
2146
  }
1775
2147
  const name = inner.id.name;
1776
2148
  const paramNames = getParamNames(inner.params, source);
1777
- // A plain-identifier first parameter receives the whole props object under
1778
- // its own name (React-style); a destructuring pattern receives the fields.
1779
- const firstParam = inner.params?.[0];
1780
- const propsAlias = firstParam && firstParam.type === 'Identifier' ? firstParam.name : null;
1781
- const propsType = getPropsType(inner.params, source);
1782
- const bodyStmts = inner.body.body;
1783
- const isClientComp = !!inner.client;
1784
- // Slot props are declared at the component boundary (`trigger: Component`),
1785
- // so content reads of `props.<name>` compile to slot renders instead of
1786
- // value bindings for THIS component only.
1787
- const prevSlotProps = __slotProps;
1788
- __slotProps = slotPropNamesFromType(propsType);
1789
- __propsParam = propsAlias || null;
1790
- if (isStatementMode(bodyStmts)) {
1791
- const raw = processStatementModeBody(source, bodyStmts, file);
1792
- const { body, css } = extractStyle(raw);
1793
- validateBlocks(name, isClientComp, body, file, source);
1794
- const comp = new ComponentIR(name, paramNames, body, { mode: 'statement', exported, defaultExport, isClient: inner.client, isAsync: inner.async, ssrAwait: componentUsesFetch(body), propsType, propsAlias });
1795
- comp.style = css;
1796
- components.push(comp);
1797
- __slotProps = prevSlotProps;
1798
- }
1799
- else {
1800
- const guardClauses = [];
1801
- let mainReturn = null;
1802
- const preamble = [];
1803
- for (const stmt of bodyStmts) {
1804
- if (stmt.type === 'VeskBlock') {
1805
- const innerBody = processStatementModeBody(source, stmt.body, file);
1806
- if (stmt.tag === 'server') {
1807
- preamble.push(new ServerBlock(innerBody));
2149
+ // Value bindings a tag/call in THIS body may resolve to (see
2150
+ // `currentComponentValues`). Registry components are excluded so a declared
2151
+ // `component Icon` keeps winning over any same-named value binding.
2152
+ const valueNames = collectComponentValueNames(inner);
2153
+ const prevComponentValues = currentComponentValues;
2154
+ currentComponentValues = new Set([...valueNames].filter((n) => !fileComponentNames.has(n)));
2155
+ try {
2156
+ // A plain-identifier first parameter receives the whole props object under
2157
+ // its own name (React-style); a destructuring pattern receives the fields.
2158
+ const firstParam = inner.params?.[0];
2159
+ const propsAlias = firstParam && firstParam.type === 'Identifier' ? firstParam.name : null;
2160
+ const propsType = getPropsType(inner.params, source);
2161
+ const bodyStmts = inner.body.body;
2162
+ const isClientComp = !!inner.client;
2163
+ // Slot props are declared at the component boundary (`trigger: Component`),
2164
+ // so content reads of `props.<name>` compile to slot renders instead of
2165
+ // value bindings for THIS component only.
2166
+ const prevSlotProps = __slotProps;
2167
+ __slotProps = slotPropNamesFromType(propsType);
2168
+ __propsParam = propsAlias || null;
2169
+ if (isStatementMode(bodyStmts)) {
2170
+ const raw = processStatementModeBody(source, bodyStmts, file);
2171
+ const { body, css } = extractStyle(raw);
2172
+ validateBlocks(name, isClientComp, body, file, source);
2173
+ const comp = new ComponentIR(name, paramNames, body, { mode: 'statement', exported, defaultExport, isClient: inner.client, isAsync: inner.async, ssrAwait: componentUsesFetch(body), propsType, propsAlias });
2174
+ comp.style = css;
2175
+ components.push(comp);
2176
+ __slotProps = prevSlotProps;
2177
+ }
2178
+ else {
2179
+ const guardClauses = [];
2180
+ let mainReturn = null;
2181
+ const preamble = [];
2182
+ for (const stmt of bodyStmts) {
2183
+ if (stmt.type === 'VeskBlock') {
2184
+ const innerBody = processStatementModeBody(source, stmt.body, file);
2185
+ if (stmt.tag === 'server') {
2186
+ preamble.push(new ServerBlock(innerBody));
2187
+ }
2188
+ else if (stmt.tag === 'client') {
2189
+ preamble.push(new ClientBlock(innerBody));
2190
+ }
1808
2191
  }
1809
- else if (stmt.tag === 'client') {
1810
- preamble.push(new ClientBlock(innerBody));
2192
+ else if (stmt.type === 'ReturnStatement') {
2193
+ mainReturn = stmt;
2194
+ }
2195
+ else if (isTrackDeclaration(stmt)) {
2196
+ const elements = stmt.declarations[0].id.elements;
2197
+ const trackName = elements[0]?.name;
2198
+ const rawName = elements.length > 1 ? elements[1]?.name : null;
2199
+ const init = getSource(source, stmt.declarations[0].init);
2200
+ if (trackName)
2201
+ preamble.push(new TrackDecl(trackName, init, rawName));
2202
+ const refName = getComponentRefName(stmt);
2203
+ if (refName)
2204
+ preamble.push(new ComponentRef(refName));
2205
+ }
2206
+ else if (stmt.type === 'IfStatement' && !mainReturn && stmt.consequent.type !== 'ThrowStatement') {
2207
+ guardClauses.push(stmt);
2208
+ }
2209
+ else if (stmt.type === 'ClassDeclaration') {
2210
+ const { line, column } = offsetToLineCol(source, stmt.start ?? 0);
2211
+ throw VeskError.classDecl({ file, line, column, code: codeFrame(source, line, column) });
2212
+ }
2213
+ else {
2214
+ const raw = getSource(source, stmt);
2215
+ if (raw)
2216
+ preamble.push(new RuntimeStatement(raw, stmt, source));
1811
2217
  }
1812
2218
  }
1813
- else if (stmt.type === 'ReturnStatement') {
1814
- mainReturn = stmt;
1815
- }
1816
- else if (isTrackDeclaration(stmt)) {
1817
- const elements = stmt.declarations[0].id.elements;
1818
- const trackName = elements[0]?.name;
1819
- const rawName = elements.length > 1 ? elements[1]?.name : null;
1820
- const init = getSource(source, stmt.declarations[0].init);
1821
- if (trackName)
1822
- preamble.push(new TrackDecl(trackName, init, rawName));
1823
- const refName = getComponentRefName(stmt);
1824
- if (refName)
1825
- preamble.push(new ComponentRef(refName));
1826
- }
1827
- else if (stmt.type === 'IfStatement' && !mainReturn && stmt.consequent.type !== 'ThrowStatement') {
1828
- guardClauses.push(stmt);
1829
- }
1830
- else if (stmt.type === 'ClassDeclaration') {
1831
- const { line, column } = offsetToLineCol(source, stmt.start ?? 0);
1832
- throw VeskError.classDecl({ file, line, column, code: codeFrame(source, line, column) });
1833
- }
1834
- else {
1835
- const raw = getSource(source, stmt);
1836
- if (raw)
1837
- preamble.push(new RuntimeStatement(raw, stmt, source));
1838
- }
2219
+ const guardBody = buildGuardChain(source, guardClauses, mainReturn);
2220
+ const { body, css } = extractStyle([...preamble, ...guardBody]);
2221
+ validateBlocks(name, isClientComp, body, file, source);
2222
+ const comp = new ComponentIR(name, paramNames, body, { exported, defaultExport, isClient: inner.client, isAsync: inner.async, ssrAwait: componentUsesFetch(body), propsType, propsAlias });
2223
+ comp.style = css;
2224
+ components.push(comp);
2225
+ __slotProps = prevSlotProps;
1839
2226
  }
1840
- const guardBody = buildGuardChain(source, guardClauses, mainReturn);
1841
- const { body, css } = extractStyle([...preamble, ...guardBody]);
1842
- validateBlocks(name, isClientComp, body, file, source);
1843
- const comp = new ComponentIR(name, paramNames, body, { exported, defaultExport, isClient: inner.client, isAsync: inner.async, ssrAwait: componentUsesFetch(body), propsType, propsAlias });
1844
- comp.style = css;
1845
- components.push(comp);
1846
- __slotProps = prevSlotProps;
2227
+ }
2228
+ finally {
2229
+ currentComponentValues = prevComponentValues;
1847
2230
  }
1848
2231
  }
1849
2232
  const asyncNames = new Set(components.filter((c) => c.isAsync || c.ssrAwait).map((c) => c.name));
@@ -1915,5 +2298,9 @@ export function generateIR(ast, source, filename) {
1915
2298
  finally {
1916
2299
  currentVskNamespaceLocals = prevNamespaceLocals;
1917
2300
  currentSourceFile = prevSourceFile;
2301
+ currentFileComponentNames = prevFileComponentNames;
2302
+ currentFileValueBindings = prevFileValueBindings;
2303
+ currentImportedNames = prevImportedNames;
2304
+ currentComponentCallHelpers = null;
1918
2305
  }
1919
2306
  }
@@ -1,4 +1,5 @@
1
1
  import { parse } from '@vesk/compiler/src/parser';
2
+ import { VeskError } from '@vesk/compiler/src/errors';
2
3
  /** True when the import target is owned by the framework (never SSR-loaded). */
3
4
  export declare function isCompilerOwnedTarget(target: string): boolean;
4
5
  /** True when the target carries no value into the SSR scope. */
@@ -37,10 +38,13 @@ export declare function applyLocalModuleImports(__vesk: Record<string, unknown>,
37
38
  * Builtin markers load through Node and are cached separately (never stale).
38
39
  */
39
40
  export declare function loadSsrModule(absPath: string): Record<string, unknown> | null;
40
- export declare function readSsrSource(absPath: string): {
41
+ export interface SsrSource {
41
42
  raw: string;
42
43
  ast: ReturnType<typeof parse> | null;
43
- } | null;
44
+ /** Set when `raw` could not be parsed; see `moduleParseFailed`. */
45
+ parseError?: VeskError;
46
+ }
47
+ export declare function readSsrSource(absPath: string): SsrSource | null;
44
48
  /** Nearest `tsconfig.json` at or above `fromDir`, or null. */
45
49
  export declare function findTsconfigPath(fromDir: string): string | null;
46
50
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"module-imports.d.ts","sourceRoot":"","sources":["../src/module-imports.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"module-imports.d.ts","sourceRoot":"","sources":["../src/module-imports.ts"],"names":[],"mappings":"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"}