@vesk/compiler 0.2.50 → 0.2.51
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/errors.d.ts +20 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +33 -0
- package/dist/ir-generator.d.ts.map +1 -1
- package/dist/ir-generator.js +456 -69
- package/dist/module-imports.d.ts +6 -2
- package/dist/module-imports.d.ts.map +1 -1
- package/dist/module-imports.js +96 -7
- package/dist/parser.d.ts +29 -0
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +221 -1
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +15 -0
- package/dist/server-jsgen.d.ts.map +1 -1
- package/dist/server-jsgen.js +32 -6
- package/dist/server-utils.d.ts +1 -0
- package/dist/server-utils.d.ts.map +1 -1
- package/dist/server-utils.js +1 -1
- package/dist/strip-ts.d.ts.map +1 -1
- package/dist/strip-ts.js +48 -6
- package/package.json +2 -2
package/dist/ir-generator.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
//
|
|
1778
|
-
//
|
|
1779
|
-
|
|
1780
|
-
const
|
|
1781
|
-
const
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
const
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
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.
|
|
1810
|
-
|
|
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
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
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
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
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
|
}
|
package/dist/module-imports.d.ts
CHANGED
|
@@ -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
|
|
41
|
+
export interface SsrSource {
|
|
41
42
|
raw: string;
|
|
42
43
|
ast: ReturnType<typeof parse> | null;
|
|
43
|
-
|
|
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":"AAMA,OAAO,EAAE,KAAK,EAAE,MAAM,2BAA2B,CAAC;
|
|
1
|
+
{"version":3,"file":"module-imports.d.ts","sourceRoot":"","sources":["../src/module-imports.ts"],"names":[],"mappings":"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"}
|