@vesk/compiler 0.2.42 → 0.2.44

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.
Files changed (42) hide show
  1. package/dist/actions.d.ts.map +1 -1
  2. package/dist/actions.js +17 -1
  3. package/dist/client-codegen.d.ts +25 -0
  4. package/dist/client-codegen.d.ts.map +1 -1
  5. package/dist/client-codegen.js +225 -15
  6. package/dist/errors.d.ts +14 -0
  7. package/dist/errors.d.ts.map +1 -1
  8. package/dist/errors.js +36 -0
  9. package/dist/ir-generator.d.ts.map +1 -1
  10. package/dist/ir-generator.js +466 -199
  11. package/dist/ir.d.ts +22 -1
  12. package/dist/ir.d.ts.map +1 -1
  13. package/dist/ir.js +18 -1
  14. package/dist/parser.d.ts.map +1 -1
  15. package/dist/parser.js +25 -1
  16. package/dist/precompile-runtime.d.ts.map +1 -1
  17. package/dist/precompile-runtime.js +4 -0
  18. package/dist/precompile.js +6 -2
  19. package/dist/server-jsgen.d.ts.map +1 -1
  20. package/dist/server-jsgen.js +8 -1
  21. package/dist/server-render.d.ts.map +1 -1
  22. package/dist/server-render.js +138 -56
  23. package/dist/server-utils.d.ts.map +1 -1
  24. package/dist/server-utils.js +33 -5
  25. package/dist/ssr-store.d.ts +60 -0
  26. package/dist/ssr-store.d.ts.map +1 -1
  27. package/dist/ssr-store.js +157 -6
  28. package/dist/tokens.d.ts +28 -0
  29. package/dist/tokens.d.ts.map +1 -1
  30. package/dist/tokens.js +200 -19
  31. package/dist/types.d.ts +1 -1
  32. package/dist/types.d.ts.map +1 -1
  33. package/dist/vesk-plugin.d.ts +2 -0
  34. package/dist/vesk-plugin.d.ts.map +1 -1
  35. package/dist/vesk-plugin.js +28 -1
  36. package/dist/vsk-collision.d.ts +44 -0
  37. package/dist/vsk-collision.d.ts.map +1 -0
  38. package/dist/vsk-collision.js +73 -0
  39. package/dist/vsk-imports.d.ts +51 -0
  40. package/dist/vsk-imports.d.ts.map +1 -1
  41. package/dist/vsk-imports.js +106 -9
  42. package/package.json +2 -2
@@ -400,6 +400,188 @@ function getJSXTagName(nameNode) {
400
400
  }
401
401
  return 'unknown';
402
402
  }
