@llui/compiler 0.5.1 → 0.5.2
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/collect-deps.d.ts +5 -1
- package/dist/collect-deps.d.ts.map +1 -1
- package/dist/collect-deps.js +93 -14
- package/dist/collect-deps.js.map +1 -1
- package/dist/cross-file-walker.d.ts +4 -1
- package/dist/cross-file-walker.d.ts.map +1 -1
- package/dist/cross-file-walker.js +72 -9
- package/dist/cross-file-walker.js.map +1 -1
- package/dist/lint-modules.d.ts.map +1 -1
- package/dist/lint-modules.js +2 -0
- package/dist/lint-modules.js.map +1 -1
- package/dist/modules/bitmask-overflow.js +1 -1
- package/dist/modules/bitmask-overflow.js.map +1 -1
- package/dist/modules/core-synthesis.d.ts +7 -0
- package/dist/modules/core-synthesis.d.ts.map +1 -1
- package/dist/modules/core-synthesis.js +24 -12
- package/dist/modules/core-synthesis.js.map +1 -1
- package/dist/modules/opaque-state-flow.d.ts +3 -0
- package/dist/modules/opaque-state-flow.d.ts.map +1 -0
- package/dist/modules/opaque-state-flow.js +208 -0
- package/dist/modules/opaque-state-flow.js.map +1 -0
- package/dist/transform.d.ts +1 -1
- package/dist/transform.d.ts.map +1 -1
- package/dist/transform.js +75 -5
- package/dist/transform.js.map +1 -1
- package/package.json +1 -1
package/dist/transform.js
CHANGED
|
@@ -104,7 +104,7 @@ const ELEMENT_HELPERS = new Set([
|
|
|
104
104
|
'ul',
|
|
105
105
|
'video',
|
|
106
106
|
]);
|
|
107
|
-
export function transformLlui(source, _filename, devMode = false, emitAgentMetadata = false, mcpPort = 5200, verbose = false, typeSources, preExtracted, crossFilePaths) {
|
|
107
|
+
export function transformLlui(source, _filename, devMode = false, emitAgentMetadata = false, mcpPort = 5200, verbose = false, typeSources, preExtracted, crossFilePaths, crossFileOpaque = false) {
|
|
108
108
|
// Use the caller-provided filename so any module reading `sf.fileName`
|
|
109
109
|
// (e.g. `componentMetaModule` emitting `__componentMeta: { file }`)
|
|
110
110
|
// sees the real path instead of a placeholder. The monolith's inline
|
|
@@ -164,9 +164,13 @@ export function transformLlui(source, _filename, devMode = false, emitAgentMetad
|
|
|
164
164
|
// function is generated per-component, so bit assignments in other files
|
|
165
165
|
// won't match. Files without component() get FULL_MASK on all bindings.
|
|
166
166
|
const fileHasComponent = hasComponentDef(sourceFile, lluiImport);
|
|
167
|
-
const { lo: fieldBits, hi: fieldBitsHi } = fileHasComponent
|
|
167
|
+
const { lo: fieldBits, hi: fieldBitsHi, opaque: fileLocalOpaque, } = fileHasComponent
|
|
168
168
|
? collectDeps(source, crossFilePaths)
|
|
169
|
-
: { lo: new Map(), hi: new Map() };
|
|
169
|
+
: { lo: new Map(), hi: new Map(), opaque: false };
|
|
170
|
+
// Union the file-local opaque flag with the cross-file flag from the
|
|
171
|
+
// vite-plugin's walker — either can independently mandate a
|
|
172
|
+
// whole-state sentinel in `__prefixes`.
|
|
173
|
+
const hasOpaqueAccessor = fileLocalOpaque || crossFileOpaque;
|
|
170
174
|
if (verbose && fileHasComponent) {
|
|
171
175
|
const pairs = [...fieldBits.entries()]
|
|
172
176
|
.map(([path, bit]) => `${path}=${bit === -1 ? 'FULL' : bit}`)
|
|
@@ -357,6 +361,7 @@ export function transformLlui(source, _filename, devMode = false, emitAgentMetad
|
|
|
357
361
|
fieldBits,
|
|
358
362
|
fieldBitsHi,
|
|
359
363
|
lluiImport,
|
|
364
|
+
hasOpaqueAccessor,
|
|
360
365
|
}));
|
|
361
366
|
// structuralMaskModule injects `__mask` into each()/branch()/scope()/show()
|
|
362
367
|
// options. Activated when the file has any low-word reactive paths
|
|
@@ -1356,6 +1361,15 @@ export function computeAccessorMask(accessor, fieldBits, visited = new Set(), fi
|
|
|
1356
1361
|
let mask = 0;
|
|
1357
1362
|
let maskHi = 0;
|
|
1358
1363
|
let readsState = false;
|
|
1364
|
+
// The state value flows into an expression we can't statically trace
|
|
1365
|
+
// (function-arg / imported / destructured / method callee, or a
|
|
1366
|
+
// dynamic `s[expr]` lookup). The callee may read any field; the
|
|
1367
|
+
// dynamic key may index any field. Any non-empty precise mask we'd
|
|
1368
|
+
// compute from the visible direct reads alone is "clean but wrong":
|
|
1369
|
+
// a reducer that narrowly touches only the opaquely-read field
|
|
1370
|
+
// produces a dirty bit that `mask & dirty` zeroes, silently skipping
|
|
1371
|
+
// the binding. Conservative correctness: bail to FULL_MASK.
|
|
1372
|
+
let opaqueStateFlow = false;
|
|
1359
1373
|
// `inNestedFn` gates only the delegation-recursion. Property-access
|
|
1360
1374
|
// path extraction happens everywhere — inner-arrow callbacks like
|
|
1361
1375
|
// `s.items.filter((i) => i.includes(s.filter))` close over our
|
|
@@ -1369,8 +1383,48 @@ export function computeAccessorMask(accessor, fieldBits, visited = new Set(), fi
|
|
|
1369
1383
|
// like `text((_s) => \`$${item.x.toLocaleString()}\`)` was how
|
|
1370
1384
|
// this bug first surfaced in the persistent-layout example work.
|
|
1371
1385
|
const parent = node.parent;
|
|
1372
|
-
|
|
1373
|
-
|
|
1386
|
+
// Every appearance of the state identifier `s` is one of:
|
|
1387
|
+
// - the parameter binding itself — ignore
|
|
1388
|
+
// - the root of `s.x.y…` (PropertyAccessExpression) — tracked by the PAE walker
|
|
1389
|
+
// - the root of `s['literal']`/`s[0]` — tracked by element-access (literal key only)
|
|
1390
|
+
// - arg0 of `helper(s)` with an Identifier callee — handled by the delegation branch below
|
|
1391
|
+
// - anything else (spread, return, ternary branch, template span,
|
|
1392
|
+
// NewExpression arg, TaggedTemplate value, const alias, arg1+ of any call,
|
|
1393
|
+
// method-call arg `obj.f(s)`, dynamic key `s[expr]`, type assertion,
|
|
1394
|
+
// parenthesized, …) — opaque: state has leaked
|
|
1395
|
+
//
|
|
1396
|
+
// The leak cases can't be reasoned about statically — the receiver
|
|
1397
|
+
// may read any field of `s`. A "precise" mask built from sibling
|
|
1398
|
+
// direct reads alone hides every field reachable only through the
|
|
1399
|
+
// leak, and (mask & dirty) silently skips updates. Conservative
|
|
1400
|
+
// correctness: any opaque flow forces FULL_MASK.
|
|
1401
|
+
if (ts.isIdentifier(node) && node.text === stateParam) {
|
|
1402
|
+
const isBinding = !!parent && ts.isParameter(parent);
|
|
1403
|
+
if (!isBinding) {
|
|
1404
|
+
readsState = true;
|
|
1405
|
+
let isTracked = false;
|
|
1406
|
+
if (parent) {
|
|
1407
|
+
if (ts.isPropertyAccessExpression(parent) && parent.expression === node) {
|
|
1408
|
+
isTracked = true;
|
|
1409
|
+
}
|
|
1410
|
+
else if (ts.isElementAccessExpression(parent) && parent.expression === node) {
|
|
1411
|
+
isTracked =
|
|
1412
|
+
ts.isStringLiteralLike(parent.argumentExpression) ||
|
|
1413
|
+
ts.isNumericLiteral(parent.argumentExpression);
|
|
1414
|
+
}
|
|
1415
|
+
else if (ts.isCallExpression(parent) &&
|
|
1416
|
+
ts.isIdentifier(parent.expression) &&
|
|
1417
|
+
parent.arguments[0] === node &&
|
|
1418
|
+
!NON_DELEGATION_HELPERS.has(parent.expression.text)) {
|
|
1419
|
+
// The delegation branch below either recurses into the
|
|
1420
|
+
// resolved body or sets opaqueStateFlow explicitly when
|
|
1421
|
+
// unresolvable — don't pre-empt that decision here.
|
|
1422
|
+
isTracked = true;
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
if (!isTracked)
|
|
1426
|
+
opaqueStateFlow = true;
|
|
1427
|
+
}
|
|
1374
1428
|
}
|
|
1375
1429
|
if (ts.isPropertyAccessExpression(node)) {
|
|
1376
1430
|
// When there's no parent we can't tell if this is the top of a
|
|
@@ -1437,6 +1491,14 @@ export function computeAccessorMask(accessor, fieldBits, visited = new Set(), fi
|
|
|
1437
1491
|
if (inner.readsState)
|
|
1438
1492
|
readsState = true;
|
|
1439
1493
|
}
|
|
1494
|
+
else {
|
|
1495
|
+
// Callee is a function parameter, imported binding, or
|
|
1496
|
+
// destructured local — `resolveAccessorBody` couldn't pin
|
|
1497
|
+
// it to a local declaration. The body could read any
|
|
1498
|
+
// field of `s`, so a precise mask from sibling direct
|
|
1499
|
+
// reads alone is unsafe.
|
|
1500
|
+
opaqueStateFlow = true;
|
|
1501
|
+
}
|
|
1440
1502
|
}
|
|
1441
1503
|
}
|
|
1442
1504
|
}
|
|
@@ -1444,6 +1506,14 @@ export function computeAccessorMask(accessor, fieldBits, visited = new Set(), fi
|
|
|
1444
1506
|
ts.forEachChild(node, (child) => walk(child, inNestedFn || enteringNested));
|
|
1445
1507
|
}
|
|
1446
1508
|
walk(accessor.body, false);
|
|
1509
|
+
if (opaqueStateFlow) {
|
|
1510
|
+
// Both words FULL_MASK: the whole-state sentinel emitted by
|
|
1511
|
+
// `core-synthesis.ts:buildPrefixesProp` may land in either the
|
|
1512
|
+
// low or high word depending on the file's prefix count. The
|
|
1513
|
+
// runtime gate `(mask & dirty) | (maskHi & dirtyHi)` only catches
|
|
1514
|
+
// the sentinel bit when the binding's mask covers BOTH words.
|
|
1515
|
+
return { mask: 0xffffffff | 0, maskHi: 0xffffffff | 0, readsState: true };
|
|
1516
|
+
}
|
|
1447
1517
|
if (mask === 0 && maskHi === 0 && readsState) {
|
|
1448
1518
|
return { mask: 0xffffffff | 0, maskHi: 0, readsState: true };
|
|
1449
1519
|
}
|