@llui/vite-plugin 0.0.40 → 0.0.42
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/accessor-resolver.d.ts +58 -0
- package/dist/accessor-resolver.d.ts.map +1 -0
- package/dist/accessor-resolver.js +119 -0
- package/dist/accessor-resolver.js.map +1 -0
- package/dist/collect-deps.d.ts +15 -8
- package/dist/collect-deps.d.ts.map +1 -1
- package/dist/collect-deps.js +134 -32
- package/dist/collect-deps.js.map +1 -1
- package/dist/transform.d.ts.map +1 -1
- package/dist/transform.js +187 -110
- package/dist/transform.js.map +1 -1
- package/package.json +1 -1
package/dist/transform.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import ts from 'typescript';
|
|
2
2
|
import { collectDeps } from './collect-deps.js';
|
|
3
|
+
import { resolveLocalConstInitializer, resolveAccessorBody, isMemoCallWithArrowArg, } from './accessor-resolver.js';
|
|
3
4
|
import { extractMsgSchema, extractEffectSchema, isRichField, } from './msg-schema.js';
|
|
4
5
|
import { extractMsgAnnotations } from './msg-annotations.js';
|
|
5
6
|
import { extractStateSchema } from './state-schema.js';
|
|
@@ -94,66 +95,68 @@ const PROP_KEYS = new Set([
|
|
|
94
95
|
'innerHTML',
|
|
95
96
|
'textContent',
|
|
96
97
|
]);
|
|
98
|
+
function isStaticPrimitiveLiteral(expr) {
|
|
99
|
+
return (ts.isStringLiteral(expr) ||
|
|
100
|
+
ts.isNumericLiteral(expr) ||
|
|
101
|
+
ts.isNoSubstitutionTemplateLiteral(expr) ||
|
|
102
|
+
expr.kind === ts.SyntaxKind.TrueKeyword ||
|
|
103
|
+
expr.kind === ts.SyntaxKind.FalseKeyword ||
|
|
104
|
+
expr.kind === ts.SyntaxKind.NullKeyword);
|
|
105
|
+
}
|
|
97
106
|
/**
|
|
98
|
-
*
|
|
99
|
-
* `
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
* const cls = (s: State) => isActive(item, s.path) ? 'on' : 'off'
|
|
105
|
-
* return a({ class: cls }, ...)
|
|
106
|
-
*
|
|
107
|
-
* Without resolution, `class: cls` would fall through the static path
|
|
108
|
-
* and emit `__e.className = cls`, coercing the function to its source
|
|
109
|
-
* string. With resolution, we see that `cls` is an arrow and emit a
|
|
110
|
-
* binding exactly as if the arrow had been inlined.
|
|
111
|
-
*
|
|
112
|
-
* Lifetime rules:
|
|
113
|
-
* - Only single-binding `const`/`let`/`var` declarations with an
|
|
114
|
-
* initializer are considered. No destructuring, no multi-declarator
|
|
115
|
-
* statements (too easy to get wrong without a type checker).
|
|
116
|
-
* - Later reassignments are NOT tracked — if the identifier is `let`
|
|
117
|
-
* and gets reassigned, the resolution is unreliable. We conservatively
|
|
118
|
-
* refuse to resolve `let` bindings for now (arrow-valued accessors
|
|
119
|
-
* are ~always `const` in practice).
|
|
120
|
-
* - The declaration must dominate the use (same block or an enclosing
|
|
121
|
-
* one). TypeScript's block semantics mean walking up parent blocks
|
|
122
|
-
* is sufficient.
|
|
107
|
+
* Classify a reactive-prop value. See `ResolvedReactiveValue` for the
|
|
108
|
+
* contract. Returns `null` only when the value is none of the recognized
|
|
109
|
+
* shapes (caller can fall back to its own branches — currently only
|
|
110
|
+
* `tryTransformElementCall` does this for `isPerItemFieldAccess` /
|
|
111
|
+
* `isHoistedPerItem`).
|
|
123
112
|
*/
|
|
124
|
-
function
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
113
|
+
function classifyReactiveValue(value) {
|
|
114
|
+
// Inline arrow / function expression at the call site
|
|
115
|
+
if (ts.isArrowFunction(value) || ts.isFunctionExpression(value)) {
|
|
116
|
+
return { kind: 'arrow', accessor: value, valueForBinding: value };
|
|
117
|
+
}
|
|
118
|
+
// Inline `memo(arrow)` at the call site
|
|
119
|
+
if (isMemoCallWithArrowArg(value)) {
|
|
120
|
+
return {
|
|
121
|
+
kind: 'memo-call',
|
|
122
|
+
accessor: value.arguments[0],
|
|
123
|
+
valueForBinding: value,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
// Identifier — resolve and classify the resolved declaration
|
|
127
|
+
if (ts.isIdentifier(value)) {
|
|
128
|
+
const resolved = resolveLocalConstInitializer(value);
|
|
129
|
+
if (!resolved) {
|
|
130
|
+
// Imported / parameter / unbound — can't prove it's a primitive,
|
|
131
|
+
// can't prove it's a function. Caller must bail to runtime.
|
|
132
|
+
return { kind: 'bail' };
|
|
133
133
|
}
|
|
134
|
-
|
|
135
|
-
|
|
134
|
+
if (ts.isArrowFunction(resolved) || ts.isFunctionExpression(resolved)) {
|
|
135
|
+
return { kind: 'arrow', accessor: resolved, valueForBinding: value };
|
|
136
136
|
}
|
|
137
|
-
if (
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
continue;
|
|
147
|
-
const decl = stmt.declarationList.declarations[0];
|
|
148
|
-
if (!ts.isIdentifier(decl.name) || decl.name.text !== name)
|
|
149
|
-
continue;
|
|
150
|
-
if (!decl.initializer)
|
|
151
|
-
continue;
|
|
152
|
-
return decl.initializer;
|
|
153
|
-
}
|
|
137
|
+
if (ts.isFunctionDeclaration(resolved)) {
|
|
138
|
+
return { kind: 'fn-decl', accessor: resolved, valueForBinding: value };
|
|
139
|
+
}
|
|
140
|
+
if (isMemoCallWithArrowArg(resolved)) {
|
|
141
|
+
return {
|
|
142
|
+
kind: 'memo-call',
|
|
143
|
+
accessor: resolved.arguments[0],
|
|
144
|
+
valueForBinding: value,
|
|
145
|
+
};
|
|
154
146
|
}
|
|
155
|
-
|
|
147
|
+
if (isStaticPrimitiveLiteral(resolved)) {
|
|
148
|
+
return { kind: 'static-literal' };
|
|
149
|
+
}
|
|
150
|
+
// Resolved to something else (object/array/expression) — conservative
|
|
151
|
+
// bail. We don't know if the runtime value is a function; the runtime
|
|
152
|
+
// element helper handles both cases correctly.
|
|
153
|
+
return { kind: 'bail' };
|
|
154
|
+
}
|
|
155
|
+
// Static literals at the call site
|
|
156
|
+
if (isStaticPrimitiveLiteral(value)) {
|
|
157
|
+
return { kind: 'static-literal' };
|
|
156
158
|
}
|
|
159
|
+
// CallExpression — caller decides (per-item, etc.)
|
|
157
160
|
return null;
|
|
158
161
|
}
|
|
159
162
|
function classifyKind(key) {
|
|
@@ -821,70 +824,92 @@ function tryTransformElementCall(node, helpers, fieldBits, compiled, bailed, f)
|
|
|
821
824
|
events.push(f.createArrayLiteralExpression([f.createStringLiteral(eventName), value]));
|
|
822
825
|
continue;
|
|
823
826
|
}
|
|
824
|
-
//
|
|
825
|
-
//
|
|
826
|
-
//
|
|
827
|
-
//
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
const
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
827
|
+
// Per-item shapes — handled before the general classifier because
|
|
828
|
+
// they appear inside `each().render` callbacks where `item` is a
|
|
829
|
+
// closed-over per-row accessor (zero-arg). The resolver above can't
|
|
830
|
+
// see them; they're shape-matched syntactically.
|
|
831
|
+
if (isPerItemFieldAccess(value) || isHoistedPerItem(value)) {
|
|
832
|
+
const kind = classifyKind(key);
|
|
833
|
+
const resolvedKey = resolveKey(key, kind);
|
|
834
|
+
bindings.push(f.createArrayLiteralExpression([
|
|
835
|
+
createMaskLiteral(f, 0xffffffff | 0),
|
|
836
|
+
f.createStringLiteral(kind),
|
|
837
|
+
f.createStringLiteral(resolvedKey),
|
|
838
|
+
value,
|
|
839
|
+
]));
|
|
840
|
+
continue;
|
|
834
841
|
}
|
|
835
|
-
|
|
836
|
-
if (ts.isArrowFunction(value) || ts.isFunctionExpression(value)) {
|
|
842
|
+
if (ts.isCallExpression(value) && isPerItemCall(value)) {
|
|
837
843
|
const kind = classifyKind(key);
|
|
838
844
|
const resolvedKey = resolveKey(key, kind);
|
|
839
|
-
const { mask, readsState } = computeAccessorMask(value, fieldBits);
|
|
840
|
-
// Zero-mask constant folding: accessor doesn't read state → treat as static
|
|
841
|
-
if (mask === 0 && !readsState) {
|
|
842
|
-
emitStaticProp(staticProps, f, kind, resolvedKey, f.createCallExpression(value, undefined, []));
|
|
843
|
-
continue;
|
|
844
|
-
}
|
|
845
845
|
bindings.push(f.createArrayLiteralExpression([
|
|
846
|
-
createMaskLiteral(f,
|
|
846
|
+
createMaskLiteral(f, 0xffffffff | 0),
|
|
847
847
|
f.createStringLiteral(kind),
|
|
848
848
|
f.createStringLiteral(resolvedKey),
|
|
849
849
|
value,
|
|
850
850
|
]));
|
|
851
851
|
continue;
|
|
852
852
|
}
|
|
853
|
-
//
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
// Unknown call expression — bail out
|
|
853
|
+
// Classify the value at a reactive-prop position:
|
|
854
|
+
// - inline arrow / fn-expr at the call site
|
|
855
|
+
// - inline `memo(arrow)` at the call site
|
|
856
|
+
// - Identifier referencing a const-bound arrow/fn-expr in scope
|
|
857
|
+
// - Identifier referencing a hoisted function declaration in scope
|
|
858
|
+
// - Identifier referencing `const x = memo(arrow)` in scope
|
|
859
|
+
// - Identifier referencing a static primitive literal
|
|
860
|
+
// - Anything else (imports, parameters, opaque expressions) — bail
|
|
861
|
+
// to runtime; the runtime helper handles `typeof v === 'function'`
|
|
862
|
+
// correctly for both function and primitive values.
|
|
863
|
+
const classified = classifyReactiveValue(value);
|
|
864
|
+
if (classified === null) {
|
|
865
|
+
// Unknown shape (a CallExpression that isn't memo/per-item, etc.)
|
|
866
|
+
// — historically bailed to runtime. Preserve that.
|
|
868
867
|
bailed.add(localName);
|
|
869
868
|
return null;
|
|
870
869
|
}
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
870
|
+
if (classified.kind === 'bail') {
|
|
871
|
+
bailed.add(localName);
|
|
872
|
+
return null;
|
|
873
|
+
}
|
|
874
|
+
if (classified.kind === 'static-literal') {
|
|
875
|
+
// Fall through to emitStaticProp (`__e.disabled = X`). Safe because
|
|
876
|
+
// we proved X is a primitive.
|
|
877
|
+
const kind = classifyKind(key);
|
|
878
|
+
const resolvedKey = resolveKey(key, kind);
|
|
879
|
+
emitStaticProp(staticProps, f, kind, resolvedKey, value);
|
|
880
|
+
continue;
|
|
881
|
+
}
|
|
882
|
+
// 'arrow' | 'fn-decl' | 'memo-call' — emit as a binding tuple. Mask is
|
|
883
|
+
// analyzed from the resolved accessor body (or the inner arrow inside
|
|
884
|
+
// a memo() call); the value emitted into the binding tuple is what the
|
|
885
|
+
// runtime calls as `accessor(state)` — for inline arrows we keep the
|
|
886
|
+
// arrow itself (preserves the historical inlining behavior), for
|
|
887
|
+
// identifier-bound forms we keep the identifier so consumers see
|
|
888
|
+
// a single canonical reference (and `memo()` proxies aren't rebuilt
|
|
889
|
+
// per render).
|
|
890
|
+
{
|
|
874
891
|
const kind = classifyKind(key);
|
|
875
892
|
const resolvedKey = resolveKey(key, kind);
|
|
893
|
+
const { mask, readsState } = computeAccessorMask(classified.accessor, fieldBits);
|
|
894
|
+
// Zero-mask constant folding only applies to inline arrows whose body
|
|
895
|
+
// we can safely call at compile time. For identifier-bound forms
|
|
896
|
+
// (`accessor !== value`) we skip the fold — calling the identifier's
|
|
897
|
+
// declaration at compile time would be unsafe (different scope) and
|
|
898
|
+
// calling the identifier in the emitted output would defeat the point.
|
|
899
|
+
if (classified.kind === 'arrow' &&
|
|
900
|
+
classified.accessor === value &&
|
|
901
|
+
mask === 0 &&
|
|
902
|
+
!readsState) {
|
|
903
|
+
emitStaticProp(staticProps, f, kind, resolvedKey, f.createCallExpression(classified.accessor, undefined, []));
|
|
904
|
+
continue;
|
|
905
|
+
}
|
|
876
906
|
bindings.push(f.createArrayLiteralExpression([
|
|
877
|
-
createMaskLiteral(f, 0xffffffff | 0),
|
|
907
|
+
createMaskLiteral(f, mask === 0 && readsState ? 0xffffffff | 0 : mask),
|
|
878
908
|
f.createStringLiteral(kind),
|
|
879
909
|
f.createStringLiteral(resolvedKey),
|
|
880
|
-
|
|
910
|
+
classified.valueForBinding,
|
|
881
911
|
]));
|
|
882
|
-
continue;
|
|
883
912
|
}
|
|
884
|
-
// Static prop
|
|
885
|
-
const kind = classifyKind(key);
|
|
886
|
-
const resolvedKey = resolveKey(key, kind);
|
|
887
|
-
emitStaticProp(staticProps, f, kind, resolvedKey, value);
|
|
888
913
|
}
|
|
889
914
|
}
|
|
890
915
|
// Build elSplit args
|
|
@@ -971,13 +996,18 @@ function tryInjectTextMask(node, lluiImport, viewHelperNames, viewHelperAliases,
|
|
|
971
996
|
const firstArg = node.arguments[0];
|
|
972
997
|
if (!firstArg)
|
|
973
998
|
return null;
|
|
974
|
-
// Only inject mask for accessor functions, not static strings
|
|
975
|
-
if (!ts.isArrowFunction(firstArg) && !ts.isFunctionExpression(firstArg))
|
|
976
|
-
return null;
|
|
977
999
|
// Don't inject if mask already provided
|
|
978
1000
|
if (node.arguments.length >= 2)
|
|
979
1001
|
return null;
|
|
980
|
-
|
|
1002
|
+
// Resolve the accessor body — accepts inline arrows, `memo(arrow)`, or
|
|
1003
|
+
// identifier references to a const-bound arrow / `memo(...)` / function
|
|
1004
|
+
// declaration in scope. Anything else (static strings, opaque imports,
|
|
1005
|
+
// parameters) leaves the call as-is — the runtime falls back to
|
|
1006
|
+
// FULL_MASK, which is correct but slower.
|
|
1007
|
+
const accessor = resolveAccessorBody(firstArg);
|
|
1008
|
+
if (!accessor)
|
|
1009
|
+
return null;
|
|
1010
|
+
const { mask } = computeAccessorMask(accessor, fieldBits);
|
|
981
1011
|
return f.createCallExpression(node.expression, node.typeArguments, [
|
|
982
1012
|
firstArg,
|
|
983
1013
|
createMaskLiteral(f, mask === 0 ? 0xffffffff | 0 : mask),
|
|
@@ -1020,9 +1050,11 @@ function tryInjectStructuralMask(node, viewHelperNames, viewHelperAliases, field
|
|
|
1020
1050
|
if (ts.isPropertyAssignment(prop) &&
|
|
1021
1051
|
ts.isIdentifier(prop.name) &&
|
|
1022
1052
|
prop.name.text === driverProp) {
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1053
|
+
// Same shape contract as `text()`'s first arg: inline arrow, inline
|
|
1054
|
+
// `memo(arrow)`, or identifier referencing a const-bound arrow /
|
|
1055
|
+
// memo / function declaration. Anything else leaves the call
|
|
1056
|
+
// unchanged — runtime falls back to FULL_MASK.
|
|
1057
|
+
driverAccessor = resolveAccessorBody(prop.initializer);
|
|
1026
1058
|
break;
|
|
1027
1059
|
}
|
|
1028
1060
|
}
|
|
@@ -3392,16 +3424,31 @@ function isHoistedPerItem(node) {
|
|
|
3392
3424
|
// mask = 0 + readsState = false → constant (can fold to static)
|
|
3393
3425
|
// mask = 0 + readsState = true → unresolvable state access (FULL_MASK)
|
|
3394
3426
|
// mask > 0 → precise mask
|
|
3395
|
-
|
|
3427
|
+
// See `NON_DELEGATION_HELPERS` in collect-deps.ts — same set of names
|
|
3428
|
+
// that aren't followed when scanning for `helper(s)` delegation calls.
|
|
3429
|
+
const NON_DELEGATION_HELPERS = new Set(['sample', 'item', 'memo', 'text', 'unsafeHtml']);
|
|
3430
|
+
function computeAccessorMask(accessor, fieldBits, visited = new Set()) {
|
|
3431
|
+
if (visited.has(accessor))
|
|
3432
|
+
return { mask: 0, readsState: false };
|
|
3433
|
+
visited.add(accessor);
|
|
3396
3434
|
if (accessor.parameters.length === 0)
|
|
3397
3435
|
return { mask: 0xffffffff | 0, readsState: false };
|
|
3398
3436
|
const paramName = accessor.parameters[0].name;
|
|
3399
3437
|
if (!ts.isIdentifier(paramName))
|
|
3400
3438
|
return { mask: 0xffffffff | 0, readsState: false };
|
|
3439
|
+
// FunctionDeclaration always has a body (we never resolve overloads here);
|
|
3440
|
+
// ArrowFunction's body may be a single expression. Both shapes are walked
|
|
3441
|
+
// identically by ts.forEachChild, so no special-casing is needed below.
|
|
3442
|
+
if (!accessor.body)
|
|
3443
|
+
return { mask: 0xffffffff | 0, readsState: false };
|
|
3401
3444
|
const stateParam = paramName.text;
|
|
3402
3445
|
let mask = 0;
|
|
3403
3446
|
let readsState = false;
|
|
3404
|
-
|
|
3447
|
+
// `inNestedFn` gates only the delegation-recursion. Property-access
|
|
3448
|
+
// path extraction happens everywhere — inner-arrow callbacks like
|
|
3449
|
+
// `s.items.filter((i) => i.includes(s.filter))` close over our
|
|
3450
|
+
// state, and their `s.filter` reads contribute to the mask.
|
|
3451
|
+
function walk(node, inNestedFn) {
|
|
3405
3452
|
// `node.parent` can be undefined for synthetic nodes produced by
|
|
3406
3453
|
// earlier AST-transform passes (the row-factory rewrite and the
|
|
3407
3454
|
// per-item heuristic both build new sub-trees whose inner nodes
|
|
@@ -3429,8 +3476,17 @@ function computeAccessorMask(accessor, fieldBits) {
|
|
|
3429
3476
|
mask |= bit;
|
|
3430
3477
|
}
|
|
3431
3478
|
else {
|
|
3479
|
+
// Match paths that overlap our chain in either direction:
|
|
3480
|
+
// - `path` extends `chain` — fieldBits has finer-grained paths
|
|
3481
|
+
// than we're reading (e.g. chain='user', fieldBits has
|
|
3482
|
+
// 'user.email').
|
|
3483
|
+
// - `chain` extends `path` — we're reading deeper than what
|
|
3484
|
+
// fieldBits tracks (e.g. chain='items.filter' from
|
|
3485
|
+
// `s.items.filter(...)`, fieldBits has 'items'). Both ends
|
|
3486
|
+
// must mask in: a change to `items` invalidates anything
|
|
3487
|
+
// downstream of it.
|
|
3432
3488
|
for (const [path, b] of fieldBits) {
|
|
3433
|
-
if (path.startsWith(chain + '.') || path
|
|
3489
|
+
if (path === chain || path.startsWith(chain + '.') || chain.startsWith(path + '.')) {
|
|
3434
3490
|
mask |= b;
|
|
3435
3491
|
}
|
|
3436
3492
|
}
|
|
@@ -3438,9 +3494,30 @@ function computeAccessorMask(accessor, fieldBits) {
|
|
|
3438
3494
|
}
|
|
3439
3495
|
}
|
|
3440
3496
|
}
|
|
3441
|
-
|
|
3497
|
+
// Delegation: `helper(s)` where `s` matches our state param.
|
|
3498
|
+
// Recurse into the helper's body so its state-path reads
|
|
3499
|
+
// contribute to our mask. Only at top level — inside a nested
|
|
3500
|
+
// function body, `s` may be shadowed and the call isn't
|
|
3501
|
+
// unambiguously handing our state in.
|
|
3502
|
+
if (!inNestedFn && ts.isCallExpression(node) && ts.isIdentifier(node.expression)) {
|
|
3503
|
+
const calleeName = node.expression.text;
|
|
3504
|
+
if (!NON_DELEGATION_HELPERS.has(calleeName)) {
|
|
3505
|
+
const arg0 = node.arguments[0];
|
|
3506
|
+
if (arg0 && ts.isIdentifier(arg0) && arg0.text === stateParam) {
|
|
3507
|
+
const resolved = resolveAccessorBody(node.expression);
|
|
3508
|
+
if (resolved) {
|
|
3509
|
+
const inner = computeAccessorMask(resolved, fieldBits, visited);
|
|
3510
|
+
mask |= inner.mask;
|
|
3511
|
+
if (inner.readsState)
|
|
3512
|
+
readsState = true;
|
|
3513
|
+
}
|
|
3514
|
+
}
|
|
3515
|
+
}
|
|
3516
|
+
}
|
|
3517
|
+
const enteringNested = ts.isArrowFunction(node) || ts.isFunctionExpression(node) || ts.isFunctionDeclaration(node);
|
|
3518
|
+
ts.forEachChild(node, (child) => walk(child, inNestedFn || enteringNested));
|
|
3442
3519
|
}
|
|
3443
|
-
walk(accessor.body);
|
|
3520
|
+
walk(accessor.body, false);
|
|
3444
3521
|
if (mask === 0 && readsState) {
|
|
3445
3522
|
return { mask: 0xffffffff | 0, readsState: true };
|
|
3446
3523
|
}
|