403
+ /** Root identifier of a dotted JSX tag: `NS.Icon` -> `NS`. */
404
+ function jsxTagRootName(nameNode) {
405
+ let node = nameNode;
406
+ while (node && node.type === 'JSXMemberExpression')
407
+ node = node.object;
408
+ return node && node.type === 'JSXIdentifier' ? node.name : '';
409
+ }
410
+ /** The final property of a dotted JSX tag: `NS.A.B` → `B`. */
411
+ function jsxTagMemberName(nameNode) {
412
+ let node = nameNode;
413
+ while (node && node.type === 'JSXMemberExpression') {
414
+ node = node.property;
415
+ if (node && node.type === 'JSXIdentifier')
416
+ return node.name;
417
+ }
418
+ return '';
419
+ }
420
+ /** How many member hops a dotted tag has: `NS.A` → 1, `NS.A.B` → 2. */
421
+ function jsxTagDepth(nameNode) {
422
+ let depth = 0;
423
+ let node = nameNode;
424
+ while (node && node.type === 'JSXMemberExpression') {
425
+ depth++;
426
+ node = node.object;
427
+ }
428
+ return depth;
429
+ }
430
+ // Set for the duration of one `generateIR` call so `processJSXElement` — which
431
+ // is reached from eleven call sites and would otherwise need a context
432
+ // parameter threaded through all of them — can reject `<ns.Tag />` for a
433
+ // `import * as ns from './x.vsk'`.
434
+ let currentVskNamespaceLocals = null;
435
+ let currentSourceFile;
436
+ /**
437
+ * Local names bound by `import * as ns from './x.vsk'` that are NOT shadowed by
438
+ * any other binding in the file. A `.vsk` namespace is only special when the
439
+ * name still refers to the import: if a parameter or a local shadows it, the
440
+ * tag is an ordinary member expression on a real object and must keep the
441
+ * `calleeExpr` path. Names the file re-binds are therefore dropped here, which
442
+ * also keeps the value-position check from rejecting valid shadowed code.
443
+ */
444
+ function collectVskNamespaceLocals(ast) {
445
+ const namespaceLocals = new Map();
446
+ for (const node of (ast.body || [])) {
447
+ if (node.type !== 'ImportDeclaration')
448
+ continue;
449
+ if (typeof node.source?.value !== 'string' || !node.source.value.endsWith('.vsk'))
450
+ continue;
451
+ for (const spec of node.specifiers || []) {
452
+ if (spec.type === 'ImportNamespaceSpecifier' && spec.local?.name) {
453
+ namespaceLocals.set(spec.local.name, { path: node.source.value, shadowed: false });
454
+ }
455
+ }
456
+ }
457
+ if (namespaceLocals.size === 0)
458
+ return new Set();
459
+ markShadowedBindings(ast, namespaceLocals);
460
+ return new Set([...namespaceLocals].filter(([, v]) => !v.shadowed).map(([k]) => k));
461
+ }
462
+ /**
463
+ * Flags every namespace local that the file also binds by some other means.
464
+ * The namespace import itself is the only declaration that does NOT shadow —
465
+ * everything else (var/let/const, function and class declarations, any
466
+ * parameter list, catch bindings) means the name is a plain local from here on.
467
+ */
468
+ function markShadowedBindings(node, namespaceLocals) {
469
+ if (!node || typeof node !== 'object')
470
+ return;
471
+ if (Array.isArray(node)) {
472
+ for (const child of node)
473
+ markShadowedBindings(child, namespaceLocals);
474
+ return;
475
+ }
476
+ if (typeof node.type === 'string') {
477
+ if (node.type === 'ImportNamespaceSpecifier')
478
+ return; // the import itself
479
+ for (const name of declaredBindingNames(node)) {
480
+ const entry = namespaceLocals.get(name);
481
+ if (entry)
482
+ entry.shadowed = true;
483
+ }
484
+ }
485
+ for (const key of Object.keys(node)) {
486
+ if (key === 'loc' || key === 'range' || key === 'start' || key === 'end')
487
+ continue;
488
+ markShadowedBindings(node[key], namespaceLocals);
489
+ }
490
+ }
491
+ /** Names a single binding-bearing node introduces. */
492
+ function declaredBindingNames(node) {
493
+ const out = [];
494
+ const pushPattern = (id) => {
495
+ if (!id)
496
+ return;
497
+ if (id.type === 'Identifier') {
498
+ out.push(id.name);
499
+ return;
500
+ }
501
+ if (id.type === 'ObjectPattern') {
502
+ for (const prop of id.properties || []) {
503
+ if (prop.type === 'RestElement')
504
+ pushPattern(prop.argument);
505
+ else
506
+ pushPattern(prop.value);
507
+ }
508
+ return;
509
+ }
510
+ if (id.type === 'ArrayPattern') {
511
+ for (const el of id.elements || [])
512
+ if (el)
513
+ pushPattern(el.type === 'RestElement' ? el.argument : el);
514
+ return;
515
+ }
516
+ if (id.type === 'AssignmentPattern')
517
+ pushPattern(id.left);
518
+ if (id.type === 'RestElement')
519
+ pushPattern(id.argument);
520
+ };
521
+ switch (node.type) {
522
+ case 'VariableDeclarator':
523
+ pushPattern(node.id);
524
+ break;
525
+ case 'FunctionDeclaration':
526
+ case 'FunctionExpression':
527
+ case 'ArrowFunctionExpression':
528
+ if (node.id)
529
+ pushPattern(node.id);
530
+ for (const param of node.params || [])
531
+ pushPattern(param);
532
+ break;
533
+ case 'ClassDeclaration':
534
+ case 'ClassExpression':
535
+ if (node.id)
536
+ pushPattern(node.id);
537
+ break;
538
+ case 'CatchClause':
539
+ pushPattern(node.param);
540
+ break;
541
+ default:
542
+ break;
543
+ }
544
+ return out;
545
+ }
546
+ /**
547
+ * Rejects reading a member off a `.vsk` namespace in *value* position. A
548
+ * namespace is resolved only for component tags, so `ns.max` in an expression
549
+ * has nothing bound to it at runtime — say so here instead of failing later
550
+ * with a bare `ReferenceError: ns is not defined`.
551
+ */
552
+ function assertNoVskNamespaceValue(source, expr) {
553
+ if (!currentVskNamespaceLocals || currentVskNamespaceLocals.size === 0)
554
+ return;
555
+ const hit = findNamespaceValueUse(expr, currentVskNamespaceLocals);
556
+ if (!hit)
557
+ return;
558
+ const { line, column } = offsetToLineCol(source, hit.start ?? 0);
559
+ throw VeskError.vskNamespaceMember({ file: currentSourceFile, line, column, code: codeFrame(source, line, column), form: 'value' });
560
+ }
561
+ function findNamespaceValueUse(node, locals) {
562
+ if (!node || typeof node !== 'object')
563
+ return null;
564
+ if (Array.isArray(node)) {
565
+ for (const child of node) {
566
+ const hit = findNamespaceValueUse(child, locals);
567
+ if (hit)
568
+ return hit;
569
+ }
570
+ return null;
571
+ }
572
+ if (typeof node.type !== 'string')
573
+ return null;
574
+ if (node.type === 'MemberExpression' && node.object?.type === 'Identifier' && locals.has(node.object.name))
575
+ return node;
576
+ for (const key of Object.keys(node)) {
577
+ if (key === 'loc' || key === 'range' || key === 'start' || key === 'end')
578
+ continue;
579
+ const hit = findNamespaceValueUse(node[key], locals);
580
+ if (hit)
581
+ return hit;
582
+ }
583
+ return null;
584
+ }
403
585
  function isHTMLTag(name) {
404
586
  return name.length > 0 && name[0] === name[0].toLowerCase();
405
587
  }
