@llui/vite-plugin 0.0.39 → 0.0.41
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 +65 -32
- package/dist/collect-deps.js.map +1 -1
- package/dist/transform.d.ts.map +1 -1
- package/dist/transform.js +179 -112
- 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.
|
|
874
877
|
const kind = classifyKind(key);
|
|
875
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
|
+
{
|
|
891
|
+
const kind = classifyKind(key);
|
|
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
|
}
|
|
@@ -1247,6 +1279,33 @@ function detectArrayOp(clause, stateName, modifiedFields, _structuralMask, _case
|
|
|
1247
1279
|
// tautology: every binding mask ANDed with zero is zero.
|
|
1248
1280
|
if (modifiedFields.length === 0)
|
|
1249
1281
|
return 'none';
|
|
1282
|
+
// The specialized methods (`reconcileClear`, `reconcileItems`,
|
|
1283
|
+
// `reconcileRemove`, `reconcileChanged`) only exist on `each` blocks.
|
|
1284
|
+
// Non-each blocks (`show`, `branch`, `scope`) leave them undefined,
|
|
1285
|
+
// so a method other than 0 (general reconcile) silently no-ops on
|
|
1286
|
+
// those blocks at runtime. If the case modifies fields BEYOND the
|
|
1287
|
+
// array op (e.g. `{ ...state, open: true, name: '', tags: [] }`),
|
|
1288
|
+
// any show/branch block whose mask intersects the case's dirty bits
|
|
1289
|
+
// would be selected for reconcile but then skipped by the no-op
|
|
1290
|
+
// method invocation — its `when`/`on` accessor never re-evaluates,
|
|
1291
|
+
// and the component appears structurally inert after mount.
|
|
1292
|
+
//
|
|
1293
|
+
// Conservative correctness: only emit a non-general method when the
|
|
1294
|
+
// array op is the SOLE field modification. With one modified field,
|
|
1295
|
+
// the only blocks selected by mask gating are ones that read that
|
|
1296
|
+
// single field — and the optimization is well-defined for that
|
|
1297
|
+
// narrow case (each blocks operating on the array). Multi-field
|
|
1298
|
+
// cases fall through to `'general'` (method=0), so every selected
|
|
1299
|
+
// block runs the standard `reconcile` path. We trade a niche
|
|
1300
|
+
// optimization (small benefit even when applicable) for guaranteed
|
|
1301
|
+
// structural reconciliation across the framework's primitive set.
|
|
1302
|
+
//
|
|
1303
|
+
// Sister of show-helper-reconcile.test.ts, which fixed the same
|
|
1304
|
+
// class of bug on the method=-1 path. Same architectural principle:
|
|
1305
|
+
// the compiler can't see every block in the view, so optimizations
|
|
1306
|
+
// that route around `reconcile` must be ironclad. When in doubt,
|
|
1307
|
+
// emit method=0 and let `_handleMsg`'s per-block mask gate filter.
|
|
1308
|
+
//
|
|
1250
1309
|
// Previously: if `(structuralMask & caseDirty) === 0`, return 'none'
|
|
1251
1310
|
// on the theory that no structural block's mask could intersect this
|
|
1252
1311
|
// case's dirty bits. That optimization was UNSAFE: `computeStructuralMask`
|
|
@@ -1266,13 +1325,9 @@ function detectArrayOp(clause, stateName, modifiedFields, _structuralMask, _case
|
|
|
1266
1325
|
// when `errors` changes — but the compiler was emitting `method = -1`
|
|
1267
1326
|
// (skip blocks entirely) for cases that only touch `errors`, and the
|
|
1268
1327
|
// error paragraphs would never mount.
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
// `if (!(block.mask & dirty)) continue` filters out uninterested
|
|
1273
|
-
// blocks at near-zero cost. We lose a micro-optimization but gain
|
|
1274
|
-
// correctness for every component that factors view helpers into
|
|
1275
|
-
// functions — which is the idiomatic pattern.
|
|
1328
|
+
if (modifiedFields.length !== 1)
|
|
1329
|
+
return 'general';
|
|
1330
|
+
const onlyField = modifiedFields[0];
|
|
1276
1331
|
// Look at the return expression's array field values
|
|
1277
1332
|
for (const stmt of clause.statements) {
|
|
1278
1333
|
const returnExpr = findReturnArray(stmt);
|
|
@@ -1289,6 +1344,13 @@ function detectArrayOp(clause, stateName, modifiedFields, _structuralMask, _case
|
|
|
1289
1344
|
: null;
|
|
1290
1345
|
if (!name)
|
|
1291
1346
|
continue;
|
|
1347
|
+
// The optimization only applies when the array op is on the
|
|
1348
|
+
// single tracked field. A `field: []` on a different field
|
|
1349
|
+
// (one not in modifiedFields, e.g. an untracked field) would
|
|
1350
|
+
// still no-op safely on each blocks via the mask gate, but to
|
|
1351
|
+
// keep the analysis tight we require an exact match.
|
|
1352
|
+
if (name !== onlyField)
|
|
1353
|
+
continue;
|
|
1292
1354
|
// Check for empty array literal: `field: []`
|
|
1293
1355
|
if (ts.isPropertyAssignment(prop) &&
|
|
1294
1356
|
ts.isArrayLiteralExpression(prop.initializer) &&
|
|
@@ -3368,6 +3430,11 @@ function computeAccessorMask(accessor, fieldBits) {
|
|
|
3368
3430
|
const paramName = accessor.parameters[0].name;
|
|
3369
3431
|
if (!ts.isIdentifier(paramName))
|
|
3370
3432
|
return { mask: 0xffffffff | 0, readsState: false };
|
|
3433
|
+
// FunctionDeclaration always has a body (we never resolve overloads here);
|
|
3434
|
+
// ArrowFunction's body may be a single expression. Both shapes are walked
|
|
3435
|
+
// identically by ts.forEachChild, so no special-casing is needed below.
|
|
3436
|
+
if (!accessor.body)
|
|
3437
|
+
return { mask: 0xffffffff | 0, readsState: false };
|
|
3371
3438
|
const stateParam = paramName.text;
|
|
3372
3439
|
let mask = 0;
|
|
3373
3440
|
let readsState = false;
|