@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.
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +17 -1
- package/dist/client-codegen.d.ts +25 -0
- package/dist/client-codegen.d.ts.map +1 -1
- package/dist/client-codegen.js +225 -15
- package/dist/errors.d.ts +14 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +36 -0
- package/dist/ir-generator.d.ts.map +1 -1
- package/dist/ir-generator.js +466 -199
- package/dist/ir.d.ts +22 -1
- package/dist/ir.d.ts.map +1 -1
- package/dist/ir.js +18 -1
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +25 -1
- package/dist/precompile-runtime.d.ts.map +1 -1
- package/dist/precompile-runtime.js +4 -0
- package/dist/precompile.js +6 -2
- package/dist/server-jsgen.d.ts.map +1 -1
- package/dist/server-jsgen.js +8 -1
- package/dist/server-render.d.ts.map +1 -1
- package/dist/server-render.js +138 -56
- package/dist/server-utils.d.ts.map +1 -1
- package/dist/server-utils.js +33 -5
- package/dist/ssr-store.d.ts +60 -0
- package/dist/ssr-store.d.ts.map +1 -1
- package/dist/ssr-store.js +157 -6
- package/dist/tokens.d.ts +28 -0
- package/dist/tokens.d.ts.map +1 -1
- package/dist/tokens.js +200 -19
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/vesk-plugin.d.ts +2 -0
- package/dist/vesk-plugin.d.ts.map +1 -1
- package/dist/vesk-plugin.js +28 -1
- package/dist/vsk-collision.d.ts +44 -0
- package/dist/vsk-collision.d.ts.map +1 -0
- package/dist/vsk-collision.js +73 -0
- package/dist/vsk-imports.d.ts +51 -0
- package/dist/vsk-imports.d.ts.map +1 -1
- package/dist/vsk-imports.js +106 -9
- package/package.json +2 -2
package/dist/ir-generator.js
CHANGED
|
@@ -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
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
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
|
-
|
|
1426
|
-
|
|
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
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
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
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
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
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
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
|
-
|
|
1535
|
-
|
|
1536
|
-
}
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
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
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
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
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
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
|
-
|
|
1580
|
-
|
|
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
|
-
|
|
1616
|
-
|
|
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
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
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
|
|
1627
|
-
|
|
1628
|
-
|
|
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
|
}
|