@@ -411,6 +593,7 @@ function isMapCall(expr) {
411
593
  expr.arguments[0].type === 'ArrowFunctionExpression');
412
594
  }
413
595
  function toExpression(source, expr) {
596
+ assertNoVskNamespaceValue(source, expr);
414
597
  return new Expression(getSource(source, expr), [], expr, source);
415
598
  }
416
599
  function processAttribute(source, attr) {
@@ -887,8 +1070,25 @@ function processJSXElement(source, element) {
887
1070
  // actual in-scope value instead of `document.createElement("it.icon")` or a
888
1071
  // registry lookup by dotted string.
889
1072
  if (nameNode && nameNode.type === 'JSXMemberExpression') {
1073
+ const root = jsxTagRootName(nameNode);
1074
+ const isVskNamespace = !!currentVskNamespaceLocals && currentVskNamespaceLocals.has(root);
1075
+ if (isVskNamespace && jsxTagDepth(nameNode) > 1) {
1076
+ // A `.vsk` module's exports are flat component names, so there is no
1077
+ // nested namespace object to walk into.
1078
+ const { line, column } = offsetToLineCol(source, nameNode.start ?? 0);
1079
+ throw VeskError.vskNamespaceMember({ file: currentSourceFile, line, column, code: codeFrame(source, line, column), form: 'nested' });
1080
+ }
890
1081
  const { props, spreadProps, slots } = extractProps(source, element);
891
1082
  const children = selfClosing ? [] : processJSXChildren(source, element.children || []);
1083
+ if (isVskNamespace) {
1084
+ // `<ns.Icon />` after `import * as ns from './lib.vsk'` resolves to the
1085
+ // same registry entry `import { Icon }` would, keyed by the EXPORTED name.
1086
+ // Deliberately carries no `calleeExpr`: both codegen paths must take the
1087
+ // registry branch so `<ns.Icon />` keeps the "component was not found"
1088
+ // guard and hydrates like any other compiled component. The namespace
1089
+ // object itself is never built.
1090
+ return [new ComponentCall(jsxTagMemberName(nameNode), props, [...children, ...slots.map((s) => new PropSlot(s.name, s.nodes))], spreadProps, element.start)];
1091
+ }
892
1092
  return [new ComponentCall(tagName, props, [...children, ...slots.map((s) => new PropSlot(s.name, s.nodes))], spreadProps, element.start, getSource(source, nameNode))];
893
1093
  }
894
1094
  if (!isHTMLTag(tagName) && selfClosing) {
@@ -1411,222 +1611,289 @@ export function generateIR(ast, source, filename) {
1411
1611
  let staticProps = null;
1412
1612
  let loadFn = null;
1413
1613
  const topLevelCode = [];
1414
- for (const node of ast.body) {
1415
- if (node.type === 'ImportDeclaration') {
1416
- const raw = getSource(source, node);
1417
- let cleaned = stripTypeImport(raw);
1418
- if (cleaned === null)
1419
- continue;
1420
- cleaned = normalizeRuntimeSpecifier(cleaned);
1421
- imports.push(cleaned);
1422
- for (const spec of node.specifiers) {
1423
- if (spec.importKind === 'type')
1614
+ const exportAliases = [];
1615
+ const reexportSources = [];
1616
+ // `export { A }` / `export { A as B }` are stripped by the parser (acorn
1617
+ // rejects a module export that names no top-level binding, and a `.vsk`
1618
+ // component is a registry entry, not a binding). The pairs ride along on the
1619
+ // AST and become registry aliases below.
1620
+ for (const pair of (ast
1621
+ .__vskSpecifierExports) || []) {
1622
+ exportAliases.push({ local: pair.local, exported: pair.exported });
1623
+ }
1624
+ // `import * as ns from './x.vsk'` has no runtime binding: a `.vsk` component
1625
+ // is a registry entry, not a module namespace object. Pre-scan the import
1626
+ // declarations so a `<ns.Tag />` can be reported clearly instead of failing
1627
+ // later as an opaque "ns is not defined".
1628
+ const vskNamespaceLocals = collectVskNamespaceLocals(ast);
1629
+ const prevNamespaceLocals = currentVskNamespaceLocals;
1630
+ const prevSourceFile = currentSourceFile;
1631
+ currentVskNamespaceLocals = vskNamespaceLocals;
1632
+ currentSourceFile = file;
1633
+ try {
1634
+ for (const node of ast.body) {
1635
+ if (node.type === 'ImportDeclaration') {
1636
+ const raw = getSource(source, node);
1637
+ let cleaned = stripTypeImport(raw);
1638
+ if (cleaned === null)
1424
1639
  continue;
1425
- if (spec.type === 'ImportSpecifier') {
1426
- importedNames.add(spec.local.name);
1640
+ cleaned = normalizeRuntimeSpecifier(cleaned);
1641
+ imports.push(cleaned);
1642
+ for (const spec of node.specifiers) {
1643
+ if (spec.importKind === 'type')
1644
+ continue;
1645
+ if (spec.type === 'ImportSpecifier') {
1646
+ importedNames.add(spec.local.name);
1647
+ }
1427
1648
  }
1428
- }
1429
- continue;
1430
- }
1431
- if (node.type === 'ExportNamedDeclaration' && node.declaration && !staticProps) {
1432
- const decl = node.declaration;
1433
- const fnName = decl.type === 'FunctionDeclaration'
1434
- ? decl.id?.name
1435
- : decl.type === 'VariableDeclaration'
1436
- ? decl.declarations[0]?.id?.name
1437
- : null;
1438
- if (fnName === 'getStaticProps') {
1439
- staticProps = stripCodeTypes(getSource(source, decl));
1440
1649
  continue;
1441
1650
  }
1442
- }
1443
- if (node.type === 'ExportNamedDeclaration' && node.declaration && !loadFn) {
1444
- const decl = node.declaration;
1445
- const fnName = decl.type === 'FunctionDeclaration'
1446
- ? decl.id?.name
1447
- : decl.type === 'VariableDeclaration'
1448
- ? decl.declarations[0]?.id?.name
1449
- : null;
1450
- if (fnName === 'load') {
1451
- loadFn = stripCodeTypes(getSource(source, decl));
1452
- continue;
1651
+ if (node.type === 'ExportNamedDeclaration' && node.declaration && !staticProps) {
1652
+ const decl = node.declaration;
1653
+ const fnName = decl.type === 'FunctionDeclaration'
1654
+ ? decl.id?.name
1655
+ : decl.type === 'VariableDeclaration'
1656
+ ? decl.declarations[0]?.id?.name
1657
+ : null;
1658
+ if (fnName === 'getStaticProps') {
1659
+ staticProps = stripCodeTypes(getSource(source, decl));
1660
+ continue;
1661
+ }
1453
1662
  }
1454
- }
1455
- // `export const standalone = true` / `export let standalone = true` is a
1456
- // compile-time routing directive (isStandaloneLayoutFile), not runtime
1457
- // code — drop it so it never reaches emitted JS (client chunks are
1458
- // IIFE-wrapped where a bare `export` is a syntax error).
1459
- if (node.type === 'ExportNamedDeclaration' && node.declaration) {
1460
- const decl = node.declaration;
1461
- if (decl.type === 'VariableDeclaration') {
1462
- const declName = decl.declarations[0]?.id?.name;
1463
- if (declName === 'standalone') {
1663
+ if (node.type === 'ExportNamedDeclaration' && node.declaration && !loadFn) {
1664
+ const decl = node.declaration;
1665
+ const fnName = decl.type === 'FunctionDeclaration'
1666
+ ? decl.id?.name
1667
+ : decl.type === 'VariableDeclaration'
1668
+ ? decl.declarations[0]?.id?.name
1669
+ : null;
1670
+ if (fnName === 'load') {
1671
+ loadFn = stripCodeTypes(getSource(source, decl));
1464
1672
  continue;
1465
1673
  }
1466
1674
  }
1467
- }
1468
- let inner = node;
1469
- let exported = false;
1470
- let defaultExport = false;
1471
- if (node.type === 'ExportNamedDeclaration' && node.declaration) {
1472
- inner = node.declaration;
1473
- exported = true;
1474
- }
1475
- else if (node.type === 'ExportDefaultDeclaration' && node.declaration) {
1476
- inner = node.declaration;
1477
- exported = true;
1478
- defaultExport = true;
1479
- }
1480
- if (inner.type === 'ComponentDeclaration') {
1481
- // handled below
1482
- }
1483
- else if (inner.type === 'ClassDeclaration') {
1484
- const { line, column } = offsetToLineCol(source, inner.start ?? 0);
1485
- throw VeskError.classDecl({ file, line, column, code: codeFrame(source, line, column) });
1486
- }
1487
- else if (inner.type === 'TSEnumDeclaration') {
1488
- const code = processEnum(inner, source, exported);
1489
- topLevelCode.push(code);
1490
- continue;
1491
- }
1492
- else {
1493
- topLevelCode.push(getSource(source, node));
1494
- continue;
1495
- }
1496
- const name = inner.id.name;
1497
- const paramNames = getParamNames(inner.params, source);
1498
- // A plain-identifier first parameter receives the whole props object under
1499
- // its own name (React-style); a destructuring pattern receives the fields.
1500
- const firstParam = inner.params?.[0];
1501
- const propsAlias = firstParam && firstParam.type === 'Identifier' ? firstParam.name : null;
1502
- const propsType = getPropsType(inner.params, source);
1503
- const bodyStmts = inner.body.body;
1504
- const isClientComp = !!inner.client;
1505
- // Slot props are declared at the component boundary (`trigger: Component`),
1506
- // so content reads of `props.<name>` compile to slot renders instead of
1507
- // value bindings for THIS component only.
1508
- const prevSlotProps = __slotProps;
1509
- __slotProps = slotPropNamesFromType(propsType);
1510
- __propsParam = propsAlias || null;
1511
- if (isStatementMode(bodyStmts)) {
1512
- const raw = processStatementModeBody(source, bodyStmts, file);
1513
- const { body, css } = extractStyle(raw);
1514
- validateBlocks(name, isClientComp, body, file, source);
1515
- const comp = new ComponentIR(name, paramNames, body, { mode: 'statement', exported, defaultExport, isClient: inner.client, isAsync: inner.async, ssrAwait: componentUsesFetch(body), propsType, propsAlias });
1516
- comp.style = css;
1517
- components.push(comp);
1518
- __slotProps = prevSlotProps;
1519
- }
1520
- else {
1521
- const guardClauses = [];
1522
- let mainReturn = null;
1523
- const preamble = [];
1524
- for (const stmt of bodyStmts) {
1525
- if (stmt.type === 'VeskBlock') {
1526
- const innerBody = processStatementModeBody(source, stmt.body, file);
1527
- if (stmt.tag === 'server') {
1528
- preamble.push(new ServerBlock(innerBody));
1529
- }
1530
- else if (stmt.tag === 'client') {
1531
- preamble.push(new ClientBlock(innerBody));
1675
+ // `export const standalone = true` / `export let standalone = true` is a
1676
+ // compile-time routing directive (isStandaloneLayoutFile), not runtime
1677
+ // code — drop it so it never reaches emitted JS (client chunks are
1678
+ // IIFE-wrapped where a bare `export` is a syntax error).
1679
+ if (node.type === 'ExportNamedDeclaration' && node.declaration) {
1680
+ const decl = node.declaration;
1681
+ if (decl.type === 'VariableDeclaration') {
1682
+ const declName = decl.declarations[0]?.id?.name;
1683
+ if (declName === 'standalone') {
1684
+ continue;
1532
1685
  }
1533
1686
  }
1534
- else if (stmt.type === 'ReturnStatement') {
1535
- mainReturn = stmt;
1536
- }
1537
- else if (isTrackDeclaration(stmt)) {
1538
- const elements = stmt.declarations[0].id.elements;
1539
- const trackName = elements[0]?.name;
1540
- const rawName = elements.length > 1 ? elements[1]?.name : null;
1541
- const init = getSource(source, stmt.declarations[0].init);
1542
- if (trackName)
1543
- preamble.push(new TrackDecl(trackName, init, rawName));
1544
- const refName = getComponentRefName(stmt);
1545
- if (refName)
1546
- preamble.push(new ComponentRef(refName));
1547
- }
1548
- else if (stmt.type === 'IfStatement' && !mainReturn && stmt.consequent.type !== 'ThrowStatement') {
1549
- guardClauses.push(stmt);
1550
- }
1551
- else if (stmt.type === 'ClassDeclaration') {
1552
- const { line, column } = offsetToLineCol(source, stmt.start ?? 0);
1553
- throw VeskError.classDecl({ file, line, column, code: codeFrame(source, line, column) });
1687
+ }
1688
+ if (node.type === 'ExportNamedDeclaration' && !node.declaration) {
1689
+ // A specifier-only export (`export { A }`, `export { A as B }`) or a
1690
+ // re-export (`export { A } from './x.vsk'`, `export { default as A } from …`).
1691
+ // Never emit the raw statement: components are registry entries, not
1692
+ // top-level bindings, so emitting it yields `Export 'A' is not defined`.
1693
+ if (node.source) {
1694
+ if (typeof node.source.value === 'string')
1695
+ reexportSources.push(node.source.value);
1696
+ // `export { A as B } from './x.vsk'`: the target's components merge into
1697
+ // this file's registry under their own names, so the specifier is the
1698
+ // same canonical -> alias mapping a same-file export would produce.
1699
+ for (const spec of node.specifiers || []) {
1700
+ if (spec.type === 'ExportSpecifier') {
1701
+ const localName = spec.local && spec.local.name;
1702
+ const exportedName = spec.exported && spec.exported.name;
1703
+ if (typeof localName === 'string' && typeof exportedName === 'string') {
1704
+ exportAliases.push({ local: localName, exported: exportedName });
1705
+ }
1706
+ }
1707
+ }
1708
+ continue;
1554
1709
  }
1555
- else {
1556
- const raw = getSource(source, stmt);
1557
- if (raw)
1558
- preamble.push(new RuntimeStatement(raw, stmt, source));
1710
+ for (const spec of node.specifiers || []) {
1711
+ if (spec.type === 'ExportSpecifier') {
1712
+ const localName = spec.local && spec.local.name;
1713
+ const exportedName = spec.exported && spec.exported.name;
1714
+ if (typeof localName === 'string' && typeof exportedName === 'string') {
1715
+ exportAliases.push({ local: localName, exported: exportedName });
1716
+ }
1717
+ }
1559
1718
  }
1719
+ continue;
1560
1720
  }
1561
- const guardBody = buildGuardChain(source, guardClauses, mainReturn);
1562
- const { body, css } = extractStyle([...preamble, ...guardBody]);
1563
- validateBlocks(name, isClientComp, body, file, source);
1564
- const comp = new ComponentIR(name, paramNames, body, { exported, defaultExport, isClient: inner.client, isAsync: inner.async, ssrAwait: componentUsesFetch(body), propsType, propsAlias });
1565
- comp.style = css;
1566
- components.push(comp);
1567
- __slotProps = prevSlotProps;
1568
- }
1569
- }
1570
- const asyncNames = new Set(components.filter((c) => c.isAsync || c.ssrAwait).map((c) => c.name));
1571
- for (const comp of components) {
1572
- if (asyncNames.has(comp.name))
1573
- continue;
1574
- const called = new Map();
1575
- collectComponentCalls(comp.body, called);
1576
- for (const [childName, start] of called) {
1577
- if (!asyncNames.has(childName))
1721
+ if (node.type === 'ExportAllDeclaration') {
1722
+ // `export * from './x.vsk'` / `export * as ns from './x.vsk'`: resolve the
1723
+ // target into this file's component registry instead of emitting a bare
1724
+ // re-export that no bundler can satisfy for `.vsk` modules.
1725
+ if (node.source && typeof node.source.value === 'string')
1726
+ reexportSources.push(node.source.value);
1727
+ continue;
1728
+ }
1729
+ let inner = node;
1730
+ let exported = false;
1731
+ let defaultExport = false;
1732
+ if (node.type === 'ExportNamedDeclaration' && node.declaration) {
1733
+ inner = node.declaration;
1734
+ exported = true;
1735
+ }
1736
+ else if (node.type === 'ExportDefaultDeclaration' && node.declaration) {
1737
+ inner = node.declaration;
1738
+ exported = true;
1739
+ defaultExport = true;
1740
+ }
1741
+ if (inner.type === 'ComponentDeclaration') {
1742
+ // handled below
1743
+ }
1744
+ else if (inner.type === 'ClassDeclaration') {
1745
+ const { line, column } = offsetToLineCol(source, inner.start ?? 0);
1746
+ throw VeskError.classDecl({ file, line, column, code: codeFrame(source, line, column) });
1747
+ }
1748
+ else if (inner.type === 'TSEnumDeclaration') {
1749
+ const code = processEnum(inner, source, exported);
1750
+ topLevelCode.push(code);
1578
1751
  continue;
1579
- const { line, column } = offsetToLineCol(source, start >= 0 ? start : 0);
1580
- throw VeskError.asyncChildInSyncParent(comp.name, childName, { file, line, column, code: codeFrame(source, line, column) });
1581
- }
1582
- }
1583
- const autoImportable = [
1584
- 'useFetch', 'useRouter', 'useParams', 'usePathname', 'useSearchParams', 'useNavigate',
1585
- 'defineAction',
1586
- 'Form', 'Field', 'required', 'email', 'minLength', 'maxLength', 'pattern', 'custom',
1587
- 'Link', 'NavLink', 'Outlet', 'Redirect',
1588
- 'Image', 'Portal',
1589
- 'Experiment',
1590
- 'LoadingIndicator', 'useLoadingIndicator',
1591
- 'JsonLd', 'ArticleSchema', 'ProductSchema', 'FAQPageSchema', 'BreadcrumbListSchema',
1592
- 'OrganizationSchema', 'LocalBusinessSchema', 'VideoSchema',
1593
- 'effect', 'derived', 'untrack', 'peek', 'tick', 'flushSync', 'on_destroy',
1594
- 'createContext',
1595
- 'redirect', 'permanentRedirect', 'notFound', 'NotFoundError',
1596
- 'createResource', 'getAction', 'validateActionInput', 'issuesToFieldMap', 'isFormAction',
1597
- 'Show', 'For', 'Switch', 'Match',
1598
- ];
1599
- // One walk of the already-parsed tree finds every auto-importable call
1600
- // target and JSX element name. Every IR raw the old per-fragment scan
1601
- // re-tokenized ("addUsedFrom" over ~15 text slices) is a subtree of this
1602
- // very same `ast` — nothing is lost, but the acorn tokenizer is invoked once
1603
- // per file instead of once per fragment.
1604
- const called = collectCallAndJsxTargets(ast);
1605
- const usedFunctions = new Set();
1606
- for (const fn of autoImportable) {
1607
- if (called.has(fn))
1608
- usedFunctions.add(fn);
1609
- }
1610
- if (usedFunctions.size > 0) {
1611
- const existing = new Set();
1612
- for (const imp of imports) {
1613
- if (importModuleTarget(imp) !== '@vesk/runtime')
1752
+ }
1753
+ else {
1754
+ topLevelCode.push(getSource(source, node));
1614
1755
  continue;
1615
- for (const n of extractImportNames(imp))
1616
- existing.add(n);
1756
+ }
1757
+ const name = inner.id.name;
1758
+ const paramNames = getParamNames(inner.params, source);
1759
+ // A plain-identifier first parameter receives the whole props object under
1760
+ // its own name (React-style); a destructuring pattern receives the fields.
1761
+ const firstParam = inner.params?.[0];
1762
+ const propsAlias = firstParam && firstParam.type === 'Identifier' ? firstParam.name : null;
1763
+ const propsType = getPropsType(inner.params, source);
1764
+ const bodyStmts = inner.body.body;
1765
+ const isClientComp = !!inner.client;
1766
+ // Slot props are declared at the component boundary (`trigger: Component`),
1767
+ // so content reads of `props.<name>` compile to slot renders instead of
1768
+ // value bindings for THIS component only.
1769
+ const prevSlotProps = __slotProps;
1770
+ __slotProps = slotPropNamesFromType(propsType);
1771
+ __propsParam = propsAlias || null;
1772
+ if (isStatementMode(bodyStmts)) {
1773
+ const raw = processStatementModeBody(source, bodyStmts, file);
1774
+ const { body, css } = extractStyle(raw);
1775
+ validateBlocks(name, isClientComp, body, file, source);
1776
+ const comp = new ComponentIR(name, paramNames, body, { mode: 'statement', exported, defaultExport, isClient: inner.client, isAsync: inner.async, ssrAwait: componentUsesFetch(body), propsType, propsAlias });
1777
+ comp.style = css;
1778
+ components.push(comp);
1779
+ __slotProps = prevSlotProps;
1780
+ }
1781
+ else {
1782
+ const guardClauses = [];
1783
+ let mainReturn = null;
1784
+ const preamble = [];
1785
+ for (const stmt of bodyStmts) {
1786
+ if (stmt.type === 'VeskBlock') {
1787
+ const innerBody = processStatementModeBody(source, stmt.body, file);
1788
+ if (stmt.tag === 'server') {
1789
+ preamble.push(new ServerBlock(innerBody));
1790
+ }
1791
+ else if (stmt.tag === 'client') {
1792
+ preamble.push(new ClientBlock(innerBody));
1793
+ }
1794
+ }
1795
+ else if (stmt.type === 'ReturnStatement') {
1796
+ mainReturn = stmt;
1797
+ }
1798
+ else if (isTrackDeclaration(stmt)) {
1799
+ const elements = stmt.declarations[0].id.elements;
1800
+ const trackName = elements[0]?.name;
1801
+ const rawName = elements.length > 1 ? elements[1]?.name : null;
1802
+ const init = getSource(source, stmt.declarations[0].init);
1803
+ if (trackName)
1804
+ preamble.push(new TrackDecl(trackName, init, rawName));
1805
+ const refName = getComponentRefName(stmt);
1806
+ if (refName)
1807
+ preamble.push(new ComponentRef(refName));
1808
+ }
1809
+ else if (stmt.type === 'IfStatement' && !mainReturn && stmt.consequent.type !== 'ThrowStatement') {
1810
+ guardClauses.push(stmt);
1811
+ }
1812
+ else if (stmt.type === 'ClassDeclaration') {
1813
+ const { line, column } = offsetToLineCol(source, stmt.start ?? 0);
1814
+ throw VeskError.classDecl({ file, line, column, code: codeFrame(source, line, column) });
1815
+ }
1816
+ else {
1817
+ const raw = getSource(source, stmt);
1818
+ if (raw)
1819
+ preamble.push(new RuntimeStatement(raw, stmt, source));
1820
+ }
1821
+ }
1822
+ const guardBody = buildGuardChain(source, guardClauses, mainReturn);
1823
+ const { body, css } = extractStyle([...preamble, ...guardBody]);
1824
+ validateBlocks(name, isClientComp, body, file, source);
1825
+ const comp = new ComponentIR(name, paramNames, body, { exported, defaultExport, isClient: inner.client, isAsync: inner.async, ssrAwait: componentUsesFetch(body), propsType, propsAlias });
1826
+ comp.style = css;
1827
+ components.push(comp);
1828
+ __slotProps = prevSlotProps;
1829
+ }
1617
1830
  }
1618
- // A name already bound by the file's own imports (any target — including a
1619
- // local module with the same export) wins; never auto-import a duplicate
1620
- // binding. Mirrors client shadowing so SSR and client agree.
1621
- const boundLocally = new Set();
1622
- for (const imp of imports) {
1623
- for (const pair of importBindingPairs(imp))
1624
- boundLocally.add(pair.local);
1831
+ const asyncNames = new Set(components.filter((c) => c.isAsync || c.ssrAwait).map((c) => c.name));
1832
+ for (const comp of components) {
1833
+ if (asyncNames.has(comp.name))
1834
+ continue;
1835
+ const called = new Map();
1836
+ collectComponentCalls(comp.body, called);
1837
+ for (const [childName, start] of called) {
1838
+ if (!asyncNames.has(childName))
1839
+ continue;
1840
+ const { line, column } = offsetToLineCol(source, start >= 0 ? start : 0);
1841
+ throw VeskError.asyncChildInSyncParent(comp.name, childName, { file, line, column, code: codeFrame(source, line, column) });
1842
+ }
1625
1843
  }
1626
- const missing = [...usedFunctions].filter(f => !existing.has(f) && !boundLocally.has(f));
1627
- if (missing.length > 0) {
1628
- imports.push(`import { ${missing.join(', ')} } from '@vesk/runtime';`);
1844
+ const autoImportable = [
1845
+ 'useFetch', 'useRouter', 'useParams', 'usePathname', 'useSearchParams', 'useNavigate',
1846
+ 'defineAction',
1847
+ 'Form', 'Field', 'required', 'email', 'minLength', 'maxLength', 'pattern', 'custom',
1848
+ 'Link', 'NavLink', 'Outlet', 'Redirect',
1849
+ 'Image', 'Portal',
1850
+ 'Experiment',
1851
+ 'LoadingIndicator', 'useLoadingIndicator',
1852
+ 'JsonLd', 'ArticleSchema', 'ProductSchema', 'FAQPageSchema', 'BreadcrumbListSchema',
1853
+ 'OrganizationSchema', 'LocalBusinessSchema', 'VideoSchema',
1854
+ 'effect', 'derived', 'untrack', 'peek', 'tick', 'flushSync', 'on_destroy',
1855
+ 'createContext',
1856
+ 'redirect', 'permanentRedirect', 'notFound', 'NotFoundError',
1857
+ 'createResource', 'getAction', 'validateActionInput', 'issuesToFieldMap', 'isFormAction',
1858
+ 'Show', 'For', 'Switch', 'Match',
1859
+ 'primitive', 'leaf',
1860
+ ];
1861
+ // One walk of the already-parsed tree finds every auto-importable call
1862
+ // target and JSX element name. Every IR raw the old per-fragment scan
1863
+ // re-tokenized ("addUsedFrom" over ~15 text slices) is a subtree of this
1864
+ // very same `ast` — nothing is lost, but the acorn tokenizer is invoked once
1865
+ // per file instead of once per fragment.
1866
+ const called = collectCallAndJsxTargets(ast);
1867
+ const usedFunctions = new Set();
1868
+ for (const fn of autoImportable) {
1869
+ if (called.has(fn))
1870
+ usedFunctions.add(fn);
1871
+ }
1872
+ if (usedFunctions.size > 0) {
1873
+ const existing = new Set();
1874
+ for (const imp of imports) {
1875
+ if (importModuleTarget(imp) !== '@vesk/runtime')
1876
+ continue;
1877
+ for (const n of extractImportNames(imp))
1878
+ existing.add(n);
1879
+ }
1880
+ // A name already bound by the file's own imports (any target — including a
1881
+ // local module with the same export) wins; never auto-import a duplicate
1882
+ // binding. Mirrors client shadowing so SSR and client agree.
1883
+ const boundLocally = new Set();
1884
+ for (const imp of imports) {
1885
+ for (const pair of importBindingPairs(imp))
1886
+ boundLocally.add(pair.local);
1887
+ }
1888
+ const missing = [...usedFunctions].filter(f => !existing.has(f) && !boundLocally.has(f));
1889
+ if (missing.length > 0) {
1890
+ imports.push(`import { ${missing.join(', ')} } from '@vesk/runtime';`);
1891
+ }
1629
1892
  }
1893
+ return new IRRoot(components, imports, importedNames, staticProps, loadFn, topLevelCode, exportAliases, reexportSources);
1894
+ }
1895
+ finally {
1896
+ currentVskNamespaceLocals = prevNamespaceLocals;
1897
+ currentSourceFile = prevSourceFile;
1630
1898
  }
1631
- return new IRRoot(components, imports, importedNames, staticProps, loadFn, topLevelCode);
1632
1899
  }