octane 0.1.28 → 0.1.30
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/compiler/compile.js +1246 -120
- package/dist/compiler/strong-mode.js +525 -8
- package/dist/index.d.ts +1 -1
- package/dist/index.js +10 -0
- package/dist/runtime.d.ts +35 -6
- package/dist/runtime.js +604 -228
- package/dist/runtime.server.js +14 -6
- package/package.json +5 -2
package/dist/compiler/compile.js
CHANGED
|
@@ -1758,6 +1758,62 @@ function moduleImportsViewTransition(astBody) {
|
|
|
1758
1758
|
}
|
|
1759
1759
|
}
|
|
1760
1760
|
|
|
1761
|
+
function classifyViewTransitionOwnership(astBody, production, ownComponents) {
|
|
1762
|
+
if (!moduleImportsViewTransition(astBody)) return { global: false, owners: new Set() };
|
|
1763
|
+
if (!production) return { global: true, owners: new Set() };
|
|
1764
|
+
|
|
1765
|
+
const imported = new Set();
|
|
1766
|
+
for (const statement of astBody) {
|
|
1767
|
+
if (statement.type === 'ImportDeclaration' && statement.source?.value === 'octane') {
|
|
1768
|
+
for (const specifier of statement.specifiers || []) {
|
|
1769
|
+
if (specifier.type === 'ImportNamespaceSpecifier') {
|
|
1770
|
+
return { global: true, owners: new Set() };
|
|
1771
|
+
}
|
|
1772
|
+
const name = specifier.imported?.name ?? specifier.imported?.value;
|
|
1773
|
+
if (name === 'ViewTransition' || name === 'unstable_ViewTransition') {
|
|
1774
|
+
imported.add(specifier.local.name);
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
} else if (
|
|
1778
|
+
(statement.type === 'ExportNamedDeclaration' || statement.type === 'ExportAllDeclaration') &&
|
|
1779
|
+
statement.source?.value === 'octane'
|
|
1780
|
+
) {
|
|
1781
|
+
return { global: true, owners: new Set() };
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
if (imported.size === 0) return { global: true, owners: new Set() };
|
|
1785
|
+
|
|
1786
|
+
const mentionsImportedTransition = (root) => {
|
|
1787
|
+
const free = collectFreeIdentifiers(
|
|
1788
|
+
root,
|
|
1789
|
+
isComponentFunction(root) ? collectComponentLocals(root) : new Set(),
|
|
1790
|
+
);
|
|
1791
|
+
for (const name of imported) {
|
|
1792
|
+
if (free.has(name)) return true;
|
|
1793
|
+
}
|
|
1794
|
+
return false;
|
|
1795
|
+
};
|
|
1796
|
+
|
|
1797
|
+
let global = false;
|
|
1798
|
+
const owners = new Set();
|
|
1799
|
+
for (const statement of astBody) {
|
|
1800
|
+
if (statement.type === 'ImportDeclaration') continue;
|
|
1801
|
+
const declaration =
|
|
1802
|
+
statement.type === 'ExportNamedDeclaration' || statement.type === 'ExportDefaultDeclaration'
|
|
1803
|
+
? statement.declaration
|
|
1804
|
+
: statement;
|
|
1805
|
+
if (!mentionsImportedTransition(declaration ?? statement)) continue;
|
|
1806
|
+
const exported =
|
|
1807
|
+
statement.type === 'ExportNamedDeclaration' || statement.type === 'ExportDefaultDeclaration';
|
|
1808
|
+
if (ownComponents && exported && isComponentFunction(declaration)) {
|
|
1809
|
+
owners.add(declaration.id.name);
|
|
1810
|
+
} else {
|
|
1811
|
+
global = true;
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
return { global, owners };
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1761
1817
|
export const HOOK_NAMES = new Set([
|
|
1762
1818
|
'useState',
|
|
1763
1819
|
'useLinkedState',
|
|
@@ -2213,7 +2269,7 @@ function isArrowStableOver(arrow, stable, componentLocals) {
|
|
|
2213
2269
|
* Idempotent: a const we already rewrote into `useCallback(...)` won't be
|
|
2214
2270
|
* re-wrapped (its init is now a CallExpression, not an ArrowFunctionExpression).
|
|
2215
2271
|
*/
|
|
2216
|
-
function rewriteAutoCallback(stmt, stable, componentLocals, ctx) {
|
|
2272
|
+
function rewriteAutoCallback(stmt, stable, componentLocals, ctx, invariant) {
|
|
2217
2273
|
if (stmt.type !== 'VariableDeclaration' || stmt.kind !== 'const') return stmt;
|
|
2218
2274
|
let modified = false;
|
|
2219
2275
|
const newDecls = stmt.declarations.map((decl) => {
|
|
@@ -2245,7 +2301,11 @@ function rewriteAutoCallback(stmt, stable, componentLocals, ctx) {
|
|
|
2245
2301
|
// `_octaneGenerated` tells rewriteHookCalls (which slots this call next)
|
|
2246
2302
|
// that the callee is compiler-inserted — it renames it to the shadow-proof
|
|
2247
2303
|
// `_$useCallback` alias instead of treating it as a user identifier.
|
|
2248
|
-
{
|
|
2304
|
+
{
|
|
2305
|
+
...b.id('useCallback'),
|
|
2306
|
+
_octaneGenerated: true,
|
|
2307
|
+
_octaneLifetimeInvariant: invariant.has(decl.id.name),
|
|
2308
|
+
},
|
|
2249
2309
|
arrow,
|
|
2250
2310
|
b.array(deps.map((n) => b.id(n))),
|
|
2251
2311
|
),
|
|
@@ -2336,6 +2396,68 @@ function rewriteAutoCalculation(stmt, componentLocals, renderReadNames, ctx) {
|
|
|
2336
2396
|
};
|
|
2337
2397
|
}
|
|
2338
2398
|
|
|
2399
|
+
// A cached calculation can own a renderable array only when its value never
|
|
2400
|
+
// escapes into setup, a callback, a component prop, or another expression. The
|
|
2401
|
+
// bare renderable holes are the sole permitted reads: then the same immutable
|
|
2402
|
+
// projection contract that admitted the calculation also witnesses the whole
|
|
2403
|
+
// array. Preserve the exact Identifier nodes rather than just their spelling so
|
|
2404
|
+
// a nested shadow cannot accidentally inherit an outer calculation's proof.
|
|
2405
|
+
function collectAutoCalculatedRenderableRefs(statements, jsxNodes, calculated) {
|
|
2406
|
+
if (calculated.size === 0) return null;
|
|
2407
|
+
const references = new Map();
|
|
2408
|
+
const seen = new WeakSet();
|
|
2409
|
+
function visit(node, isJsxChild = false) {
|
|
2410
|
+
if (node === null || typeof node !== 'object') return;
|
|
2411
|
+
if (Array.isArray(node)) {
|
|
2412
|
+
for (const child of node) visit(child, isJsxChild);
|
|
2413
|
+
return;
|
|
2414
|
+
}
|
|
2415
|
+
if (seen.has(node)) return;
|
|
2416
|
+
seen.add(node);
|
|
2417
|
+
if (node.type === 'Element' || node.type === 'JSXElement') {
|
|
2418
|
+
visit(node.children, true);
|
|
2419
|
+
return;
|
|
2420
|
+
}
|
|
2421
|
+
if (
|
|
2422
|
+
isJsxChild &&
|
|
2423
|
+
(node.type === 'Text' ||
|
|
2424
|
+
node.type === 'TSRXExpression' ||
|
|
2425
|
+
node.type === 'JSXExpressionContainer')
|
|
2426
|
+
) {
|
|
2427
|
+
const expression = node.expression;
|
|
2428
|
+
if (expression?.type === 'Identifier' && calculated.has(expression.name)) {
|
|
2429
|
+
let nodes = references.get(expression.name);
|
|
2430
|
+
if (nodes === undefined) references.set(expression.name, (nodes = new Set()));
|
|
2431
|
+
nodes.add(expression);
|
|
2432
|
+
}
|
|
2433
|
+
return;
|
|
2434
|
+
}
|
|
2435
|
+
for (const key in node) {
|
|
2436
|
+
if (AST_WALK_SKIP_KEYS.has(key)) continue;
|
|
2437
|
+
visit(node[key], key === 'children');
|
|
2438
|
+
}
|
|
2439
|
+
}
|
|
2440
|
+
visit(jsxNodes, true);
|
|
2441
|
+
|
|
2442
|
+
let proven = null;
|
|
2443
|
+
for (const [name, declaration] of calculated) {
|
|
2444
|
+
const nodes = references.get(name);
|
|
2445
|
+
if (nodes === undefined) continue;
|
|
2446
|
+
let escaped = false;
|
|
2447
|
+
for (const statement of statements) {
|
|
2448
|
+
if (statement === declaration) continue;
|
|
2449
|
+
if (collectFreeIdentifiers(statement, []).has(name)) {
|
|
2450
|
+
escaped = true;
|
|
2451
|
+
break;
|
|
2452
|
+
}
|
|
2453
|
+
}
|
|
2454
|
+
if (escaped || collectFreeIdentifiers(jsxNodes, [], nodes).has(name)) continue;
|
|
2455
|
+
if (proven === null) proven = new WeakSet();
|
|
2456
|
+
for (const node of nodes) proven.add(node);
|
|
2457
|
+
}
|
|
2458
|
+
return proven;
|
|
2459
|
+
}
|
|
2460
|
+
|
|
2339
2461
|
// Names the render tree actually reads, resolved against the scopes the
|
|
2340
2462
|
// statement walker cannot see (nested functions, loops, `@for` item bindings,
|
|
2341
2463
|
// directive-arm blocks) so a shadowing inner binding never nominates an
|
|
@@ -3343,6 +3465,67 @@ function containsComponentCallOrControlFlow(stmts) {
|
|
|
3343
3465
|
return found;
|
|
3344
3466
|
}
|
|
3345
3467
|
|
|
3468
|
+
/**
|
|
3469
|
+
* A host-only conditional can share its keyed row's immutable item/dependency
|
|
3470
|
+
* proof, but entering that conditional still needs the row's active scope. Keep
|
|
3471
|
+
* components and every other control-flow boundary on their existing path.
|
|
3472
|
+
*/
|
|
3473
|
+
function hasOnlyHostConditionalItemBodies(stmts) {
|
|
3474
|
+
let hasConditional = false;
|
|
3475
|
+
let disallowed = false;
|
|
3476
|
+
const seen = new WeakSet();
|
|
3477
|
+
function walk(node) {
|
|
3478
|
+
if (disallowed || !node) return;
|
|
3479
|
+
if (Array.isArray(node)) {
|
|
3480
|
+
for (const child of node) walk(child);
|
|
3481
|
+
return;
|
|
3482
|
+
}
|
|
3483
|
+
if (typeof node !== 'object') return;
|
|
3484
|
+
const type = node.type;
|
|
3485
|
+
if (!type || seen.has(node)) return;
|
|
3486
|
+
seen.add(node);
|
|
3487
|
+
if (
|
|
3488
|
+
type === 'ArrowFunctionExpression' ||
|
|
3489
|
+
type === 'FunctionExpression' ||
|
|
3490
|
+
type === 'FunctionDeclaration'
|
|
3491
|
+
) {
|
|
3492
|
+
return;
|
|
3493
|
+
}
|
|
3494
|
+
if ((type === 'Element' || type === 'JSXElement') && isComponentTag(node)) {
|
|
3495
|
+
disallowed = true;
|
|
3496
|
+
return;
|
|
3497
|
+
}
|
|
3498
|
+
if (type === 'IfStatement' || type === 'JSXIfExpression') {
|
|
3499
|
+
hasConditional = true;
|
|
3500
|
+
} else if (
|
|
3501
|
+
type === 'ForStatement' ||
|
|
3502
|
+
type === 'ForInStatement' ||
|
|
3503
|
+
type === 'ForOfStatement' ||
|
|
3504
|
+
type === 'WhileStatement' ||
|
|
3505
|
+
type === 'DoWhileStatement' ||
|
|
3506
|
+
type === 'TryStatement' ||
|
|
3507
|
+
type === 'SwitchStatement' ||
|
|
3508
|
+
type === 'ActivityStatement' ||
|
|
3509
|
+
type === 'JSXForExpression' ||
|
|
3510
|
+
type === 'JSXTryExpression' ||
|
|
3511
|
+
type === 'JSXSwitchExpression' ||
|
|
3512
|
+
type === 'JSXActivityExpression' ||
|
|
3513
|
+
((type === 'TSRXExpression' || type === 'JSXExpressionContainer') &&
|
|
3514
|
+
node.expression &&
|
|
3515
|
+
isCreatePortalCall(node.expression))
|
|
3516
|
+
) {
|
|
3517
|
+
disallowed = true;
|
|
3518
|
+
return;
|
|
3519
|
+
}
|
|
3520
|
+
for (const key in node) {
|
|
3521
|
+
if (AST_WALK_SKIP_KEYS.has(key)) continue;
|
|
3522
|
+
walk(node[key]);
|
|
3523
|
+
}
|
|
3524
|
+
}
|
|
3525
|
+
for (const statement of stmts) walk(statement);
|
|
3526
|
+
return hasConditional && !disallowed;
|
|
3527
|
+
}
|
|
3528
|
+
|
|
3346
3529
|
/**
|
|
3347
3530
|
* Classify the narrow opaque shape that an automatically memoized keyed item
|
|
3348
3531
|
* may safely contain: ordinary JSX component calls, but no template control
|
|
@@ -3535,15 +3718,16 @@ function containsRenderCall(stmts, memoCtx = null) {
|
|
|
3535
3718
|
}
|
|
3536
3719
|
|
|
3537
3720
|
// A hook call is identified by naming convention — the same signal React and
|
|
3538
|
-
// React Compiler key on — plus
|
|
3539
|
-
// bindings
|
|
3540
|
-
//
|
|
3541
|
-
//
|
|
3721
|
+
// React Compiler key on — plus the exact `unstable_` and `UNSTABLE_` staging
|
|
3722
|
+
// prefixes bindings expose (`unstable_useRouterState` in @octanejs/remix-router
|
|
3723
|
+
// and `UNSTABLE_useTreeGridState` in @octanejs/aria). Enumerate the prefixes
|
|
3724
|
+
// rather than matching "any `_use`" so ordinary helpers are not mistaken for
|
|
3725
|
+
// hooks by spelling alone.
|
|
3542
3726
|
//
|
|
3543
3727
|
// Getting this wrong in the permissive direction is not a staleness bug: a
|
|
3544
3728
|
// cache wrapped around a hook call freezes its subscription and its state cell
|
|
3545
3729
|
// for the life of the component.
|
|
3546
|
-
const HOOK_NAME_CONVENTION_RE = /^(?:
|
|
3730
|
+
const HOOK_NAME_CONVENTION_RE = /^(?:(?:unstable|UNSTABLE)_)?use(?:$|[A-Z])/;
|
|
3547
3731
|
|
|
3548
3732
|
function isHookCalleeName(name) {
|
|
3549
3733
|
return HOOK_NAME_CONVENTION_RE.test(name);
|
|
@@ -3716,6 +3900,202 @@ function collectImmutableModuleFunctions(body) {
|
|
|
3716
3900
|
return declared;
|
|
3717
3901
|
}
|
|
3718
3902
|
|
|
3903
|
+
// A child warm plan is useful only if that child can reach an async creation.
|
|
3904
|
+
// Same-module declarations are the only closed call graph we can prove: an
|
|
3905
|
+
// imported/dynamic component, custom hook, helper call, or lazy state initializer
|
|
3906
|
+
// may suspend behind an opaque boundary, so each keeps its existing warm edge.
|
|
3907
|
+
// A useState call with a primitive literal initializer and publishing its stable
|
|
3908
|
+
// setter are synchronous, so neither turns a synchronous tree into a warm plan.
|
|
3909
|
+
function classifySameModuleWarmPotential(ctx) {
|
|
3910
|
+
for (const [, info] of ctx.componentInfo) {
|
|
3911
|
+
const component = info.node;
|
|
3912
|
+
const statements = component.body.body || [];
|
|
3913
|
+
const locals = collectComponentLocals(component);
|
|
3914
|
+
const invariant = computeInvariantLocals(statements, locals, false);
|
|
3915
|
+
const dependencies = new Set();
|
|
3916
|
+
const seen = new WeakSet();
|
|
3917
|
+
// A reassigned function binding can point at an async component by the
|
|
3918
|
+
// time its warm edge runs. Destructuring/default/rest parameters can also
|
|
3919
|
+
// invoke user code before the authored component body is reached.
|
|
3920
|
+
let opaque =
|
|
3921
|
+
!ctx.moduleFunctionDeclarations.has(component.id?.name) ||
|
|
3922
|
+
(component.params || []).some((parameter) => parameter.type !== 'Identifier');
|
|
3923
|
+
|
|
3924
|
+
function walk(node) {
|
|
3925
|
+
if (opaque || node === null || typeof node !== 'object') return;
|
|
3926
|
+
if (Array.isArray(node)) {
|
|
3927
|
+
for (const child of node) walk(child);
|
|
3928
|
+
return;
|
|
3929
|
+
}
|
|
3930
|
+
if (seen.has(node)) return;
|
|
3931
|
+
seen.add(node);
|
|
3932
|
+
|
|
3933
|
+
// Deferred handlers do not execute during this component's render. State
|
|
3934
|
+
// initializers are admitted only when proven primitive and non-callable.
|
|
3935
|
+
if (FN_TYPES.has(node.type)) return;
|
|
3936
|
+
|
|
3937
|
+
if ((node.type === 'Element' || node.type === 'JSXElement') && isComponentTag(node)) {
|
|
3938
|
+
const name = tagBindingName(node);
|
|
3939
|
+
if (
|
|
3940
|
+
name === null ||
|
|
3941
|
+
locals.has(name) ||
|
|
3942
|
+
ctx._octaneBoundaryNames.has(name) ||
|
|
3943
|
+
!ctx.componentInfo.has(name)
|
|
3944
|
+
) {
|
|
3945
|
+
opaque = true;
|
|
3946
|
+
return;
|
|
3947
|
+
}
|
|
3948
|
+
dependencies.add(name);
|
|
3949
|
+
} else if (node.type === 'CallExpression' || node.type === 'NewExpression') {
|
|
3950
|
+
const hook = stableHookCallName(node);
|
|
3951
|
+
if (
|
|
3952
|
+
hook !== 'useState' ||
|
|
3953
|
+
node.arguments.length > 1 ||
|
|
3954
|
+
(node.arguments.length === 1 && !isInvariantLiteral(unwrapTsExpr(node.arguments[0])))
|
|
3955
|
+
) {
|
|
3956
|
+
opaque = true;
|
|
3957
|
+
return;
|
|
3958
|
+
}
|
|
3959
|
+
} else if (node.type === 'VariableDeclarator' && node.id?.type === 'ArrayPattern') {
|
|
3960
|
+
// The built-in state tuple is the only iterator this proof owns. Any
|
|
3961
|
+
// other destructuring can execute a custom iterator or rest/default.
|
|
3962
|
+
if (
|
|
3963
|
+
stableHookCallName(unwrapTsExpr(node.init)) !== 'useState' ||
|
|
3964
|
+
node.id.elements.some((element) => element !== null && element.type !== 'Identifier')
|
|
3965
|
+
) {
|
|
3966
|
+
opaque = true;
|
|
3967
|
+
return;
|
|
3968
|
+
}
|
|
3969
|
+
} else if (node.type === 'AssignmentExpression') {
|
|
3970
|
+
if (
|
|
3971
|
+
node.operator !== '=' ||
|
|
3972
|
+
node.left?.type !== 'Identifier' ||
|
|
3973
|
+
node.right?.type !== 'Identifier' ||
|
|
3974
|
+
!invariant.has(node.right.name)
|
|
3975
|
+
) {
|
|
3976
|
+
opaque = true;
|
|
3977
|
+
return;
|
|
3978
|
+
}
|
|
3979
|
+
} else if (
|
|
3980
|
+
node.type === 'AwaitExpression' ||
|
|
3981
|
+
node.type === 'YieldExpression' ||
|
|
3982
|
+
node.type === 'MemberExpression' ||
|
|
3983
|
+
node.type === 'JSXMemberExpression' ||
|
|
3984
|
+
node.type === 'OptionalMemberExpression' ||
|
|
3985
|
+
node.type === 'OptionalCallExpression' ||
|
|
3986
|
+
node.type === 'SpreadElement' ||
|
|
3987
|
+
node.type === 'SpreadAttribute' ||
|
|
3988
|
+
node.type === 'JSXSpreadAttribute' ||
|
|
3989
|
+
node.type === 'ObjectPattern' ||
|
|
3990
|
+
node.type === 'AssignmentPattern' ||
|
|
3991
|
+
node.type === 'RestElement' ||
|
|
3992
|
+
node.type === 'ForOfStatement' ||
|
|
3993
|
+
node.type === 'JSXForExpression' ||
|
|
3994
|
+
node.type === 'ForInStatement' ||
|
|
3995
|
+
node.type === 'ForStatement' ||
|
|
3996
|
+
node.type === 'WhileStatement' ||
|
|
3997
|
+
node.type === 'DoWhileStatement' ||
|
|
3998
|
+
node.type === 'ThrowStatement' ||
|
|
3999
|
+
node.type === 'TryStatement' ||
|
|
4000
|
+
node.type === 'JSXTryExpression' ||
|
|
4001
|
+
node.type === 'ImportExpression' ||
|
|
4002
|
+
node.type === 'TaggedTemplateExpression' ||
|
|
4003
|
+
node.type === 'UpdateExpression'
|
|
4004
|
+
) {
|
|
4005
|
+
opaque = true;
|
|
4006
|
+
return;
|
|
4007
|
+
}
|
|
4008
|
+
|
|
4009
|
+
for (const key in node) {
|
|
4010
|
+
if (AST_WALK_SKIP_KEYS.has(key)) continue;
|
|
4011
|
+
walk(node[key]);
|
|
4012
|
+
}
|
|
4013
|
+
}
|
|
4014
|
+
|
|
4015
|
+
walk(statements);
|
|
4016
|
+
walk(component.body.render);
|
|
4017
|
+
info.warmPotential = opaque;
|
|
4018
|
+
info.warmDependencies = dependencies;
|
|
4019
|
+
}
|
|
4020
|
+
|
|
4021
|
+
// Propagate async reachability through forward references and recursive
|
|
4022
|
+
// same-module chains. An all-synchronous cycle stays false; one opaque or
|
|
4023
|
+
// async descendant makes every component that can reach it conservative.
|
|
4024
|
+
let changed = true;
|
|
4025
|
+
while (changed) {
|
|
4026
|
+
changed = false;
|
|
4027
|
+
for (const [, info] of ctx.componentInfo) {
|
|
4028
|
+
if (info.warmPotential) continue;
|
|
4029
|
+
for (const name of info.warmDependencies) {
|
|
4030
|
+
if (ctx.componentInfo.get(name)?.warmPotential !== false) {
|
|
4031
|
+
info.warmPotential = true;
|
|
4032
|
+
changed = true;
|
|
4033
|
+
break;
|
|
4034
|
+
}
|
|
4035
|
+
}
|
|
4036
|
+
}
|
|
4037
|
+
}
|
|
4038
|
+
}
|
|
4039
|
+
|
|
4040
|
+
// Omitting a JSX descriptor also omits its live `Component.defaultProps` read.
|
|
4041
|
+
// Only private declarations whose every reference is an immediately rendered,
|
|
4042
|
+
// attribute-free host child can therefore bypass descriptor construction. JSX
|
|
4043
|
+
// descriptors returned/stored/passed elsewhere expose their `.type`, so those
|
|
4044
|
+
// sites count as escapes even though their tag looks equally static.
|
|
4045
|
+
function collectPrivateUnescapedComponents(body, ctx) {
|
|
4046
|
+
const candidates = new Set();
|
|
4047
|
+
for (const statement of body) {
|
|
4048
|
+
if (
|
|
4049
|
+
statement.type === 'FunctionDeclaration' &&
|
|
4050
|
+
statement.id?.type === 'Identifier' &&
|
|
4051
|
+
ctx.componentInfo.get(statement.id.name)?.returnJsx === true &&
|
|
4052
|
+
ctx.moduleFunctionDeclarations.has(statement.id.name)
|
|
4053
|
+
) {
|
|
4054
|
+
candidates.add(statement.id.name);
|
|
4055
|
+
}
|
|
4056
|
+
}
|
|
4057
|
+
if (candidates.size === 0) return candidates;
|
|
4058
|
+
|
|
4059
|
+
const allowed = new Set();
|
|
4060
|
+
const visitHost = (host) => {
|
|
4061
|
+
for (const child of host.children || []) {
|
|
4062
|
+
if (child?.type !== 'Element' && child?.type !== 'JSXElement') continue;
|
|
4063
|
+
if (!isComponentTag(child)) {
|
|
4064
|
+
visitHost(child);
|
|
4065
|
+
continue;
|
|
4066
|
+
}
|
|
4067
|
+
const name = tagBindingName(child);
|
|
4068
|
+
const attrs = child.attributes || child.openingElement?.attributes || [];
|
|
4069
|
+
if (name !== null && candidates.has(name) && attrs.length === 0 && !child.children?.length) {
|
|
4070
|
+
allowed.add(child);
|
|
4071
|
+
}
|
|
4072
|
+
}
|
|
4073
|
+
};
|
|
4074
|
+
for (const info of ctx.componentInfo.values()) {
|
|
4075
|
+
if (!info.returnJsx) continue;
|
|
4076
|
+
for (const statement of info.node.body.body || []) {
|
|
4077
|
+
if (statement.type === 'ReturnStatement' && isPlainHostRoot(statement.argument)) {
|
|
4078
|
+
visitHost(statement.argument);
|
|
4079
|
+
}
|
|
4080
|
+
}
|
|
4081
|
+
}
|
|
4082
|
+
|
|
4083
|
+
// An array deliberately does not introduce a synthetic module Block scope:
|
|
4084
|
+
// sibling function references must remain free for the escape proof.
|
|
4085
|
+
const escaped = collectFreeIdentifiers(body, [], allowed);
|
|
4086
|
+
for (const name of candidates) {
|
|
4087
|
+
if (escaped.has(name)) {
|
|
4088
|
+
candidates.delete(name);
|
|
4089
|
+
continue;
|
|
4090
|
+
}
|
|
4091
|
+
// A declaration binds its own name inside its body. Scan that disjoint
|
|
4092
|
+
// subtree once more so self-assigned properties cannot hide behind it.
|
|
4093
|
+
const declaration = ctx.moduleFunctionDeclarations.get(name);
|
|
4094
|
+
if (collectFreeIdentifiers(declaration.body, [], allowed).has(name)) candidates.delete(name);
|
|
4095
|
+
}
|
|
4096
|
+
return candidates;
|
|
4097
|
+
}
|
|
4098
|
+
|
|
3719
4099
|
// Conservative semantic boundary for compiler-owned component-region memoization.
|
|
3720
4100
|
// The cached region assumes React Compiler's pure-render / immutable-snapshot
|
|
3721
4101
|
// contract, but still fails closed for constructs whose commit or retry behavior
|
|
@@ -4370,6 +4750,111 @@ function isSsrMarkerlessForItem(node) {
|
|
|
4370
4750
|
return jsxChildren.length === 1 && isPlainHostRoot(jsxChildren[0]);
|
|
4371
4751
|
}
|
|
4372
4752
|
|
|
4753
|
+
// Direct host-row mounting skips component-render bookkeeping. Keep the proof
|
|
4754
|
+
// narrower than markerless item rendering so opaque host lifecycles fail closed.
|
|
4755
|
+
const HOST_MOUNT_SAFE_TAGS = new Set([
|
|
4756
|
+
'a',
|
|
4757
|
+
'article',
|
|
4758
|
+
'aside',
|
|
4759
|
+
'b',
|
|
4760
|
+
'br',
|
|
4761
|
+
'div',
|
|
4762
|
+
'em',
|
|
4763
|
+
'footer',
|
|
4764
|
+
'h1',
|
|
4765
|
+
'h2',
|
|
4766
|
+
'h3',
|
|
4767
|
+
'h4',
|
|
4768
|
+
'h5',
|
|
4769
|
+
'h6',
|
|
4770
|
+
'header',
|
|
4771
|
+
'hr',
|
|
4772
|
+
'i',
|
|
4773
|
+
'li',
|
|
4774
|
+
'main',
|
|
4775
|
+
'nav',
|
|
4776
|
+
'ol',
|
|
4777
|
+
'p',
|
|
4778
|
+
'section',
|
|
4779
|
+
'small',
|
|
4780
|
+
'span',
|
|
4781
|
+
'strong',
|
|
4782
|
+
'table',
|
|
4783
|
+
'tbody',
|
|
4784
|
+
'td',
|
|
4785
|
+
'tfoot',
|
|
4786
|
+
'th',
|
|
4787
|
+
'thead',
|
|
4788
|
+
'tr',
|
|
4789
|
+
'ul',
|
|
4790
|
+
]);
|
|
4791
|
+
|
|
4792
|
+
const HOST_MOUNT_UNSAFE_ATTRIBUTES = new Set([
|
|
4793
|
+
'autofocus',
|
|
4794
|
+
'checked',
|
|
4795
|
+
'dangerouslysetinnerhtml',
|
|
4796
|
+
'defaultchecked',
|
|
4797
|
+
'defaultvalue',
|
|
4798
|
+
'form',
|
|
4799
|
+
'formaction',
|
|
4800
|
+
'formenctype',
|
|
4801
|
+
'formmethod',
|
|
4802
|
+
'formnovalidate',
|
|
4803
|
+
'formtarget',
|
|
4804
|
+
'innerhtml',
|
|
4805
|
+
'is',
|
|
4806
|
+
'multiple',
|
|
4807
|
+
'ref',
|
|
4808
|
+
'selected',
|
|
4809
|
+
'slot',
|
|
4810
|
+
'src',
|
|
4811
|
+
'srcset',
|
|
4812
|
+
'style',
|
|
4813
|
+
'value',
|
|
4814
|
+
]);
|
|
4815
|
+
|
|
4816
|
+
function isHostMountSafeTree(root) {
|
|
4817
|
+
const seen = new WeakSet();
|
|
4818
|
+
function walk(node) {
|
|
4819
|
+
if (node == null || typeof node !== 'object') return true;
|
|
4820
|
+
if (Array.isArray(node)) return node.every(walk);
|
|
4821
|
+
if (seen.has(node)) return true;
|
|
4822
|
+
seen.add(node);
|
|
4823
|
+
const type = node.type;
|
|
4824
|
+
if (
|
|
4825
|
+
type === 'ArrowFunctionExpression' ||
|
|
4826
|
+
type === 'FunctionExpression' ||
|
|
4827
|
+
type === 'FunctionDeclaration'
|
|
4828
|
+
) {
|
|
4829
|
+
// Delegated event callbacks run after the live row has mounted.
|
|
4830
|
+
return true;
|
|
4831
|
+
}
|
|
4832
|
+
if (type === 'JSXFragment' || type === 'Fragment') return false;
|
|
4833
|
+
if (type === 'Element' || type === 'JSXElement') {
|
|
4834
|
+
const tag = node.id || node.openingElement?.name;
|
|
4835
|
+
if (
|
|
4836
|
+
(tag?.type !== 'Identifier' && tag?.type !== 'JSXIdentifier') ||
|
|
4837
|
+
!HOST_MOUNT_SAFE_TAGS.has(tag.name)
|
|
4838
|
+
) {
|
|
4839
|
+
return false;
|
|
4840
|
+
}
|
|
4841
|
+
for (const attribute of node.attributes || node.openingElement?.attributes || []) {
|
|
4842
|
+
if (attribute.type !== 'Attribute' && attribute.type !== 'JSXAttribute') return false;
|
|
4843
|
+
const name = jsxAttrRawName(attribute);
|
|
4844
|
+
if (typeof name !== 'string' || HOST_MOUNT_UNSAFE_ATTRIBUTES.has(name.toLowerCase())) {
|
|
4845
|
+
return false;
|
|
4846
|
+
}
|
|
4847
|
+
}
|
|
4848
|
+
}
|
|
4849
|
+
for (const key in node) {
|
|
4850
|
+
if (AST_WALK_SKIP_KEYS.has(key)) continue;
|
|
4851
|
+
if (!walk(node[key])) return false;
|
|
4852
|
+
}
|
|
4853
|
+
return true;
|
|
4854
|
+
}
|
|
4855
|
+
return walk(root);
|
|
4856
|
+
}
|
|
4857
|
+
|
|
4373
4858
|
/**
|
|
4374
4859
|
* Whether a function can return from its own body before reaching its compiled
|
|
4375
4860
|
* output. Nested functions are separate execution scopes and do not affect the
|
|
@@ -6162,15 +6647,22 @@ function compileInternal(source, filename, options, analyzedAst, mode, bundlerMe
|
|
|
6162
6647
|
exactOrigins: new Map(), // see registerExactOrigin
|
|
6163
6648
|
originAliases: [], // see registerOriginAlias
|
|
6164
6649
|
hoistedTemplates: [], // { name, ast, html, ns, frag, origins }
|
|
6650
|
+
internedTemplates: new Map(), // HTML -> one template or namespace/fragment variants
|
|
6165
6651
|
hoistedHelpers: [], // statement NODES (sub-components, hook Symbols, key fns) + hook-slot-base markers
|
|
6166
6652
|
delegatedEvents: new Set(), // bubble event names seen in JSX — auto-emits delegateEvents(...)
|
|
6167
6653
|
capturedEvents: new Set(), // capture-phase event names (onXxxCapture) — auto-emits delegateCaptureEvents(...)
|
|
6654
|
+
unownedDelegatedEvents: new Set(),
|
|
6655
|
+
unownedCapturedEvents: new Set(),
|
|
6168
6656
|
cssInjections: [], // { hash, css } — one entry per component with a <style> block
|
|
6657
|
+
ownedCssInjections: new Set(),
|
|
6658
|
+
componentOwners: [],
|
|
6659
|
+
currentComponentOwner: null,
|
|
6169
6660
|
currentComponentLocals: null, // Set<string> while compiling a component body; null otherwise
|
|
6170
6661
|
currentMapTemps: null, // receiver/method temps owned by the current emitted function
|
|
6171
6662
|
currentAutoMemoOffset: 0, // flat compiler-cache cell offset for the body being emitted
|
|
6172
6663
|
currentAutoMemoCacheName: null, // collision-free local bound to the body's cache array
|
|
6173
6664
|
currentAutoMemoCommittedName: null, // committed cache snapshot (copy-on-write source)
|
|
6665
|
+
currentAutoCalculatedRenderableRefs: null, // proven non-escaping calculation holes, inherited by lexical child bodies
|
|
6174
6666
|
nextAutoMemoCacheId: 0, // unique non-index slots property per compiled render function
|
|
6175
6667
|
inlineHookMemo: inlineHookMemoEnabled, // de-callbacked useMemo/useCallback + pu creations
|
|
6176
6668
|
_puInlineLowering: false, // true only while a body pipeline ends in inlineHookMemoPass
|
|
@@ -6375,8 +6867,54 @@ function compileInternal(source, filename, options, analyzedAst, mode, bundlerMe
|
|
|
6375
6867
|
ctx.moduleFunctionDeclarations = collectImmutableModuleFunctions(ast.body);
|
|
6376
6868
|
// M3 inherit-range exclusion set (see inheritSoleCompRoot).
|
|
6377
6869
|
ctx._octaneBoundaryNames = collectOctaneBoundaryNames(ast.body);
|
|
6378
|
-
|
|
6379
|
-
|
|
6870
|
+
const productionEffects = !ctx.hmr && !ctx.dev && !ctx.profile;
|
|
6871
|
+
const exportedComponents = ast.body.filter(
|
|
6872
|
+
(statement) =>
|
|
6873
|
+
(statement.type === 'ExportNamedDeclaration' ||
|
|
6874
|
+
statement.type === 'ExportDefaultDeclaration') &&
|
|
6875
|
+
isComponentFunction(statement.declaration),
|
|
6876
|
+
);
|
|
6877
|
+
// Prelude registration historically precedes every authored module effect.
|
|
6878
|
+
// An effect above any component could observe its stylesheet, delegated
|
|
6879
|
+
// listeners, or transition capability, so retain the entire module prelude.
|
|
6880
|
+
let effectBeforeComponent = false;
|
|
6881
|
+
let precedingModuleEffect = false;
|
|
6882
|
+
if (productionEffects && exportedComponents.length > 1) {
|
|
6883
|
+
for (const statement of ast.body) {
|
|
6884
|
+
const declaration =
|
|
6885
|
+
statement.type === 'ExportNamedDeclaration' || statement.type === 'ExportDefaultDeclaration'
|
|
6886
|
+
? statement.declaration
|
|
6887
|
+
: statement;
|
|
6888
|
+
if (isComponentFunction(declaration)) {
|
|
6889
|
+
if (precedingModuleEffect) {
|
|
6890
|
+
effectBeforeComponent = true;
|
|
6891
|
+
break;
|
|
6892
|
+
}
|
|
6893
|
+
continue;
|
|
6894
|
+
}
|
|
6895
|
+
if (
|
|
6896
|
+
statement.type === 'ImportDeclaration' ||
|
|
6897
|
+
statement.type === 'ExportAllDeclaration' ||
|
|
6898
|
+
(statement.type === 'ExportNamedDeclaration' && declaration == null) ||
|
|
6899
|
+
declaration?.type === 'FunctionDeclaration' ||
|
|
6900
|
+
declaration?.type === 'TSInterfaceDeclaration' ||
|
|
6901
|
+
declaration?.type === 'TSTypeAliasDeclaration' ||
|
|
6902
|
+
statement.type === 'EmptyStatement'
|
|
6903
|
+
) {
|
|
6904
|
+
continue;
|
|
6905
|
+
}
|
|
6906
|
+
precedingModuleEffect = true;
|
|
6907
|
+
}
|
|
6908
|
+
}
|
|
6909
|
+
ctx.componentEffectOwnership =
|
|
6910
|
+
productionEffects && exportedComponents.length > 1 && !effectBeforeComponent;
|
|
6911
|
+
const viewTransitions = classifyViewTransitionOwnership(
|
|
6912
|
+
ast.body,
|
|
6913
|
+
productionEffects,
|
|
6914
|
+
ctx.componentEffectOwnership,
|
|
6915
|
+
);
|
|
6916
|
+
ctx._usesViewTransition = viewTransitions.global;
|
|
6917
|
+
ctx.viewTransitionOwners = viewTransitions.owners;
|
|
6380
6918
|
|
|
6381
6919
|
// List of exported components needing HMR wrapping. Each entry: { name,
|
|
6382
6920
|
// exportKind: 'default' | 'named' }. We emit the `Comp = hmr(Comp)` lines
|
|
@@ -6425,6 +6963,19 @@ function compileInternal(source, filename, options, analyzedAst, mode, bundlerMe
|
|
|
6425
6963
|
});
|
|
6426
6964
|
}
|
|
6427
6965
|
}
|
|
6966
|
+
// Return-based JSX functions never attach fetch-tree warm plans, and a sole
|
|
6967
|
+
// same-module component already rejects a self-only warm edge below. Avoid a
|
|
6968
|
+
// whole-module graph walk unless a compiled-void body can actually reach a
|
|
6969
|
+
// distinct same-module declaration.
|
|
6970
|
+
if (
|
|
6971
|
+
ctx.componentInfo.size > 1 &&
|
|
6972
|
+
[...ctx.componentInfo.values()].some((info) => info.node.body?.type === 'JSXCodeBlock')
|
|
6973
|
+
) {
|
|
6974
|
+
classifySameModuleWarmPotential(ctx);
|
|
6975
|
+
}
|
|
6976
|
+
ctx.privateUnescapedComponents = ctx.autoMemo
|
|
6977
|
+
? collectPrivateUnescapedComponents(ast.body, ctx)
|
|
6978
|
+
: new Set();
|
|
6428
6979
|
for (const [, info] of ctx.componentInfo) {
|
|
6429
6980
|
const compNode = info.node;
|
|
6430
6981
|
const locals = collectComponentLocals(compNode);
|
|
@@ -6736,6 +7287,8 @@ function compileInternal(source, filename, options, analyzedAst, mode, bundlerMe
|
|
|
6736
7287
|
}
|
|
6737
7288
|
}
|
|
6738
7289
|
|
|
7290
|
+
finalizeComponentInitializers(ctx, bodyNodes);
|
|
7291
|
+
|
|
6739
7292
|
// Auto-emit delegateEvents([...]) / delegateCaptureEvents([...]) once at module
|
|
6740
7293
|
// scope for every (bubble / capture) event seen.
|
|
6741
7294
|
if (ctx.delegatedEvents.size > 0) {
|
|
@@ -6777,22 +7330,24 @@ function compileInternal(source, filename, options, analyzedAst, mode, bundlerMe
|
|
|
6777
7330
|
),
|
|
6778
7331
|
);
|
|
6779
7332
|
}
|
|
6780
|
-
const styleNodes = ctx.cssInjections
|
|
6781
|
-
|
|
6782
|
-
|
|
6783
|
-
|
|
6784
|
-
|
|
6785
|
-
|
|
6786
|
-
|
|
6787
|
-
|
|
6788
|
-
|
|
6789
|
-
b.
|
|
6790
|
-
|
|
7333
|
+
const styleNodes = ctx.cssInjections
|
|
7334
|
+
.filter((i) => !ctx.ownedCssInjections.has(i))
|
|
7335
|
+
.map((i) => {
|
|
7336
|
+
// Point the authored `<style>` block at the CSS this injection carries.
|
|
7337
|
+
// Both sides are whole units — the block and the stylesheet it became —
|
|
7338
|
+
// which is the useful pairing for a scoped style.
|
|
7339
|
+
const anchor = claimCssOrigins(ctx, i);
|
|
7340
|
+
return inheritOriginLoc(
|
|
7341
|
+
b.stmt(
|
|
7342
|
+
b.call(
|
|
7343
|
+
'_$injectStyle',
|
|
7344
|
+
b.literal(i.hash, JSON.stringify(i.hash)),
|
|
7345
|
+
b.literal(i.css, JSON.stringify(i.css)),
|
|
7346
|
+
),
|
|
6791
7347
|
),
|
|
6792
|
-
|
|
6793
|
-
|
|
6794
|
-
);
|
|
6795
|
-
});
|
|
7348
|
+
anchor ?? moduleOrigin,
|
|
7349
|
+
);
|
|
7350
|
+
});
|
|
6796
7351
|
const templateNodes = ctx.hoistedTemplates.map((t) => {
|
|
6797
7352
|
const args = [b.literal(t.html, JSON.stringify(t.html))];
|
|
6798
7353
|
if (t.ns || t.frag) args.push(b.literal(t.ns | 0));
|
|
@@ -10152,6 +10707,103 @@ function singleRootInitializer(ctx, component) {
|
|
|
10152
10707
|
return markPure(b.call('_$__s', component));
|
|
10153
10708
|
}
|
|
10154
10709
|
|
|
10710
|
+
function finalizeComponentInitializers(ctx, bodyNodes) {
|
|
10711
|
+
if (!ctx.componentEffectOwnership || ctx.componentOwners.length === 0) return;
|
|
10712
|
+
|
|
10713
|
+
const delegatedOwners = new Map();
|
|
10714
|
+
const capturedOwners = new Map();
|
|
10715
|
+
const ownableStyles = new Set();
|
|
10716
|
+
for (const owner of ctx.componentOwners) {
|
|
10717
|
+
for (const event of owner.delegatedEvents) {
|
|
10718
|
+
delegatedOwners.set(event, (delegatedOwners.get(event) ?? 0) + 1);
|
|
10719
|
+
}
|
|
10720
|
+
for (const event of owner.capturedEvents) {
|
|
10721
|
+
capturedOwners.set(event, (capturedOwners.get(event) ?? 0) + 1);
|
|
10722
|
+
}
|
|
10723
|
+
for (const style of owner.styles) ownableStyles.add(style);
|
|
10724
|
+
}
|
|
10725
|
+
// The prelude emitted all stylesheets in authored order. Moving only owned
|
|
10726
|
+
// sheets behind an unowned style map or local component reverses that cascade.
|
|
10727
|
+
const preserveModuleStyleOrder = ctx.cssInjections.some((style) => !ownableStyles.has(style));
|
|
10728
|
+
|
|
10729
|
+
const replacements = new Map();
|
|
10730
|
+
for (const owner of ctx.componentOwners) {
|
|
10731
|
+
const calls = [];
|
|
10732
|
+
if (owner.transition && !ctx._usesViewTransition) {
|
|
10733
|
+
ctx.runtimeNeeded.add('__vtSeen');
|
|
10734
|
+
calls.push(b.call(rtAlias('__vtSeen')));
|
|
10735
|
+
}
|
|
10736
|
+
|
|
10737
|
+
const delegated = [...owner.delegatedEvents]
|
|
10738
|
+
.filter((event) => delegatedOwners.get(event) === 1 && !ctx.unownedDelegatedEvents.has(event))
|
|
10739
|
+
.sort();
|
|
10740
|
+
if (delegated.length !== 0) {
|
|
10741
|
+
for (const event of delegated) ctx.delegatedEvents.delete(event);
|
|
10742
|
+
ctx.runtimeNeeded.add('delegateEvents');
|
|
10743
|
+
calls.push(
|
|
10744
|
+
b.call(
|
|
10745
|
+
'_$delegateEvents',
|
|
10746
|
+
b.array(delegated.map((event) => b.literal(event, JSON.stringify(event)))),
|
|
10747
|
+
),
|
|
10748
|
+
);
|
|
10749
|
+
}
|
|
10750
|
+
|
|
10751
|
+
const captured = [...owner.capturedEvents]
|
|
10752
|
+
.filter((event) => capturedOwners.get(event) === 1 && !ctx.unownedCapturedEvents.has(event))
|
|
10753
|
+
.sort();
|
|
10754
|
+
if (captured.length !== 0) {
|
|
10755
|
+
for (const event of captured) ctx.capturedEvents.delete(event);
|
|
10756
|
+
ctx.runtimeNeeded.add('delegateCaptureEvents');
|
|
10757
|
+
calls.push(
|
|
10758
|
+
b.call(
|
|
10759
|
+
'_$delegateCaptureEvents',
|
|
10760
|
+
b.array(captured.map((event) => b.literal(event, JSON.stringify(event)))),
|
|
10761
|
+
),
|
|
10762
|
+
);
|
|
10763
|
+
}
|
|
10764
|
+
|
|
10765
|
+
if (!preserveModuleStyleOrder) {
|
|
10766
|
+
for (const style of owner.styles) {
|
|
10767
|
+
ctx.ownedCssInjections.add(style);
|
|
10768
|
+
const origin = claimCssOrigins(ctx, style);
|
|
10769
|
+
calls.push(
|
|
10770
|
+
inheritOriginLoc(
|
|
10771
|
+
b.call(
|
|
10772
|
+
'_$injectStyle',
|
|
10773
|
+
b.literal(style.hash, JSON.stringify(style.hash)),
|
|
10774
|
+
b.literal(style.css, JSON.stringify(style.css)),
|
|
10775
|
+
),
|
|
10776
|
+
origin ?? owner.origin,
|
|
10777
|
+
),
|
|
10778
|
+
);
|
|
10779
|
+
}
|
|
10780
|
+
}
|
|
10781
|
+
if (calls.length === 0) continue;
|
|
10782
|
+
|
|
10783
|
+
const declarator = owner.declaration.declarations[0];
|
|
10784
|
+
const initializer = inheritOriginLoc(
|
|
10785
|
+
markPure(b.call(b.arrow([], b.sequence([...calls, declarator.init])))),
|
|
10786
|
+
owner.origin,
|
|
10787
|
+
);
|
|
10788
|
+
replacements.set(owner.declaration, {
|
|
10789
|
+
...owner.declaration,
|
|
10790
|
+
declarations: [{ ...declarator, init: initializer }],
|
|
10791
|
+
});
|
|
10792
|
+
}
|
|
10793
|
+
|
|
10794
|
+
if (replacements.size === 0) return;
|
|
10795
|
+
for (let index = 0; index < bodyNodes.length; index++) {
|
|
10796
|
+
const statement = bodyNodes[index];
|
|
10797
|
+
const replacement = replacements.get(statement);
|
|
10798
|
+
if (replacement !== undefined) {
|
|
10799
|
+
bodyNodes[index] = replacement;
|
|
10800
|
+
} else if (statement.type === 'ExportNamedDeclaration') {
|
|
10801
|
+
const declaration = replacements.get(statement.declaration);
|
|
10802
|
+
if (declaration !== undefined) bodyNodes[index] = { ...statement, declaration };
|
|
10803
|
+
}
|
|
10804
|
+
}
|
|
10805
|
+
}
|
|
10806
|
+
|
|
10155
10807
|
function compileComponent(node, ctx, options) {
|
|
10156
10808
|
const name = node.id.name;
|
|
10157
10809
|
rejectAsyncOrGenerator(node, name);
|
|
@@ -10160,6 +10812,19 @@ function compileComponent(node, ctx, options) {
|
|
|
10160
10812
|
const isDefault = !!node.default;
|
|
10161
10813
|
const hmrWrap = !!(options && options.hmrWrap);
|
|
10162
10814
|
const returnedOutput = node.body?.type === 'JSXCodeBlock' && hasOwnValueReturn(node);
|
|
10815
|
+
const owner =
|
|
10816
|
+
ctx.componentEffectOwnership && isExported
|
|
10817
|
+
? {
|
|
10818
|
+
name,
|
|
10819
|
+
origin: node,
|
|
10820
|
+
delegatedEvents: new Set(),
|
|
10821
|
+
capturedEvents: new Set(),
|
|
10822
|
+
styles: [],
|
|
10823
|
+
transition: ctx.viewTransitionOwners.has(name),
|
|
10824
|
+
declaration: null,
|
|
10825
|
+
}
|
|
10826
|
+
: null;
|
|
10827
|
+
const firstStyle = ctx.cssInjections.length;
|
|
10163
10828
|
|
|
10164
10829
|
// Scoped `<style>` block. New TSRX surfaces each style block as a
|
|
10165
10830
|
// `JSXStyleElement` child of the rendered tree (parser pre-computes the
|
|
@@ -10191,9 +10856,11 @@ function compileComponent(node, ctx, options) {
|
|
|
10191
10856
|
const prevAutoMemoCallsitesSafe = ctx.currentAutoMemoCallsitesSafe;
|
|
10192
10857
|
const prevKnownStr = ctx.knownStringLocals;
|
|
10193
10858
|
const prevProfileComponentId = ctx.currentProfileComponentId;
|
|
10859
|
+
const previousComponentOwner = ctx.currentComponentOwner;
|
|
10194
10860
|
ctx.currentComponentLocals = collectComponentLocals(node);
|
|
10195
10861
|
ctx.currentAutoMemoCallsitesSafe = ctx.componentInfo.get(name)?.autoMemoCallsitesSafe !== false;
|
|
10196
10862
|
ctx.knownStringLocals = collectKnownStringLocals(node);
|
|
10863
|
+
ctx.currentComponentOwner = owner;
|
|
10197
10864
|
if (ctx.profile) ctx.currentProfileComponentId = profileComponentId(ctx, name, node);
|
|
10198
10865
|
let fnNode;
|
|
10199
10866
|
try {
|
|
@@ -10211,7 +10878,9 @@ function compileComponent(node, ctx, options) {
|
|
|
10211
10878
|
ctx.currentAutoMemoCallsitesSafe = prevAutoMemoCallsitesSafe;
|
|
10212
10879
|
ctx.knownStringLocals = prevKnownStr;
|
|
10213
10880
|
ctx.currentProfileComponentId = prevProfileComponentId;
|
|
10881
|
+
ctx.currentComponentOwner = previousComponentOwner;
|
|
10214
10882
|
}
|
|
10883
|
+
if (owner !== null) owner.styles = ctx.cssInjections.slice(firstStyle);
|
|
10215
10884
|
|
|
10216
10885
|
// Parallel-use warm plan: attached to the INNER function object (not the
|
|
10217
10886
|
// module const) so the component's own body — where the function-
|
|
@@ -10252,6 +10921,11 @@ function compileComponent(node, ctx, options) {
|
|
|
10252
10921
|
sourceBeforeNode !== '' &&
|
|
10253
10922
|
new RegExp(`\\b${name.replace(/\$/g, '\\$')}\\b`).test(sourceBeforeNode);
|
|
10254
10923
|
if (referencedAboveDeclaration) {
|
|
10924
|
+
if (owner !== null) {
|
|
10925
|
+
for (const event of owner.delegatedEvents) ctx.unownedDelegatedEvents.add(event);
|
|
10926
|
+
for (const event of owner.capturedEvents) ctx.unownedCapturedEvents.add(event);
|
|
10927
|
+
if (owner.transition) ctx._usesViewTransition = true;
|
|
10928
|
+
}
|
|
10255
10929
|
// Every stamp is `typeof`-guarded: route code-splitters (TanStack's) may
|
|
10256
10930
|
// EXTRACT the declaration into its own module and leave these statements
|
|
10257
10931
|
// behind — `typeof` on the then-undeclared identifier short-circuits
|
|
@@ -10341,6 +11015,10 @@ function compileComponent(node, ctx, options) {
|
|
|
10341
11015
|
b.declaration(declKind, [b.declarator(b.id(name, node.id ?? node), valueExpr)]),
|
|
10342
11016
|
node,
|
|
10343
11017
|
);
|
|
11018
|
+
if (owner !== null) {
|
|
11019
|
+
owner.declaration = declNode;
|
|
11020
|
+
ctx.componentOwners.push(owner);
|
|
11021
|
+
}
|
|
10344
11022
|
if (isDefault) {
|
|
10345
11023
|
if (options && options.hmrMutable) {
|
|
10346
11024
|
return {
|
|
@@ -10371,6 +11049,8 @@ function compileComponent(node, ctx, options) {
|
|
|
10371
11049
|
*/
|
|
10372
11050
|
function compileFunctionBody(node, ctx, name, parentNs = 'html', cssHash = null, options = null) {
|
|
10373
11051
|
const returnedOutput = options?.returnedOutput === true;
|
|
11052
|
+
const previousAutoCalculatedRenderableRefs = ctx.currentAutoCalculatedRenderableRefs;
|
|
11053
|
+
let autoCalculatedDeclarations = null;
|
|
10374
11054
|
const prevMapTemps = ctx.currentMapTemps;
|
|
10375
11055
|
const mapTemps = [];
|
|
10376
11056
|
ctx.currentMapTemps = mapTemps;
|
|
@@ -10444,7 +11124,8 @@ function compileFunctionBody(node, ctx, name, parentNs = 'html', cssHash = null,
|
|
|
10444
11124
|
);
|
|
10445
11125
|
}
|
|
10446
11126
|
workingStatements = removeMountEventCallbackDeclarations(statements, mountCallbackSinks).map(
|
|
10447
|
-
(s) =>
|
|
11127
|
+
(s) =>
|
|
11128
|
+
rewriteAutoCallback(s, stableSet, ctx.currentComponentLocals, ctx, bodyInvariantLocals),
|
|
10448
11129
|
);
|
|
10449
11130
|
}
|
|
10450
11131
|
if (returnedOutput) {
|
|
@@ -10496,9 +11177,21 @@ function compileFunctionBody(node, ctx, name, parentNs = 'html', cssHash = null,
|
|
|
10496
11177
|
if (ctx.currentComponentLocals && ctx.mode !== 'server') {
|
|
10497
11178
|
const renderReadNames = collectRenderReadNames(jsxNodes, ctx);
|
|
10498
11179
|
if (renderReadNames.size > 0) {
|
|
10499
|
-
workingStatements = workingStatements.map((statement) =>
|
|
10500
|
-
|
|
10501
|
-
|
|
11180
|
+
workingStatements = workingStatements.map((statement) => {
|
|
11181
|
+
const rewritten = rewriteAutoCalculation(
|
|
11182
|
+
statement,
|
|
11183
|
+
ctx.currentComponentLocals,
|
|
11184
|
+
renderReadNames,
|
|
11185
|
+
ctx,
|
|
11186
|
+
);
|
|
11187
|
+
if (ctx.autoMemo && rewritten !== statement) {
|
|
11188
|
+
(autoCalculatedDeclarations ??= new Map()).set(
|
|
11189
|
+
statement.declarations[0].id.name,
|
|
11190
|
+
statement,
|
|
11191
|
+
);
|
|
11192
|
+
}
|
|
11193
|
+
return rewritten;
|
|
11194
|
+
});
|
|
10502
11195
|
}
|
|
10503
11196
|
}
|
|
10504
11197
|
workingStatements = parallelUseMemoizePass(workingStatements, ctx, name, creations, [], null);
|
|
@@ -10607,6 +11300,19 @@ function compileFunctionBody(node, ctx, name, parentNs = 'html', cssHash = null,
|
|
|
10607
11300
|
// node itself always does.
|
|
10608
11301
|
const prevFnOrigin = ctx._fnOrigin;
|
|
10609
11302
|
ctx._fnOrigin = node.loc ? node : (node.id ?? prevFnOrigin);
|
|
11303
|
+
if (autoCalculatedDeclarations !== null) {
|
|
11304
|
+
const refs = collectAutoCalculatedRenderableRefs(
|
|
11305
|
+
statements,
|
|
11306
|
+
jsxNodes,
|
|
11307
|
+
autoCalculatedDeclarations,
|
|
11308
|
+
);
|
|
11309
|
+
if (refs !== null) {
|
|
11310
|
+
ctx.currentAutoCalculatedRenderableRefs = {
|
|
11311
|
+
nodes: refs,
|
|
11312
|
+
parent: previousAutoCalculatedRenderableRefs,
|
|
11313
|
+
};
|
|
11314
|
+
}
|
|
11315
|
+
}
|
|
10610
11316
|
// Located origin for the function SHELL itself: synthetic helper shapes
|
|
10611
11317
|
// (hoisted @for bodies, `__tsrx$N` sub-templates) carry no loc of their
|
|
10612
11318
|
// own — their scaffolding inherits the nearest located enclosing origin.
|
|
@@ -10639,6 +11345,7 @@ function compileFunctionBody(node, ctx, name, parentNs = 'html', cssHash = null,
|
|
|
10639
11345
|
ctx.currentBodyIsComponentScope = prevBodyIsComponentScope;
|
|
10640
11346
|
ctx._inheritBody = prevInheritBody;
|
|
10641
11347
|
ctx._fnOrigin = prevFnOrigin;
|
|
11348
|
+
ctx.currentAutoCalculatedRenderableRefs = previousAutoCalculatedRenderableRefs;
|
|
10642
11349
|
ctx._foldedDirectiveCalls = prevFDC;
|
|
10643
11350
|
ctx._valueDirectiveLowering = prevValueDirectiveLowering;
|
|
10644
11351
|
|
|
@@ -12390,6 +13097,12 @@ function buildWarmArtifacts(node, ctx, componentName, creations, warmChildren) {
|
|
|
12390
13097
|
const warmKids = warmChildren.filter(
|
|
12391
13098
|
(w) =>
|
|
12392
13099
|
guardOk(w.guards, w.locals) &&
|
|
13100
|
+
// Server and universal-renderer warm plans have independent compilation
|
|
13101
|
+
// contexts. Only the DOM client's closed same-module graph can prove
|
|
13102
|
+
// that this edge and every descendant are synchronously render-only.
|
|
13103
|
+
(ctx.mode === 'server' ||
|
|
13104
|
+
ctx._universalRuntimeUnit != null ||
|
|
13105
|
+
ctx.componentInfo.get(w.compName)?.warmPotential !== false) &&
|
|
12393
13106
|
!paramNames.has(w.compName) &&
|
|
12394
13107
|
!(locals && locals.has(w.compName)) &&
|
|
12395
13108
|
!(w.locals && w.locals.has(w.compName)) &&
|
|
@@ -13051,7 +13764,17 @@ function authoredHookMemoOf(stmt) {
|
|
|
13051
13764
|
if (containsHookShapedCall(fn.body)) return null;
|
|
13052
13765
|
if (fn.body.type === 'BlockStatement' && !blockBodyInlineSafe(fn.body)) return null;
|
|
13053
13766
|
}
|
|
13054
|
-
return {
|
|
13767
|
+
return {
|
|
13768
|
+
name,
|
|
13769
|
+
decl,
|
|
13770
|
+
kind: stmt.kind,
|
|
13771
|
+
fn,
|
|
13772
|
+
deps,
|
|
13773
|
+
generatedInvariant:
|
|
13774
|
+
name === 'useCallback' &&
|
|
13775
|
+
callee._octaneGenerated === true &&
|
|
13776
|
+
callee._octaneLifetimeInvariant === true,
|
|
13777
|
+
};
|
|
13055
13778
|
}
|
|
13056
13779
|
|
|
13057
13780
|
// Compute statements writing the site's result into `target` (an expression
|
|
@@ -13127,7 +13850,7 @@ function lowerPuMemoDecl(stmt, ctx) {
|
|
|
13127
13850
|
function lowerAuthoredHookMemo(stmt, ctx) {
|
|
13128
13851
|
const entry = authoredHookMemoOf(stmt);
|
|
13129
13852
|
if (entry === null) return null;
|
|
13130
|
-
const { name, decl, kind, fn, deps } = entry;
|
|
13853
|
+
const { name, decl, kind, fn, deps, generatedInvariant } = entry;
|
|
13131
13854
|
// Explicit `null` deps: recompute every render — no cache at all. A
|
|
13132
13855
|
// useCallback degenerates to the factory itself; a useMemo evaluates
|
|
13133
13856
|
// inline (via the shared block machinery when the body is a block).
|
|
@@ -13143,6 +13866,24 @@ function lowerAuthoredHookMemo(stmt, ctx) {
|
|
|
13143
13866
|
}
|
|
13144
13867
|
const names = hookMemoNames(ctx);
|
|
13145
13868
|
const base = ctx.currentHookMemoOffset;
|
|
13869
|
+
if (generatedInvariant) {
|
|
13870
|
+
// A compiler-owned callback whose captures are lifetime-invariant cannot
|
|
13871
|
+
// miss after its first render. Its function value doubles as the init flag,
|
|
13872
|
+
// and publishing it immediately preserves identity across suspension.
|
|
13873
|
+
ctx.currentHookMemoOffset = base + 1;
|
|
13874
|
+
const valueCell = () => b.member(b.id(names.cache), hkNumLit(base), true);
|
|
13875
|
+
return [
|
|
13876
|
+
{
|
|
13877
|
+
...stmt,
|
|
13878
|
+
declarations: [
|
|
13879
|
+
{
|
|
13880
|
+
...decl,
|
|
13881
|
+
init: b.logical('??', valueCell(), b.assignment('=', valueCell(), fn)),
|
|
13882
|
+
},
|
|
13883
|
+
],
|
|
13884
|
+
},
|
|
13885
|
+
];
|
|
13886
|
+
}
|
|
13146
13887
|
const k = deps.length;
|
|
13147
13888
|
ctx.currentHookMemoOffset = base + k + 2;
|
|
13148
13889
|
const cellRef = (i) => b.member(b.id(names.cache), hkNumLit(i), true);
|
|
@@ -13510,11 +14251,32 @@ function compileReturnJsxFunction(node, ctx, options) {
|
|
|
13510
14251
|
ctx.currentMapTemps = mapTemps;
|
|
13511
14252
|
let newStatements;
|
|
13512
14253
|
try {
|
|
13513
|
-
|
|
14254
|
+
const authoredStatements = node.body.body || [];
|
|
14255
|
+
const renderedRoots = authoredStatements
|
|
14256
|
+
.filter((statement) => statement.type === 'ReturnStatement' && isJsxNode(statement.argument))
|
|
14257
|
+
.map((statement) => statement.argument);
|
|
14258
|
+
const renderReadNames = collectRenderReadNames(renderedRoots, ctx);
|
|
14259
|
+
let renderScopeEstablished = false;
|
|
14260
|
+
newStatements = authoredStatements.map((sourceStatement) => {
|
|
14261
|
+
// Return-JSX functions keep their ordinary callable ABI. Introducing a
|
|
14262
|
+
// cache into a hookless function would make an existing direct call
|
|
14263
|
+
// require a render scope, so only declarations following an authored,
|
|
14264
|
+
// unconditional Octane hook may use the existing calculation lowering.
|
|
14265
|
+
const calculated = renderScopeEstablished
|
|
14266
|
+
? rewriteAutoCalculation(sourceStatement, ctx.currentComponentLocals, renderReadNames, ctx)
|
|
14267
|
+
: sourceStatement;
|
|
14268
|
+
if (!renderScopeEstablished && sourceStatement.type === 'VariableDeclaration') {
|
|
14269
|
+
renderScopeEstablished = (sourceStatement.declarations || []).some(
|
|
14270
|
+
(declaration) => stableHookCallName(unwrapTsExpr(declaration.init)) !== null,
|
|
14271
|
+
);
|
|
14272
|
+
} else if (!renderScopeEstablished && sourceStatement.type === 'ExpressionStatement') {
|
|
14273
|
+
renderScopeEstablished =
|
|
14274
|
+
stableHookCallName(unwrapTsExpr(sourceStatement.expression)) !== null;
|
|
14275
|
+
}
|
|
13514
14276
|
// A return-based component's undefined output is ambiguous with the compiled
|
|
13515
14277
|
// void-body signal at runtime. Preserve JSX roots for the specialized lowering
|
|
13516
14278
|
// below, but normalize every other owned return to an explicit empty value.
|
|
13517
|
-
const s = normalizeOwnRenderableReturns(
|
|
14279
|
+
const s = normalizeOwnRenderableReturns(calculated, true);
|
|
13518
14280
|
const prepared =
|
|
13519
14281
|
s.type === 'ReturnStatement' && s.argument && isJsxNode(s.argument)
|
|
13520
14282
|
? s
|
|
@@ -13535,6 +14297,30 @@ function compileReturnJsxFunction(node, ctx, options) {
|
|
|
13535
14297
|
ctx.currentAutoMemoCallsitesSafe = prevAutoMemoCallsitesSafe;
|
|
13536
14298
|
ctx.currentMapTemps = prevMapTemps;
|
|
13537
14299
|
}
|
|
14300
|
+
if (
|
|
14301
|
+
ctx.autoMemo &&
|
|
14302
|
+
ctx.privateUnescapedComponents.has(name) &&
|
|
14303
|
+
(node.params?.length ?? 0) === 0 &&
|
|
14304
|
+
(node.body.body?.length ?? 0) === 1 &&
|
|
14305
|
+
isPlainHostRoot(node.body.body[0]?.argument) &&
|
|
14306
|
+
compInlinedSubs.length === 0 &&
|
|
14307
|
+
mapTemps.length === 0 &&
|
|
14308
|
+
cssHash === null
|
|
14309
|
+
) {
|
|
14310
|
+
const descriptor = newStatements[0]?.argument;
|
|
14311
|
+
const renderer = descriptor?.arguments?.[0];
|
|
14312
|
+
const props = descriptor?.arguments?.[1];
|
|
14313
|
+
if (
|
|
14314
|
+
descriptor?.type === 'CallExpression' &&
|
|
14315
|
+
descriptor.callee?.type === 'Identifier' &&
|
|
14316
|
+
descriptor.callee.name === '_$createElement' &&
|
|
14317
|
+
descriptor.arguments.length === 2 &&
|
|
14318
|
+
renderer?.type === 'Identifier' &&
|
|
14319
|
+
isStaticFragmentRendererProps(props, ctx)
|
|
14320
|
+
) {
|
|
14321
|
+
ctx.componentInfo.get(name).staticFragmentRenderer = { name: renderer.name, props };
|
|
14322
|
+
}
|
|
14323
|
+
}
|
|
13538
14324
|
// The rebuilt function shell maps to the authored declaration. Hoisted
|
|
13539
14325
|
// helper fns (compInlinedSubs — filled by the statement mapping above) are
|
|
13540
14326
|
// function DECLARATION nodes embedded at the top of the body, matching the
|
|
@@ -13714,6 +14500,57 @@ function objectProp(hn, valNode) {
|
|
|
13714
14500
|
return b.prop('init', b.id(hn), valNode);
|
|
13715
14501
|
}
|
|
13716
14502
|
|
|
14503
|
+
// A bare, immutable same-module component needs no descriptor when its JSX is
|
|
14504
|
+
// consumed immediately by a returned host. Keep every value/props boundary on
|
|
14505
|
+
// the ordinary path: only this attribute-free call can remain in the template
|
|
14506
|
+
// without moving authored expression evaluation into its hoisted renderer.
|
|
14507
|
+
function isStaticFragmentRendererProps(props, ctx) {
|
|
14508
|
+
if (props?.type !== 'ObjectExpression') return false;
|
|
14509
|
+
for (const property of props.properties || []) {
|
|
14510
|
+
if (
|
|
14511
|
+
property.type !== 'Property' ||
|
|
14512
|
+
property.kind !== 'init' ||
|
|
14513
|
+
property.computed ||
|
|
14514
|
+
property.method ||
|
|
14515
|
+
property.shorthand
|
|
14516
|
+
) {
|
|
14517
|
+
return false;
|
|
14518
|
+
}
|
|
14519
|
+
const descriptor = property.value;
|
|
14520
|
+
const component = descriptor?.arguments?.[0];
|
|
14521
|
+
const componentProps = descriptor?.arguments?.[1];
|
|
14522
|
+
if (
|
|
14523
|
+
descriptor?.type !== 'CallExpression' ||
|
|
14524
|
+
descriptor.callee?.type !== 'Identifier' ||
|
|
14525
|
+
descriptor.callee.name !== '_$createElement' ||
|
|
14526
|
+
descriptor.arguments.length !== 2 ||
|
|
14527
|
+
component?.type !== 'Identifier' ||
|
|
14528
|
+
!ctx.privateUnescapedComponents.has(component.name) ||
|
|
14529
|
+
componentProps?.type !== 'ObjectExpression' ||
|
|
14530
|
+
componentProps.properties.length !== 0
|
|
14531
|
+
) {
|
|
14532
|
+
return false;
|
|
14533
|
+
}
|
|
14534
|
+
}
|
|
14535
|
+
return true;
|
|
14536
|
+
}
|
|
14537
|
+
|
|
14538
|
+
function isStaticReturnedFragmentComponent(node, ctx) {
|
|
14539
|
+
if (!ctx.autoMemo || ctx._foldCtx?.immediateRenderedOutput !== true) return false;
|
|
14540
|
+
const name = tagBindingName(node);
|
|
14541
|
+
if (
|
|
14542
|
+
name === null ||
|
|
14543
|
+
ctx.currentComponentLocals == null ||
|
|
14544
|
+
ctx.currentComponentLocals.has(name) ||
|
|
14545
|
+
!ctx.privateUnescapedComponents.has(name) ||
|
|
14546
|
+
ctx.componentInfo.get(name)?.staticFragmentRenderer === undefined
|
|
14547
|
+
) {
|
|
14548
|
+
return false;
|
|
14549
|
+
}
|
|
14550
|
+
const attrs = node.attributes || node.openingElement?.attributes || [];
|
|
14551
|
+
return attrs.length === 0 && (node.children || []).length === 0;
|
|
14552
|
+
}
|
|
14553
|
+
|
|
13717
14554
|
// Walk a host element or JSX fragment, replacing each DYNAMIC part (an
|
|
13718
14555
|
// attribute/child expression) with `props.hN` and collecting
|
|
13719
14556
|
// `{ hN: <originalExpr> }` into `holeProps`. Static structure (tag, literal attrs,
|
|
@@ -13831,6 +14668,22 @@ function extractFragment(node, ctx, holeProps, parentNs = 'html') {
|
|
|
13831
14668
|
// a hole too, since it may be a local/member/dynamic tag that the hoisted
|
|
13832
14669
|
// renderer cannot reference directly.
|
|
13833
14670
|
newChildren.push(extractFragmentComponent(child, ctx, holeProps, childNs));
|
|
14671
|
+
} else if (isComponentTag(child) && isStaticReturnedFragmentComponent(child, ctx)) {
|
|
14672
|
+
// The original function remains callable and still returns its authored
|
|
14673
|
+
// descriptor. Only this nonescaping call can reuse its already-hoisted,
|
|
14674
|
+
// hookless fragment directly through the existing lite component ABI.
|
|
14675
|
+
const extracted = extractFragment(child, ctx, holeProps, childNs);
|
|
14676
|
+
newChildren.push({
|
|
14677
|
+
...extracted,
|
|
14678
|
+
openingElement: {
|
|
14679
|
+
...extracted.openingElement,
|
|
14680
|
+
metadata: {
|
|
14681
|
+
...extracted.openingElement.metadata,
|
|
14682
|
+
staticFragmentRenderer: ctx.componentInfo.get(tagBindingName(child))
|
|
14683
|
+
.staticFragmentRenderer,
|
|
14684
|
+
},
|
|
14685
|
+
},
|
|
14686
|
+
});
|
|
13834
14687
|
} else if (isComponentTag(child)) {
|
|
13835
14688
|
const hn = `h${holeProps.length}`;
|
|
13836
14689
|
holeProps.push(
|
|
@@ -16108,6 +16961,7 @@ function emitAutoMemoRegion(
|
|
|
16108
16961
|
contextAware,
|
|
16109
16962
|
depNode,
|
|
16110
16963
|
initValue = null,
|
|
16964
|
+
restoreCachedContext = false,
|
|
16111
16965
|
) {
|
|
16112
16966
|
const cell = allocAutoMemoCell(ctx, dependencies.length + (contextAware ? 1 : 0));
|
|
16113
16967
|
const contextIndex = contextAware ? cell.base + dependencies.length : null;
|
|
@@ -16157,7 +17011,15 @@ function emitAutoMemoRegion(
|
|
|
16157
17011
|
}
|
|
16158
17012
|
ctx.runtimeNeeded.add('compilerCacheContext');
|
|
16159
17013
|
const cacheContextCall = () =>
|
|
16160
|
-
|
|
17014
|
+
restoreCachedContext
|
|
17015
|
+
? b.call(
|
|
17016
|
+
'_$compilerCacheContext',
|
|
17017
|
+
b.id('__s'),
|
|
17018
|
+
b.literal(slotIndex),
|
|
17019
|
+
cacheAt(contextIndex),
|
|
17020
|
+
b.literal(true),
|
|
17021
|
+
)
|
|
17022
|
+
: b.call('_$compilerCacheContext', b.id('__s'), b.literal(slotIndex), cacheAt(contextIndex));
|
|
16161
17023
|
return b.block([
|
|
16162
17024
|
...depDecls,
|
|
16163
17025
|
b.if(
|
|
@@ -16773,10 +17635,11 @@ function planJsx(
|
|
|
16773
17635
|
const elVar = ensureVar(c.hostPath || []);
|
|
16774
17636
|
c.elVar = elVar;
|
|
16775
17637
|
const org = c.origin ?? planOrigin;
|
|
16776
|
-
|
|
17638
|
+
const key = `_${hostKey}$${c.id}`;
|
|
17639
|
+
if (!noTemplate && bag.host(key, elVar)) {
|
|
16777
17640
|
mountLines.push(
|
|
16778
17641
|
inheritOriginLoc(
|
|
16779
|
-
b.stmt(b.assignment('=', b.id(bag.local(
|
|
17642
|
+
b.stmt(b.assignment('=', b.id(bag.local(key)), hostVarNode(elVar))),
|
|
16780
17643
|
org,
|
|
16781
17644
|
),
|
|
16782
17645
|
);
|
|
@@ -17035,11 +17898,26 @@ function planJsx(
|
|
|
17035
17898
|
const pushAfterStmt = (id, org, node) => pushAfter(id, inheritOriginLoc(node, org));
|
|
17036
17899
|
for (const fc of forCalls) {
|
|
17037
17900
|
const isMappedList = fc.mapMethodExpr !== null;
|
|
17038
|
-
|
|
17901
|
+
const forHelper = isMappedList
|
|
17902
|
+
? fc.hostMountSafe
|
|
17903
|
+
? 'fastMapSlot'
|
|
17904
|
+
: 'mapSlot'
|
|
17905
|
+
: fc.keyedSelectionIndex >= 0
|
|
17906
|
+
? fc.hostMountSafe
|
|
17907
|
+
? 'fastKeyedForBlock'
|
|
17908
|
+
: 'keyedForBlock'
|
|
17909
|
+
: fc.hostMountSafe
|
|
17910
|
+
? 'fastForBlock'
|
|
17911
|
+
: 'forBlock';
|
|
17912
|
+
ctx.runtimeNeeded.add(forHelper);
|
|
17913
|
+
if (isMappedList && fc.hostMountSafe && fc.autoMemoDeps !== null) {
|
|
17914
|
+
// Memoized maps query the original native-array guard before dispatch.
|
|
17915
|
+
ctx.runtimeNeeded.add('mapSlot');
|
|
17916
|
+
}
|
|
17039
17917
|
const slotIndex = fc.slotIndex;
|
|
17040
17918
|
const org = fc.origin ?? planOrigin;
|
|
17041
17919
|
registerDirectiveOrigin(ctx, org, [
|
|
17042
|
-
|
|
17920
|
+
rtAlias(forHelper),
|
|
17043
17921
|
fc.keyHelper,
|
|
17044
17922
|
fc.bodyHelper,
|
|
17045
17923
|
fc.emptyHelper,
|
|
@@ -17052,13 +17930,19 @@ function planJsx(
|
|
|
17052
17930
|
// for survivors when deps unchanged this render),
|
|
17053
17931
|
// bit 3 = indexIndependent (body binds no `index` → a pure reorder
|
|
17054
17932
|
// that only changes a survivor's position need not re-render it),
|
|
17055
|
-
// bit 4 = SSR emitted markerless direct-host items; hydrate them by root
|
|
17933
|
+
// bit 4 = SSR emitted markerless direct-host items; hydrate them by root,
|
|
17934
|
+
// bit 5 = conditional item bodies require their active scope,
|
|
17935
|
+
// bits 6+ = the zero-based keyedForBlock selection dependency index.
|
|
17936
|
+
// keyedForBlock itself identifies a selection, including dependency zero;
|
|
17937
|
+
// ordinary forBlock reuses bit 5 without an argument or tuple allocation.
|
|
17056
17938
|
const flags =
|
|
17057
17939
|
(fc.pure ? 1 : 0) |
|
|
17058
17940
|
(fc.singleRoot ? 2 : 0) |
|
|
17059
17941
|
(fc.depEligible ? 4 : 0) |
|
|
17060
17942
|
(fc.indexIndependent ? 8 : 0) |
|
|
17061
|
-
(fc.ssrMarkerless ? 16 : 0)
|
|
17943
|
+
(fc.ssrMarkerless ? 16 : 0) |
|
|
17944
|
+
(fc.requiresScope ? 32 : 0) |
|
|
17945
|
+
(fc.keyedSelectionIndex >= 0 ? fc.keyedSelectionIndex << 6 : 0);
|
|
17062
17946
|
// Arg layout: forBlock(__s, slot, host, items, keyFn, body, flags?, deps?,
|
|
17063
17947
|
// emptyBody?, anchor?, ownEnd?).
|
|
17064
17948
|
// Optional args backfill positionally: `flags`/`deps` placeholders
|
|
@@ -17117,7 +18001,7 @@ function planJsx(
|
|
|
17117
18001
|
if (fc.autoMemoDeps !== null) {
|
|
17118
18002
|
const mappedCall = b.stmt(
|
|
17119
18003
|
b.call(
|
|
17120
|
-
|
|
18004
|
+
rtAlias(forHelper),
|
|
17121
18005
|
b.id('__s'),
|
|
17122
18006
|
b.literal(slotIndex),
|
|
17123
18007
|
hostExpr(),
|
|
@@ -17159,7 +18043,7 @@ function planJsx(
|
|
|
17159
18043
|
org,
|
|
17160
18044
|
b.stmt(
|
|
17161
18045
|
b.call(
|
|
17162
|
-
|
|
18046
|
+
rtAlias(forHelper),
|
|
17163
18047
|
b.id('__s'),
|
|
17164
18048
|
b.literal(slotIndex),
|
|
17165
18049
|
hostExpr(),
|
|
@@ -17188,7 +18072,7 @@ function planJsx(
|
|
|
17188
18072
|
slotIndex,
|
|
17189
18073
|
b.stmt(
|
|
17190
18074
|
b.call(
|
|
17191
|
-
|
|
18075
|
+
rtAlias(forHelper),
|
|
17192
18076
|
b.id('__s'),
|
|
17193
18077
|
b.literal(slotIndex),
|
|
17194
18078
|
hostExpr(),
|
|
@@ -17207,7 +18091,7 @@ function planJsx(
|
|
|
17207
18091
|
org,
|
|
17208
18092
|
b.stmt(
|
|
17209
18093
|
b.call(
|
|
17210
|
-
|
|
18094
|
+
rtAlias(forHelper),
|
|
17211
18095
|
b.id('__s'),
|
|
17212
18096
|
b.literal(slotIndex),
|
|
17213
18097
|
hostExpr(),
|
|
@@ -17327,6 +18211,59 @@ function planJsx(
|
|
|
17327
18211
|
continue;
|
|
17328
18212
|
}
|
|
17329
18213
|
ctx.runtimeNeeded.add('setText');
|
|
18214
|
+
const updateHole = () =>
|
|
18215
|
+
b.if(
|
|
18216
|
+
b.logical('||', b.id('_o'), b.binary('!==', chp(), V())),
|
|
18217
|
+
b.block([
|
|
18218
|
+
b.const('_t', chv()),
|
|
18219
|
+
b.if(
|
|
18220
|
+
andChain([
|
|
18221
|
+
b.binary('!=', b.id('_t'), b.literal(null)),
|
|
18222
|
+
b.unary('!', b.id('_o')),
|
|
18223
|
+
b.binary('!==', V(), b.literal(null)),
|
|
18224
|
+
]),
|
|
18225
|
+
b.stmt(b.call('_$setText', b.id('_t'), V())),
|
|
18226
|
+
b.stmt(
|
|
18227
|
+
b.assignment(
|
|
18228
|
+
'=',
|
|
18229
|
+
chv(),
|
|
18230
|
+
b.call(
|
|
18231
|
+
'_$childTextHole',
|
|
18232
|
+
b.id('__s'),
|
|
18233
|
+
b.literal(slotIndex),
|
|
18234
|
+
hostExpr(),
|
|
18235
|
+
V(),
|
|
18236
|
+
b.id('_t'),
|
|
18237
|
+
),
|
|
18238
|
+
),
|
|
18239
|
+
),
|
|
18240
|
+
),
|
|
18241
|
+
b.stmt(b.assignment('=', chp(), V())),
|
|
18242
|
+
]),
|
|
18243
|
+
null,
|
|
18244
|
+
);
|
|
18245
|
+
const ordinary = updateHole();
|
|
18246
|
+
let update = ordinary;
|
|
18247
|
+
if (cc.autoMemoValue === true) {
|
|
18248
|
+
// Only a compiler-owned, non-escaping plain data array may skip
|
|
18249
|
+
// descriptor reconciliation. Accessor-backed, nested, or deferred
|
|
18250
|
+
// children must still be observed on every parent render; the
|
|
18251
|
+
// runtime caches eligibility by immutable snapshot identity.
|
|
18252
|
+
ctx.runtimeNeeded.add('compilerCacheArray');
|
|
18253
|
+
const cached = emitAutoMemoRegion(
|
|
18254
|
+
ctx,
|
|
18255
|
+
['_v'],
|
|
18256
|
+
slotIndex,
|
|
18257
|
+
updateHole(),
|
|
18258
|
+
// Array → scalar → the same array must reconstruct the list.
|
|
18259
|
+
b.binary('!==', chp(), V()),
|
|
18260
|
+
true,
|
|
18261
|
+
undefined,
|
|
18262
|
+
null,
|
|
18263
|
+
true,
|
|
18264
|
+
);
|
|
18265
|
+
update = b.if(b.call('_$compilerCacheArray', V(), chp()), cached, ordinary);
|
|
18266
|
+
}
|
|
17330
18267
|
pushAfterStmt(
|
|
17331
18268
|
cc.id,
|
|
17332
18269
|
org,
|
|
@@ -17344,36 +18281,7 @@ function planJsx(
|
|
|
17344
18281
|
),
|
|
17345
18282
|
),
|
|
17346
18283
|
),
|
|
17347
|
-
|
|
17348
|
-
b.logical('||', b.id('_o'), b.binary('!==', chp(), V())),
|
|
17349
|
-
b.block([
|
|
17350
|
-
b.const('_t', chv()),
|
|
17351
|
-
b.if(
|
|
17352
|
-
andChain([
|
|
17353
|
-
b.binary('!=', b.id('_t'), b.literal(null)),
|
|
17354
|
-
b.unary('!', b.id('_o')),
|
|
17355
|
-
b.binary('!==', V(), b.literal(null)),
|
|
17356
|
-
]),
|
|
17357
|
-
b.stmt(b.call('_$setText', b.id('_t'), V())),
|
|
17358
|
-
b.stmt(
|
|
17359
|
-
b.assignment(
|
|
17360
|
-
'=',
|
|
17361
|
-
chv(),
|
|
17362
|
-
b.call(
|
|
17363
|
-
'_$childTextHole',
|
|
17364
|
-
b.id('__s'),
|
|
17365
|
-
b.literal(slotIndex),
|
|
17366
|
-
hostExpr(),
|
|
17367
|
-
V(),
|
|
17368
|
-
b.id('_t'),
|
|
17369
|
-
),
|
|
17370
|
-
),
|
|
17371
|
-
),
|
|
17372
|
-
),
|
|
17373
|
-
b.stmt(b.assignment('=', chp(), V())),
|
|
17374
|
-
]),
|
|
17375
|
-
null,
|
|
17376
|
-
),
|
|
18284
|
+
update,
|
|
17377
18285
|
]),
|
|
17378
18286
|
);
|
|
17379
18287
|
continue;
|
|
@@ -17809,6 +18717,7 @@ function bagLetter(i) {
|
|
|
17809
18717
|
function makeBag() {
|
|
17810
18718
|
const fields = [];
|
|
17811
18719
|
const byKey = new Map();
|
|
18720
|
+
const byHost = new Map();
|
|
17812
18721
|
const reg = (key, constExpr) => {
|
|
17813
18722
|
let r = byKey.get(key);
|
|
17814
18723
|
if (r === undefined) {
|
|
@@ -17831,6 +18740,16 @@ function makeBag() {
|
|
|
17831
18740
|
return {
|
|
17832
18741
|
/** Mount-write target for `key` — the pre-declared local. */
|
|
17833
18742
|
local: (key) => reg(key, undefined).local,
|
|
18743
|
+
/** Register one immutable DOM-host field; aliases reuse its first mount write. */
|
|
18744
|
+
host: (key, host) => {
|
|
18745
|
+
const existing = byHost.get(host);
|
|
18746
|
+
if (existing !== undefined) {
|
|
18747
|
+
byKey.set(key, existing);
|
|
18748
|
+
return false;
|
|
18749
|
+
}
|
|
18750
|
+
byHost.set(host, reg(key, undefined));
|
|
18751
|
+
return true;
|
|
18752
|
+
},
|
|
17834
18753
|
/** Seed `key` with a constant expression (no local, no mount write). */
|
|
17835
18754
|
constField: (key, expr) => {
|
|
17836
18755
|
reg(key, expr);
|
|
@@ -17975,8 +18894,10 @@ function emitDeferredMount(bind, elVar, bag) {
|
|
|
17975
18894
|
if (!(bind.kind === 'class' && bind.fresh)) {
|
|
17976
18895
|
bag.constField(bind.kind === 'style' ? `_sty$${bind.id}` : `_prev$${bind.id}`, 'undefined');
|
|
17977
18896
|
}
|
|
18897
|
+
const key = `_el$${bind.id}`;
|
|
18898
|
+
if (!bag.host(key, elVar)) return null;
|
|
17978
18899
|
return inheritOriginLoc(
|
|
17979
|
-
b.stmt(b.assignment('=', b.id(bag.local(
|
|
18900
|
+
b.stmt(b.assignment('=', b.id(bag.local(key)), hostVarNode(elVar))),
|
|
17980
18901
|
bindingOrigin(bind),
|
|
17981
18902
|
);
|
|
17982
18903
|
}
|
|
@@ -17994,6 +18915,9 @@ function emitBindingMount(bind, elVar, bag) {
|
|
|
17994
18915
|
const st = (node) => inheritOriginLoc(node, org);
|
|
17995
18916
|
const el = () => hostVarNode(elVar);
|
|
17996
18917
|
const local = (key) => b.id(bag.local(key));
|
|
18918
|
+
const hostKey = `_el$${bind.id}`;
|
|
18919
|
+
const mountHost = () =>
|
|
18920
|
+
bag.host(hostKey, elVar) ? [b.stmt(b.assignment('=', local(hostKey), el()))] : [];
|
|
17997
18921
|
const V = () => b.id('_v');
|
|
17998
18922
|
// The tokens that NAME this binding's lowering carry the authored attribute
|
|
17999
18923
|
// name; everything else in the call maps to the value expression.
|
|
@@ -18011,10 +18935,7 @@ function emitBindingMount(bind, elVar, bag) {
|
|
|
18011
18935
|
return st(b.stmt(b.call('_$markNativeChangeDiagnosticStatic', el())));
|
|
18012
18936
|
}
|
|
18013
18937
|
if (bind.kind === 'nativeChangeRuntime') {
|
|
18014
|
-
return [
|
|
18015
|
-
st(b.stmt(b.assignment('=', local(`_el$${bind.id}`), el()))),
|
|
18016
|
-
st(b.stmt(b.call('_$queueNativeChangeDiagnostic', el()))),
|
|
18017
|
-
];
|
|
18938
|
+
return [...mountHost().map(st), st(b.stmt(b.call('_$queueNativeChangeDiagnostic', el())))];
|
|
18018
18939
|
}
|
|
18019
18940
|
switch (bind.kind) {
|
|
18020
18941
|
case 'textOnlyChild': {
|
|
@@ -18036,7 +18957,7 @@ function emitBindingMount(bind, elVar, bag) {
|
|
|
18036
18957
|
b.block([
|
|
18037
18958
|
b.const('_v', bind.expr),
|
|
18038
18959
|
b.stmt(b.call('_$setDangerouslySetInnerHTML', el(), V())),
|
|
18039
|
-
|
|
18960
|
+
...mountHost(),
|
|
18040
18961
|
b.stmt(b.assignment('=', local(`_prev$${bind.id}`), V())),
|
|
18041
18962
|
]),
|
|
18042
18963
|
);
|
|
@@ -18065,7 +18986,7 @@ function emitBindingMount(bind, elVar, bag) {
|
|
|
18065
18986
|
return st(
|
|
18066
18987
|
b.block([
|
|
18067
18988
|
b.stmt(b.call('_$setDangerouslySetInnerHTMLSources', el(), sources)),
|
|
18068
|
-
|
|
18989
|
+
...mountHost(),
|
|
18069
18990
|
]),
|
|
18070
18991
|
);
|
|
18071
18992
|
}
|
|
@@ -18074,14 +18995,11 @@ function emitBindingMount(bind, elVar, bag) {
|
|
|
18074
18995
|
b.id(bag.local(`${spread ? '_sp' : '_prev'}$${binding.id}`)),
|
|
18075
18996
|
);
|
|
18076
18997
|
return st(
|
|
18077
|
-
b.block([
|
|
18078
|
-
b.stmt(b.call('_$setFormControlSources', el(), sources)),
|
|
18079
|
-
b.stmt(b.assignment('=', local(`_el$${bind.id}`), el())),
|
|
18080
|
-
]),
|
|
18998
|
+
b.block([b.stmt(b.call('_$setFormControlSources', el(), sources)), ...mountHost()]),
|
|
18081
18999
|
);
|
|
18082
19000
|
}
|
|
18083
19001
|
case 'hostCommit': {
|
|
18084
|
-
const
|
|
19002
|
+
const hostMount = mountHost();
|
|
18085
19003
|
const propsLocal = local(`_host$${bind.id}`);
|
|
18086
19004
|
const sources = commitSourceRows(bind.sources, (binding, spread) =>
|
|
18087
19005
|
b.id(bag.local(`${spread ? '_sp' : '_prev'}$${binding.id}`)),
|
|
@@ -18112,7 +19030,7 @@ function emitBindingMount(bind, elVar, bag) {
|
|
|
18112
19030
|
);
|
|
18113
19031
|
return st(
|
|
18114
19032
|
b.block([
|
|
18115
|
-
|
|
19033
|
+
...hostMount,
|
|
18116
19034
|
b.stmt(
|
|
18117
19035
|
b.assignment(
|
|
18118
19036
|
'=',
|
|
@@ -18155,7 +19073,7 @@ function emitBindingMount(bind, elVar, bag) {
|
|
|
18155
19073
|
b.block([
|
|
18156
19074
|
b.const('_v', bind.expr),
|
|
18157
19075
|
b.stmt(b.call(callee(), el(), nameLit(), V())),
|
|
18158
|
-
|
|
19076
|
+
...mountHost(),
|
|
18159
19077
|
b.stmt(b.assignment('=', local(`_prev$${bind.id}`), V())),
|
|
18160
19078
|
]),
|
|
18161
19079
|
);
|
|
@@ -18172,12 +19090,7 @@ function emitBindingMount(bind, elVar, bag) {
|
|
|
18172
19090
|
// update must re-run every render to reassert drift (React's
|
|
18173
19091
|
// controlled contract). Not in DEFERRABLE_MOUNT_KINDS: the mount
|
|
18174
19092
|
// runs inside the hydration window and arms the element.
|
|
18175
|
-
return st(
|
|
18176
|
-
b.block([
|
|
18177
|
-
b.stmt(b.call(callee(), el(), bind.expr)),
|
|
18178
|
-
b.stmt(b.assignment('=', local(`_el$${bind.id}`), el())),
|
|
18179
|
-
]),
|
|
18180
|
-
);
|
|
19093
|
+
return st(b.block([b.stmt(b.call(callee(), el(), bind.expr)), ...mountHost()]));
|
|
18181
19094
|
}
|
|
18182
19095
|
case 'autoFocus': {
|
|
18183
19096
|
// Mount-only (React ignores later autoFocus changes); the focus
|
|
@@ -18187,11 +19100,7 @@ function emitBindingMount(bind, elVar, bag) {
|
|
|
18187
19100
|
case 'class': {
|
|
18188
19101
|
// On SVG/MathML hosts the `className` property is read-only — fall back
|
|
18189
19102
|
// to setAttribute. Compile-time choice, zero runtime branching.
|
|
18190
|
-
const body = [
|
|
18191
|
-
b.const('_v', bind.expr),
|
|
18192
|
-
b.stmt(b.call(callee(), el(), V())),
|
|
18193
|
-
b.stmt(b.assignment('=', local(`_el$${bind.id}`), el())),
|
|
18194
|
-
];
|
|
19103
|
+
const body = [b.const('_v', bind.expr), b.stmt(b.call(callee(), el(), V())), ...mountHost()];
|
|
18195
19104
|
if (!bind.fresh) body.push(b.stmt(b.assignment('=', local(`_prev$${bind.id}`), V())));
|
|
18196
19105
|
return st(b.block(body));
|
|
18197
19106
|
}
|
|
@@ -18200,7 +19109,7 @@ function emitBindingMount(bind, elVar, bag) {
|
|
|
18200
19109
|
b.block([
|
|
18201
19110
|
b.const('_v', bind.expr),
|
|
18202
19111
|
b.stmt(b.call(callee(), el(), V(), undefinedNode())),
|
|
18203
|
-
|
|
19112
|
+
...mountHost(),
|
|
18204
19113
|
b.stmt(b.assignment('=', local(`_sty$${bind.id}`), V())),
|
|
18205
19114
|
]),
|
|
18206
19115
|
);
|
|
@@ -18223,7 +19132,7 @@ function emitBindingMount(bind, elVar, bag) {
|
|
|
18223
19132
|
: [];
|
|
18224
19133
|
// Register the mount locals BEFORE the cleanup closure resolves their
|
|
18225
19134
|
// bag letters (letter() throws for a never-mounted field).
|
|
18226
|
-
const
|
|
19135
|
+
const hostMount = mountHost();
|
|
18227
19136
|
const spLocal = local(`_sp$${bind.id}`);
|
|
18228
19137
|
const cleanup = b.arrow(
|
|
18229
19138
|
[],
|
|
@@ -18250,7 +19159,7 @@ function emitBindingMount(bind, elVar, bag) {
|
|
|
18250
19159
|
b.block([
|
|
18251
19160
|
b.const('_v', bind.expr),
|
|
18252
19161
|
b.stmt(b.call('_$setSpread', el(), V(), undefinedNode(), b.id('__s'), ...flags)),
|
|
18253
|
-
|
|
19162
|
+
...hostMount,
|
|
18254
19163
|
b.stmt(b.assignment('=', spLocal, V())),
|
|
18255
19164
|
cleanupsPush(cleanup),
|
|
18256
19165
|
]),
|
|
@@ -18264,7 +19173,7 @@ function emitBindingMount(bind, elVar, bag) {
|
|
|
18264
19173
|
b.assignment('=', b.member(el(), slotKeyLiteral(bind), true), value),
|
|
18265
19174
|
);
|
|
18266
19175
|
if (bind.mountOnly) return st(slotAssign);
|
|
18267
|
-
return [
|
|
19176
|
+
return [...mountHost().map(st), st(slotAssign)];
|
|
18268
19177
|
}
|
|
18269
19178
|
case 'formAction': {
|
|
18270
19179
|
// <form action={fn}> / <button formAction={fn}>: wire the submit handler
|
|
@@ -18274,7 +19183,7 @@ function emitBindingMount(bind, elVar, bag) {
|
|
|
18274
19183
|
b.block([
|
|
18275
19184
|
b.const('_v', bind.expr),
|
|
18276
19185
|
b.stmt(b.call(callee(), el(), nameLit(), V(), undefinedNode())),
|
|
18277
|
-
|
|
19186
|
+
...mountHost(),
|
|
18278
19187
|
b.stmt(b.assignment('=', local(`_prev$${bind.id}`), V())),
|
|
18279
19188
|
]),
|
|
18280
19189
|
);
|
|
@@ -18314,6 +19223,7 @@ function emitBindingMount(bind, elVar, bag) {
|
|
|
18314
19223
|
// cleanup read because updates re-point it. Assigning both bag locals inside
|
|
18315
19224
|
// the queue call evaluates each mount value once while avoiding throwaway
|
|
18316
19225
|
// temporaries; Suspense still receives the exact ref/target pair.
|
|
19226
|
+
const initializeHost = bag.host(hostKey, elVar);
|
|
18317
19227
|
return st(
|
|
18318
19228
|
b.block([
|
|
18319
19229
|
b.stmt(
|
|
@@ -18321,7 +19231,7 @@ function emitBindingMount(bind, elVar, bag) {
|
|
|
18321
19231
|
'_$queueRefAttach',
|
|
18322
19232
|
b.id('__s'),
|
|
18323
19233
|
b.assignment('=', local(`_ref$${bind.id}`), bind.expr),
|
|
18324
|
-
b.assignment('=', local(
|
|
19234
|
+
initializeHost ? b.assignment('=', local(hostKey), el()) : local(hostKey),
|
|
18325
19235
|
),
|
|
18326
19236
|
),
|
|
18327
19237
|
cleanupsPush(
|
|
@@ -19616,8 +20526,21 @@ function emitElementHtml(
|
|
|
19616
20526
|
// is unreachable from the output: the key's map position resolves to
|
|
19617
20527
|
// the HANDLER expression, and the name itself emits nothing.
|
|
19618
20528
|
registerExactOrigin(ctx, attr.name, attr.name?.end, [`'${slotKey}'`, `"${slotKey}"`]);
|
|
19619
|
-
if (capture)
|
|
19620
|
-
|
|
20529
|
+
if (capture) {
|
|
20530
|
+
ctx.capturedEvents.add(eventName);
|
|
20531
|
+
if (ctx.currentComponentOwner !== null) {
|
|
20532
|
+
ctx.currentComponentOwner.capturedEvents.add(eventName);
|
|
20533
|
+
} else {
|
|
20534
|
+
ctx.unownedCapturedEvents.add(eventName);
|
|
20535
|
+
}
|
|
20536
|
+
} else {
|
|
20537
|
+
ctx.delegatedEvents.add(eventName);
|
|
20538
|
+
if (ctx.currentComponentOwner !== null) {
|
|
20539
|
+
ctx.currentComponentOwner.delegatedEvents.add(eventName);
|
|
20540
|
+
} else {
|
|
20541
|
+
ctx.unownedDelegatedEvents.add(eventName);
|
|
20542
|
+
}
|
|
20543
|
+
}
|
|
19621
20544
|
// Hot-path optimisation: `() => fn(arg, …)` arrows with zero params get
|
|
19622
20545
|
// compiled to a `{ fn, args }` bundle so the runtime can identity-diff
|
|
19623
20546
|
// fn + each arg and skip the property reassignment when nothing
|
|
@@ -20613,7 +21536,7 @@ function soleRenderPropChild(children) {
|
|
|
20613
21536
|
// expression remains AST throughout so a nested `() => @{…}` sub-template
|
|
20614
21537
|
// hoists (and server-mode `use(thenable)` calls get their stable keys).
|
|
20615
21538
|
function makeChildCall(expr, ctx, componentName, inlinedSubs, cssHash) {
|
|
20616
|
-
|
|
21539
|
+
const child = {
|
|
20617
21540
|
id: ctx.nextHelperId++,
|
|
20618
21541
|
loc: devLoc(ctx, expr),
|
|
20619
21542
|
origin: expr,
|
|
@@ -20625,6 +21548,27 @@ function makeChildCall(expr, ctx, componentName, inlinedSubs, cssHash) {
|
|
|
20625
21548
|
inlinedSubs,
|
|
20626
21549
|
),
|
|
20627
21550
|
};
|
|
21551
|
+
if (
|
|
21552
|
+
ctx.autoMemo &&
|
|
21553
|
+
// Imported calculations do not expose their return type. Restrict the
|
|
21554
|
+
// array guard to deferred component-child bodies, where cached descriptor
|
|
21555
|
+
// lists otherwise cross an expensive value-position boundary; an ordinary
|
|
21556
|
+
// scalar directly in its owning component must not acquire this region.
|
|
21557
|
+
ctx.currentBodyIsComponentScope !== true &&
|
|
21558
|
+
expr?.type === 'Identifier'
|
|
21559
|
+
) {
|
|
21560
|
+
for (
|
|
21561
|
+
let proof = ctx.currentAutoCalculatedRenderableRefs;
|
|
21562
|
+
proof !== null;
|
|
21563
|
+
proof = proof.parent
|
|
21564
|
+
) {
|
|
21565
|
+
if (proof.nodes.has(expr)) {
|
|
21566
|
+
child.autoMemoValue = true;
|
|
21567
|
+
break;
|
|
21568
|
+
}
|
|
21569
|
+
}
|
|
21570
|
+
}
|
|
21571
|
+
return child;
|
|
20628
21572
|
}
|
|
20629
21573
|
|
|
20630
21574
|
const AST_STRUCTURAL_KEY_SKIP = new Set([
|
|
@@ -20761,7 +21705,10 @@ function makeCompCall(
|
|
|
20761
21705
|
) {
|
|
20762
21706
|
const id = ctx.nextHelperId++;
|
|
20763
21707
|
const compName = tagBindingName(node);
|
|
20764
|
-
const
|
|
21708
|
+
const staticFragmentRenderer = node.openingElement?.metadata?.staticFragmentRenderer;
|
|
21709
|
+
const compNode = staticFragmentRenderer
|
|
21710
|
+
? inheritOriginLoc(b.id(staticFragmentRenderer.name), node.openingElement?.name || node.id)
|
|
21711
|
+
: tagExprNode(node);
|
|
20765
21712
|
// `</Card>` emits nothing — lend it the opening name's generated ranges so a
|
|
20766
21713
|
// closing tag is reachable for components the way it is for host elements.
|
|
20767
21714
|
registerOriginAlias(ctx, node.closingElement?.name, node.id || node.openingElement?.name);
|
|
@@ -20878,7 +21825,7 @@ function makeCompCall(
|
|
|
20878
21825
|
}
|
|
20879
21826
|
|
|
20880
21827
|
// The props object as a node; the call-site emit embeds it directly.
|
|
20881
|
-
const propsExpr = inheritOriginLoc(b.object(propNodes), node);
|
|
21828
|
+
const propsExpr = staticFragmentRenderer?.props ?? inheritOriginLoc(b.object(propNodes), node);
|
|
20882
21829
|
|
|
20883
21830
|
// Design (c) v0: decide whether the call site can use componentSlotLite
|
|
20884
21831
|
// (Scope-only, no Block / no Comment markers / no CompSlot wrapper).
|
|
@@ -20909,7 +21856,12 @@ function makeCompCall(
|
|
|
20909
21856
|
// elides iff the callee carries the definition-site `$$singleRoot` stamp
|
|
20910
21857
|
// (docs/comment-marker-elision-plan.md M1).
|
|
20911
21858
|
let maybeSingleRoot = false;
|
|
20912
|
-
if (
|
|
21859
|
+
if (staticFragmentRenderer) {
|
|
21860
|
+
// The renderer is already a void, hookless component body. A memo boundary
|
|
21861
|
+
// would force a full Block and could hide a hookful descendant's update.
|
|
21862
|
+
liteEligible = true;
|
|
21863
|
+
voidComponent = true;
|
|
21864
|
+
} else if (ctx.componentInfo && compName !== null) {
|
|
20913
21865
|
const callSiteOk = !hasSpreadProp && !hasChildrenProp;
|
|
20914
21866
|
const calleeInfo = ctx.componentInfo.get(compName);
|
|
20915
21867
|
if (calleeInfo) {
|
|
@@ -21196,6 +22148,96 @@ function makeSwitchCall(node, ctx, inlinedSubs, parentNs = 'html', cssHash = nul
|
|
|
21196
22148
|
// for-of inside element children → forBlock call
|
|
21197
22149
|
// ===========================================================================
|
|
21198
22150
|
|
|
22151
|
+
/**
|
|
22152
|
+
* Prove that one parent dependency affects a keyed host row only through its
|
|
22153
|
+
* root class's strict comparison with the row key. The runtime can then update
|
|
22154
|
+
* only the old/new selected blocks when every other dependency and the iterable
|
|
22155
|
+
* snapshot remain unchanged.
|
|
22156
|
+
*
|
|
22157
|
+
* This recognizes `selected === item.id` (or its reverse) under `key item.id`,
|
|
22158
|
+
* and the same proof for any other explicit, noncomputed item property.
|
|
22159
|
+
* Dynamic ternary arms, extra references to
|
|
22160
|
+
* `selected` anywhere in the row (including deferred event callbacks), spreads,
|
|
22161
|
+
* and anything other than one direct host root all fail closed.
|
|
22162
|
+
*/
|
|
22163
|
+
function keyedSelectionDepIndex(itemName, keyBody, subStmts, runtimeDepNames, ctx) {
|
|
22164
|
+
if (subStmts.length !== 1 || !isPlainHostRoot(subStmts[0])) return -1;
|
|
22165
|
+
const root = subStmts[0];
|
|
22166
|
+
const attrs = root.attributes || root.openingElement?.attributes || [];
|
|
22167
|
+
if (attrs.some((attr) => attr.type !== 'Attribute' && attr.type !== 'JSXAttribute')) {
|
|
22168
|
+
return -1;
|
|
22169
|
+
}
|
|
22170
|
+
const classAttrs = attrs.filter((attr) => {
|
|
22171
|
+
const name = attr.name?.name || attr.name;
|
|
22172
|
+
return name === 'class' || name === 'className';
|
|
22173
|
+
});
|
|
22174
|
+
if (classAttrs.length !== 1) return -1;
|
|
22175
|
+
const value = classAttrs[0].value;
|
|
22176
|
+
const conditional = unwrapTsExpr(
|
|
22177
|
+
value?.type === 'JSXExpressionContainer' ? value.expression : value,
|
|
22178
|
+
);
|
|
22179
|
+
if (conditional?.type !== 'ConditionalExpression') return -1;
|
|
22180
|
+
const consequent = unwrapTsExpr(conditional.consequent);
|
|
22181
|
+
const alternate = unwrapTsExpr(conditional.alternate);
|
|
22182
|
+
if (
|
|
22183
|
+
consequent?.type !== 'Literal' ||
|
|
22184
|
+
typeof consequent.value !== 'string' ||
|
|
22185
|
+
alternate?.type !== 'Literal' ||
|
|
22186
|
+
typeof alternate.value !== 'string'
|
|
22187
|
+
) {
|
|
22188
|
+
return -1;
|
|
22189
|
+
}
|
|
22190
|
+
|
|
22191
|
+
const test = unwrapTsExpr(conditional.test);
|
|
22192
|
+
if (test?.type !== 'BinaryExpression' || test.operator !== '===') return -1;
|
|
22193
|
+
const key = unwrapTsExpr(keyBody);
|
|
22194
|
+
if (
|
|
22195
|
+
key?.type !== 'MemberExpression' ||
|
|
22196
|
+
key.computed === true ||
|
|
22197
|
+
key.optional === true ||
|
|
22198
|
+
key.object?.type !== 'Identifier' ||
|
|
22199
|
+
key.object.name !== itemName ||
|
|
22200
|
+
key.property?.type !== 'Identifier'
|
|
22201
|
+
) {
|
|
22202
|
+
return -1;
|
|
22203
|
+
}
|
|
22204
|
+
const keyProperty = key.property.name;
|
|
22205
|
+
const isItemKey = (expression) => {
|
|
22206
|
+
const member = unwrapTsExpr(expression);
|
|
22207
|
+
return (
|
|
22208
|
+
member?.type === 'MemberExpression' &&
|
|
22209
|
+
member.computed !== true &&
|
|
22210
|
+
member.optional !== true &&
|
|
22211
|
+
member.object?.type === 'Identifier' &&
|
|
22212
|
+
member.object.name === itemName &&
|
|
22213
|
+
member.property?.type === 'Identifier' &&
|
|
22214
|
+
member.property.name === keyProperty
|
|
22215
|
+
);
|
|
22216
|
+
};
|
|
22217
|
+
const left = unwrapTsExpr(test.left);
|
|
22218
|
+
const right = unwrapTsExpr(test.right);
|
|
22219
|
+
const selected = isItemKey(left) ? right : isItemKey(right) ? left : null;
|
|
22220
|
+
if (
|
|
22221
|
+
selected?.type !== 'Identifier' ||
|
|
22222
|
+
selected.name === itemName ||
|
|
22223
|
+
!ctx.currentComponentLocals?.has(selected.name)
|
|
22224
|
+
) {
|
|
22225
|
+
return -1;
|
|
22226
|
+
}
|
|
22227
|
+
// Ignore this exact comparison operand, then use the existing scope-aware
|
|
22228
|
+
// reference walker to detect any other capture of the same outer binding.
|
|
22229
|
+
// Shadowed callback parameters therefore stay harmless while a real event
|
|
22230
|
+
// closure over the selection correctly disables the specialization.
|
|
22231
|
+
if (
|
|
22232
|
+
collectFreeIdentifiers(b.block(subStmts), new Set([itemName]), new Set([selected])).has(
|
|
22233
|
+
selected.name,
|
|
22234
|
+
)
|
|
22235
|
+
) {
|
|
22236
|
+
return -1;
|
|
22237
|
+
}
|
|
22238
|
+
return runtimeDepNames.indexOf(selected.name);
|
|
22239
|
+
}
|
|
22240
|
+
|
|
21199
22241
|
function makeForCall(node, ctx, inlinedSubs, parentNs = 'html', cssHash = null) {
|
|
21200
22242
|
// `@for await (...)` (async iteration) has no meaning for the runtime's
|
|
21201
22243
|
// synchronous keyed reconciler. The TSRX parser currently rejects the surface
|
|
@@ -21345,16 +22387,16 @@ function makeForCall(node, ctx, inlinedSubs, parentNs = 'html', cssHash = null)
|
|
|
21345
22387
|
// - PURE: body closes over nothing parent-reactive, no hooks, no comps,
|
|
21346
22388
|
// no control flow. Reconciler skips renderBlock when item ref + index
|
|
21347
22389
|
// unchanged. Identified by `pure = true`.
|
|
21348
|
-
// - DEP-PURE: body DOES close over parent locals but
|
|
21349
|
-
//
|
|
21350
|
-
//
|
|
21351
|
-
//
|
|
21352
|
-
//
|
|
21353
|
-
// item-ref-and-index-stable survivors — no per-row snapshot work.
|
|
22390
|
+
// - DEP-PURE: body DOES close over parent locals but has no hooks or opaque
|
|
22391
|
+
// components. Proven host-only conditional content is permitted when the
|
|
22392
|
+
// runtime preserves the row's active scope. The compiler emits an explicit
|
|
22393
|
+
// deps array so the reconciler can do ONE equality check per parent render
|
|
22394
|
+
// and, if unchanged, skip item-ref-and-index-stable survivors entirely.
|
|
21354
22395
|
// - NORMAL: anything else → body runs every render.
|
|
21355
22396
|
let pure = false;
|
|
21356
22397
|
const depNames = [];
|
|
21357
22398
|
let depEligible = false;
|
|
22399
|
+
let requiresScope = false;
|
|
21358
22400
|
let itemMemo = false;
|
|
21359
22401
|
let itemMemoContextAware = false;
|
|
21360
22402
|
let itemMemoWitnesses = [];
|
|
@@ -21528,8 +22570,23 @@ function makeForCall(node, ctx, inlinedSubs, parentNs = 'html', cssHash = null)
|
|
|
21528
22570
|
// survivor shortcut has no compiler-cache epoch cell to consult).
|
|
21529
22571
|
if (itemMemoContextAware && autoMemoDeps === null) itemMemo = false;
|
|
21530
22572
|
const hostPure = !hasParentClosure && !hasHook && !hasNestedComp && !hasRenderCall;
|
|
22573
|
+
const conditionalHostDepEligible =
|
|
22574
|
+
ctx.autoMemo === true &&
|
|
22575
|
+
hasNestedComp &&
|
|
22576
|
+
hasParentClosure &&
|
|
22577
|
+
!hasHook &&
|
|
22578
|
+
!hasRenderCall &&
|
|
22579
|
+
node.nativeArrayMap === undefined &&
|
|
22580
|
+
autoMemoDeps !== null &&
|
|
22581
|
+
autoMemoDeps.every((name) => seenDeps.has(name)) &&
|
|
22582
|
+
!containsAutoMemoContextRead(bodyAst, ctx) &&
|
|
22583
|
+
hasOnlyHostConditionalItemBodies(subStmts);
|
|
21531
22584
|
const hostDepEligible =
|
|
21532
|
-
!hostPure &&
|
|
22585
|
+
!hostPure &&
|
|
22586
|
+
!hasHook &&
|
|
22587
|
+
hasParentClosure &&
|
|
22588
|
+
(!hasNestedComp || conditionalHostDepEligible) &&
|
|
22589
|
+
!hasRenderCall;
|
|
21533
22590
|
if (itemMemo && itemMemoWitnesses.length > 0) {
|
|
21534
22591
|
pure = hostPure;
|
|
21535
22592
|
depEligible = hostDepEligible;
|
|
@@ -21538,6 +22595,7 @@ function makeForCall(node, ctx, inlinedSubs, parentNs = 'html', cssHash = null)
|
|
|
21538
22595
|
pure = hostPure || (itemMemo && depNames.length === 0);
|
|
21539
22596
|
depEligible = !pure && !hasHook && (hostDepEligible || (itemMemo && depNames.length > 0));
|
|
21540
22597
|
}
|
|
22598
|
+
requiresScope = depEligible && conditionalHostDepEligible;
|
|
21541
22599
|
depNames.sort();
|
|
21542
22600
|
}
|
|
21543
22601
|
|
|
@@ -21565,6 +22623,23 @@ function makeForCall(node, ctx, inlinedSubs, parentNs = 'html', cssHash = null)
|
|
|
21565
22623
|
envNames === null
|
|
21566
22624
|
? depNames
|
|
21567
22625
|
: [...envNames, ...depNames.filter((name) => !envNames.includes(name))];
|
|
22626
|
+
// Restrict targeted invalidation to the existing production-only pure-render
|
|
22627
|
+
// contract. The whole-list memo proof rules out render-time mutations and
|
|
22628
|
+
// opaque state, while DEP-PURE guarantees a hookless host-only item body.
|
|
22629
|
+
// Native `.map()` uses a distinct mapSlot ABI and stays on its current path.
|
|
22630
|
+
const keyedSelectionIndex =
|
|
22631
|
+
ctx.autoMemo === true &&
|
|
22632
|
+
depEligible &&
|
|
22633
|
+
!requiresScope &&
|
|
22634
|
+
!itemMemo &&
|
|
22635
|
+
autoMemoDeps !== null &&
|
|
22636
|
+
!isDestructured &&
|
|
22637
|
+
!node.index &&
|
|
22638
|
+
!emptyStmts &&
|
|
22639
|
+
node.nativeArrayMap === undefined &&
|
|
22640
|
+
node.key != null
|
|
22641
|
+
? keyedSelectionDepIndex(itemName, keyFn.body, subStmts, runtimeDepNames, ctx)
|
|
22642
|
+
: -1;
|
|
21568
22643
|
let emptyHelperName = 'null';
|
|
21569
22644
|
if (emptyStmts) {
|
|
21570
22645
|
emptyHelperName = hoistBodyHelper(
|
|
@@ -21646,6 +22721,23 @@ function makeForCall(node, ctx, inlinedSubs, parentNs = 'html', cssHash = null)
|
|
|
21646
22721
|
}
|
|
21647
22722
|
}
|
|
21648
22723
|
|
|
22724
|
+
const ssrMarkerless = isSsrMarkerlessForItem(node);
|
|
22725
|
+
const hostRootTag = subStmts[0]?.id?.name ?? subStmts[0]?.openingElement?.name?.name;
|
|
22726
|
+
const hostMountSafe =
|
|
22727
|
+
ctx.autoMemo === true &&
|
|
22728
|
+
ctx._universalRuntimeUnit == null &&
|
|
22729
|
+
(parentNs === 'html' || (parentNs === 'opaque' && HTML_ONLY_TAGS.has(hostRootTag))) &&
|
|
22730
|
+
!emptyStmts &&
|
|
22731
|
+
!isDestructured &&
|
|
22732
|
+
!node.index &&
|
|
22733
|
+
subStmts.length === 1 &&
|
|
22734
|
+
singleRoot &&
|
|
22735
|
+
ssrMarkerless &&
|
|
22736
|
+
!containsComponentCallOrControlFlow(subStmts) &&
|
|
22737
|
+
!containsRenderCall(subStmts) &&
|
|
22738
|
+
!containsAutoMemoUnsafeStructure(subStmts) &&
|
|
22739
|
+
isHostMountSafeTree(subStmts[0]);
|
|
22740
|
+
|
|
21649
22741
|
const mapCall = node.nativeArrayMap || null;
|
|
21650
22742
|
return {
|
|
21651
22743
|
id: ctx.nextHelperId++,
|
|
@@ -21679,18 +22771,21 @@ function makeForCall(node, ctx, inlinedSubs, parentNs = 'html', cssHash = null)
|
|
|
21679
22771
|
pure,
|
|
21680
22772
|
singleRoot,
|
|
21681
22773
|
singleRootExpr,
|
|
21682
|
-
ssrMarkerless
|
|
22774
|
+
ssrMarkerless,
|
|
22775
|
+
hostMountSafe,
|
|
21683
22776
|
// The env union doubles as the deps array: emitted whenever the helpers
|
|
21684
22777
|
// capture anything (Phase 2 — the runtime stamps it as block.extra), and
|
|
21685
22778
|
// ALSO compared for the dep-pure survivor short-circuit when depEligible.
|
|
21686
22779
|
// Component-local entries remain the tuple prefix the helpers destructure;
|
|
21687
22780
|
// any appended import witnesses are comparison-only.
|
|
21688
22781
|
depEligible,
|
|
22782
|
+
requiresScope,
|
|
21689
22783
|
itemMemoWitnesses,
|
|
21690
22784
|
itemMemoFlags,
|
|
21691
22785
|
autoMemoDeps,
|
|
21692
22786
|
autoMemoWitnesses,
|
|
21693
22787
|
autoMemoContextAware,
|
|
22788
|
+
keyedSelectionIndex,
|
|
21694
22789
|
depNames: runtimeDepNames,
|
|
21695
22790
|
// True only when the header binds NO `index <name>` — the body then can't
|
|
21696
22791
|
// observe an item's position, so a pure reorder (same item ref, position
|
|
@@ -21894,10 +22989,41 @@ function bodyContainsJsx(node) {
|
|
|
21894
22989
|
|
|
21895
22990
|
/** @param {TemplateIR} ast */
|
|
21896
22991
|
function allocTemplate(ctx, ast, ns = 0, frag = 0) {
|
|
22992
|
+
const { html, origins } = serializeTemplateIr(ast);
|
|
22993
|
+
const intern = !ctx.hmr && !ctx.dev && !ctx.profile;
|
|
22994
|
+
const bucket = intern ? ctx.internedTemplates.get(html) : undefined;
|
|
22995
|
+
const existing = Array.isArray(bucket)
|
|
22996
|
+
? bucket.find((template) => template.ns === ns && template.frag === frag)
|
|
22997
|
+
: bucket?.ns === ns && bucket.frag === frag
|
|
22998
|
+
? bucket
|
|
22999
|
+
: undefined;
|
|
23000
|
+
if (existing !== undefined) {
|
|
23001
|
+
if (ctx.inspect && origins !== null && existing.origins !== null) {
|
|
23002
|
+
for (const origin of origins) {
|
|
23003
|
+
const canonical = existing.origins.find(
|
|
23004
|
+
(entry) =>
|
|
23005
|
+
entry.kind === origin.kind && entry.start === origin.start && entry.end === origin.end,
|
|
23006
|
+
);
|
|
23007
|
+
if (canonical !== undefined) {
|
|
23008
|
+
registerOriginAlias(
|
|
23009
|
+
ctx,
|
|
23010
|
+
{ start: origin.srcStart, end: origin.srcEnd },
|
|
23011
|
+
{ start: canonical.srcStart },
|
|
23012
|
+
);
|
|
23013
|
+
}
|
|
23014
|
+
}
|
|
23015
|
+
}
|
|
23016
|
+
return existing.name;
|
|
23017
|
+
}
|
|
21897
23018
|
const id = ctx.nextTemplateId++;
|
|
21898
23019
|
const name = `_t$${id}`;
|
|
21899
|
-
const { html, origins }
|
|
21900
|
-
ctx.hoistedTemplates.push(
|
|
23020
|
+
const template = { name, ast, html, ns, frag, origins };
|
|
23021
|
+
ctx.hoistedTemplates.push(template);
|
|
23022
|
+
if (intern) {
|
|
23023
|
+
if (bucket === undefined) ctx.internedTemplates.set(html, template);
|
|
23024
|
+
else if (Array.isArray(bucket)) bucket.push(template);
|
|
23025
|
+
else ctx.internedTemplates.set(html, [bucket, template]);
|
|
23026
|
+
}
|
|
21901
23027
|
return name;
|
|
21902
23028
|
}
|
|
21903
23029
|
|