octane 0.1.29 → 0.1.31
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 +1521 -138
- 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 +602 -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,90 @@ function containsComponentCallOrControlFlow(stmts) {
|
|
|
3343
3465
|
return found;
|
|
3344
3466
|
}
|
|
3345
3467
|
|
|
3468
|
+
/**
|
|
3469
|
+
* A host-only conditional can share its containing row's immutable
|
|
3470
|
+
* item/dependency proof, including through a narrowly proven nested keyed list.
|
|
3471
|
+
* Entering either construct still needs the row's active scope. Lists without a
|
|
3472
|
+
* conditional, components, opaque list expressions, and every other
|
|
3473
|
+
* control-flow boundary remain on their existing path.
|
|
3474
|
+
*/
|
|
3475
|
+
function hasOnlyHostConditionalItemBodies(stmts) {
|
|
3476
|
+
let hasConditional = false;
|
|
3477
|
+
let disallowed = false;
|
|
3478
|
+
const seen = new WeakSet();
|
|
3479
|
+
function walk(node) {
|
|
3480
|
+
if (disallowed || !node) return;
|
|
3481
|
+
if (Array.isArray(node)) {
|
|
3482
|
+
for (const child of node) walk(child);
|
|
3483
|
+
return;
|
|
3484
|
+
}
|
|
3485
|
+
if (typeof node !== 'object') return;
|
|
3486
|
+
const type = node.type;
|
|
3487
|
+
if (!type || seen.has(node)) return;
|
|
3488
|
+
seen.add(node);
|
|
3489
|
+
if (
|
|
3490
|
+
type === 'ArrowFunctionExpression' ||
|
|
3491
|
+
type === 'FunctionExpression' ||
|
|
3492
|
+
type === 'FunctionDeclaration'
|
|
3493
|
+
) {
|
|
3494
|
+
return;
|
|
3495
|
+
}
|
|
3496
|
+
if ((type === 'Element' || type === 'JSXElement') && isComponentTag(node)) {
|
|
3497
|
+
disallowed = true;
|
|
3498
|
+
return;
|
|
3499
|
+
}
|
|
3500
|
+
if (type === 'IfStatement' || type === 'JSXIfExpression') {
|
|
3501
|
+
hasConditional = true;
|
|
3502
|
+
} else if (type === 'JSXForExpression') {
|
|
3503
|
+
const declaration = node.left;
|
|
3504
|
+
const item = declaration?.declarations?.[0]?.id;
|
|
3505
|
+
const key = unwrapTsExpr(node.key);
|
|
3506
|
+
if (
|
|
3507
|
+
node.await === true ||
|
|
3508
|
+
node.index != null ||
|
|
3509
|
+
node.nativeArrayMap !== undefined ||
|
|
3510
|
+
declaration?.type !== 'VariableDeclaration' ||
|
|
3511
|
+
declaration.declarations?.length !== 1 ||
|
|
3512
|
+
item?.type !== 'Identifier' ||
|
|
3513
|
+
!isAutoMemoCalculationDependency(node.right) ||
|
|
3514
|
+
key?.type !== 'MemberExpression' ||
|
|
3515
|
+
key.computed === true ||
|
|
3516
|
+
key.optional === true ||
|
|
3517
|
+
key.object?.type !== 'Identifier' ||
|
|
3518
|
+
key.object.name !== item.name ||
|
|
3519
|
+
key.property?.type !== 'Identifier'
|
|
3520
|
+
) {
|
|
3521
|
+
disallowed = true;
|
|
3522
|
+
return;
|
|
3523
|
+
}
|
|
3524
|
+
} else if (
|
|
3525
|
+
type === 'ForStatement' ||
|
|
3526
|
+
type === 'ForInStatement' ||
|
|
3527
|
+
type === 'ForOfStatement' ||
|
|
3528
|
+
type === 'WhileStatement' ||
|
|
3529
|
+
type === 'DoWhileStatement' ||
|
|
3530
|
+
type === 'TryStatement' ||
|
|
3531
|
+
type === 'SwitchStatement' ||
|
|
3532
|
+
type === 'ActivityStatement' ||
|
|
3533
|
+
type === 'JSXTryExpression' ||
|
|
3534
|
+
type === 'JSXSwitchExpression' ||
|
|
3535
|
+
type === 'JSXActivityExpression' ||
|
|
3536
|
+
((type === 'TSRXExpression' || type === 'JSXExpressionContainer') &&
|
|
3537
|
+
node.expression &&
|
|
3538
|
+
isCreatePortalCall(node.expression))
|
|
3539
|
+
) {
|
|
3540
|
+
disallowed = true;
|
|
3541
|
+
return;
|
|
3542
|
+
}
|
|
3543
|
+
for (const key in node) {
|
|
3544
|
+
if (AST_WALK_SKIP_KEYS.has(key)) continue;
|
|
3545
|
+
walk(node[key]);
|
|
3546
|
+
}
|
|
3547
|
+
}
|
|
3548
|
+
for (const statement of stmts) walk(statement);
|
|
3549
|
+
return hasConditional && !disallowed;
|
|
3550
|
+
}
|
|
3551
|
+
|
|
3346
3552
|
/**
|
|
3347
3553
|
* Classify the narrow opaque shape that an automatically memoized keyed item
|
|
3348
3554
|
* may safely contain: ordinary JSX component calls, but no template control
|
|
@@ -3535,15 +3741,16 @@ function containsRenderCall(stmts, memoCtx = null) {
|
|
|
3535
3741
|
}
|
|
3536
3742
|
|
|
3537
3743
|
// A hook call is identified by naming convention — the same signal React and
|
|
3538
|
-
// React Compiler key on — plus
|
|
3539
|
-
// bindings
|
|
3540
|
-
//
|
|
3541
|
-
//
|
|
3744
|
+
// React Compiler key on — plus the exact `unstable_` and `UNSTABLE_` staging
|
|
3745
|
+
// prefixes bindings expose (`unstable_useRouterState` in @octanejs/remix-router
|
|
3746
|
+
// and `UNSTABLE_useTreeGridState` in @octanejs/aria). Enumerate the prefixes
|
|
3747
|
+
// rather than matching "any `_use`" so ordinary helpers are not mistaken for
|
|
3748
|
+
// hooks by spelling alone.
|
|
3542
3749
|
//
|
|
3543
3750
|
// Getting this wrong in the permissive direction is not a staleness bug: a
|
|
3544
3751
|
// cache wrapped around a hook call freezes its subscription and its state cell
|
|
3545
3752
|
// for the life of the component.
|
|
3546
|
-
const HOOK_NAME_CONVENTION_RE = /^(?:
|
|
3753
|
+
const HOOK_NAME_CONVENTION_RE = /^(?:(?:unstable|UNSTABLE)_)?use(?:$|[A-Z])/;
|
|
3547
3754
|
|
|
3548
3755
|
function isHookCalleeName(name) {
|
|
3549
3756
|
return HOOK_NAME_CONVENTION_RE.test(name);
|
|
@@ -3716,6 +3923,202 @@ function collectImmutableModuleFunctions(body) {
|
|
|
3716
3923
|
return declared;
|
|
3717
3924
|
}
|
|
3718
3925
|
|
|
3926
|
+
// A child warm plan is useful only if that child can reach an async creation.
|
|
3927
|
+
// Same-module declarations are the only closed call graph we can prove: an
|
|
3928
|
+
// imported/dynamic component, custom hook, helper call, or lazy state initializer
|
|
3929
|
+
// may suspend behind an opaque boundary, so each keeps its existing warm edge.
|
|
3930
|
+
// A useState call with a primitive literal initializer and publishing its stable
|
|
3931
|
+
// setter are synchronous, so neither turns a synchronous tree into a warm plan.
|
|
3932
|
+
function classifySameModuleWarmPotential(ctx) {
|
|
3933
|
+
for (const [, info] of ctx.componentInfo) {
|
|
3934
|
+
const component = info.node;
|
|
3935
|
+
const statements = component.body.body || [];
|
|
3936
|
+
const locals = collectComponentLocals(component);
|
|
3937
|
+
const invariant = computeInvariantLocals(statements, locals, false);
|
|
3938
|
+
const dependencies = new Set();
|
|
3939
|
+
const seen = new WeakSet();
|
|
3940
|
+
// A reassigned function binding can point at an async component by the
|
|
3941
|
+
// time its warm edge runs. Destructuring/default/rest parameters can also
|
|
3942
|
+
// invoke user code before the authored component body is reached.
|
|
3943
|
+
let opaque =
|
|
3944
|
+
!ctx.moduleFunctionDeclarations.has(component.id?.name) ||
|
|
3945
|
+
(component.params || []).some((parameter) => parameter.type !== 'Identifier');
|
|
3946
|
+
|
|
3947
|
+
function walk(node) {
|
|
3948
|
+
if (opaque || node === null || typeof node !== 'object') return;
|
|
3949
|
+
if (Array.isArray(node)) {
|
|
3950
|
+
for (const child of node) walk(child);
|
|
3951
|
+
return;
|
|
3952
|
+
}
|
|
3953
|
+
if (seen.has(node)) return;
|
|
3954
|
+
seen.add(node);
|
|
3955
|
+
|
|
3956
|
+
// Deferred handlers do not execute during this component's render. State
|
|
3957
|
+
// initializers are admitted only when proven primitive and non-callable.
|
|
3958
|
+
if (FN_TYPES.has(node.type)) return;
|
|
3959
|
+
|
|
3960
|
+
if ((node.type === 'Element' || node.type === 'JSXElement') && isComponentTag(node)) {
|
|
3961
|
+
const name = tagBindingName(node);
|
|
3962
|
+
if (
|
|
3963
|
+
name === null ||
|
|
3964
|
+
locals.has(name) ||
|
|
3965
|
+
ctx._octaneBoundaryNames.has(name) ||
|
|
3966
|
+
!ctx.componentInfo.has(name)
|
|
3967
|
+
) {
|
|
3968
|
+
opaque = true;
|
|
3969
|
+
return;
|
|
3970
|
+
}
|
|
3971
|
+
dependencies.add(name);
|
|
3972
|
+
} else if (node.type === 'CallExpression' || node.type === 'NewExpression') {
|
|
3973
|
+
const hook = stableHookCallName(node);
|
|
3974
|
+
if (
|
|
3975
|
+
hook !== 'useState' ||
|
|
3976
|
+
node.arguments.length > 1 ||
|
|
3977
|
+
(node.arguments.length === 1 && !isInvariantLiteral(unwrapTsExpr(node.arguments[0])))
|
|
3978
|
+
) {
|
|
3979
|
+
opaque = true;
|
|
3980
|
+
return;
|
|
3981
|
+
}
|
|
3982
|
+
} else if (node.type === 'VariableDeclarator' && node.id?.type === 'ArrayPattern') {
|
|
3983
|
+
// The built-in state tuple is the only iterator this proof owns. Any
|
|
3984
|
+
// other destructuring can execute a custom iterator or rest/default.
|
|
3985
|
+
if (
|
|
3986
|
+
stableHookCallName(unwrapTsExpr(node.init)) !== 'useState' ||
|
|
3987
|
+
node.id.elements.some((element) => element !== null && element.type !== 'Identifier')
|
|
3988
|
+
) {
|
|
3989
|
+
opaque = true;
|
|
3990
|
+
return;
|
|
3991
|
+
}
|
|
3992
|
+
} else if (node.type === 'AssignmentExpression') {
|
|
3993
|
+
if (
|
|
3994
|
+
node.operator !== '=' ||
|
|
3995
|
+
node.left?.type !== 'Identifier' ||
|
|
3996
|
+
node.right?.type !== 'Identifier' ||
|
|
3997
|
+
!invariant.has(node.right.name)
|
|
3998
|
+
) {
|
|
3999
|
+
opaque = true;
|
|
4000
|
+
return;
|
|
4001
|
+
}
|
|
4002
|
+
} else if (
|
|
4003
|
+
node.type === 'AwaitExpression' ||
|
|
4004
|
+
node.type === 'YieldExpression' ||
|
|
4005
|
+
node.type === 'MemberExpression' ||
|
|
4006
|
+
node.type === 'JSXMemberExpression' ||
|
|
4007
|
+
node.type === 'OptionalMemberExpression' ||
|
|
4008
|
+
node.type === 'OptionalCallExpression' ||
|
|
4009
|
+
node.type === 'SpreadElement' ||
|
|
4010
|
+
node.type === 'SpreadAttribute' ||
|
|
4011
|
+
node.type === 'JSXSpreadAttribute' ||
|
|
4012
|
+
node.type === 'ObjectPattern' ||
|
|
4013
|
+
node.type === 'AssignmentPattern' ||
|
|
4014
|
+
node.type === 'RestElement' ||
|
|
4015
|
+
node.type === 'ForOfStatement' ||
|
|
4016
|
+
node.type === 'JSXForExpression' ||
|
|
4017
|
+
node.type === 'ForInStatement' ||
|
|
4018
|
+
node.type === 'ForStatement' ||
|
|
4019
|
+
node.type === 'WhileStatement' ||
|
|
4020
|
+
node.type === 'DoWhileStatement' ||
|
|
4021
|
+
node.type === 'ThrowStatement' ||
|
|
4022
|
+
node.type === 'TryStatement' ||
|
|
4023
|
+
node.type === 'JSXTryExpression' ||
|
|
4024
|
+
node.type === 'ImportExpression' ||
|
|
4025
|
+
node.type === 'TaggedTemplateExpression' ||
|
|
4026
|
+
node.type === 'UpdateExpression'
|
|
4027
|
+
) {
|
|
4028
|
+
opaque = true;
|
|
4029
|
+
return;
|
|
4030
|
+
}
|
|
4031
|
+
|
|
4032
|
+
for (const key in node) {
|
|
4033
|
+
if (AST_WALK_SKIP_KEYS.has(key)) continue;
|
|
4034
|
+
walk(node[key]);
|
|
4035
|
+
}
|
|
4036
|
+
}
|
|
4037
|
+
|
|
4038
|
+
walk(statements);
|
|
4039
|
+
walk(component.body.render);
|
|
4040
|
+
info.warmPotential = opaque;
|
|
4041
|
+
info.warmDependencies = dependencies;
|
|
4042
|
+
}
|
|
4043
|
+
|
|
4044
|
+
// Propagate async reachability through forward references and recursive
|
|
4045
|
+
// same-module chains. An all-synchronous cycle stays false; one opaque or
|
|
4046
|
+
// async descendant makes every component that can reach it conservative.
|
|
4047
|
+
let changed = true;
|
|
4048
|
+
while (changed) {
|
|
4049
|
+
changed = false;
|
|
4050
|
+
for (const [, info] of ctx.componentInfo) {
|
|
4051
|
+
if (info.warmPotential) continue;
|
|
4052
|
+
for (const name of info.warmDependencies) {
|
|
4053
|
+
if (ctx.componentInfo.get(name)?.warmPotential !== false) {
|
|
4054
|
+
info.warmPotential = true;
|
|
4055
|
+
changed = true;
|
|
4056
|
+
break;
|
|
4057
|
+
}
|
|
4058
|
+
}
|
|
4059
|
+
}
|
|
4060
|
+
}
|
|
4061
|
+
}
|
|
4062
|
+
|
|
4063
|
+
// Omitting a JSX descriptor also omits its live `Component.defaultProps` read.
|
|
4064
|
+
// Only private declarations whose every reference is an immediately rendered,
|
|
4065
|
+
// attribute-free host child can therefore bypass descriptor construction. JSX
|
|
4066
|
+
// descriptors returned/stored/passed elsewhere expose their `.type`, so those
|
|
4067
|
+
// sites count as escapes even though their tag looks equally static.
|
|
4068
|
+
function collectPrivateUnescapedComponents(body, ctx) {
|
|
4069
|
+
const candidates = new Set();
|
|
4070
|
+
for (const statement of body) {
|
|
4071
|
+
if (
|
|
4072
|
+
statement.type === 'FunctionDeclaration' &&
|
|
4073
|
+
statement.id?.type === 'Identifier' &&
|
|
4074
|
+
ctx.componentInfo.get(statement.id.name)?.returnJsx === true &&
|
|
4075
|
+
ctx.moduleFunctionDeclarations.has(statement.id.name)
|
|
4076
|
+
) {
|
|
4077
|
+
candidates.add(statement.id.name);
|
|
4078
|
+
}
|
|
4079
|
+
}
|
|
4080
|
+
if (candidates.size === 0) return candidates;
|
|
4081
|
+
|
|
4082
|
+
const allowed = new Set();
|
|
4083
|
+
const visitHost = (host) => {
|
|
4084
|
+
for (const child of host.children || []) {
|
|
4085
|
+
if (child?.type !== 'Element' && child?.type !== 'JSXElement') continue;
|
|
4086
|
+
if (!isComponentTag(child)) {
|
|
4087
|
+
visitHost(child);
|
|
4088
|
+
continue;
|
|
4089
|
+
}
|
|
4090
|
+
const name = tagBindingName(child);
|
|
4091
|
+
const attrs = child.attributes || child.openingElement?.attributes || [];
|
|
4092
|
+
if (name !== null && candidates.has(name) && attrs.length === 0 && !child.children?.length) {
|
|
4093
|
+
allowed.add(child);
|
|
4094
|
+
}
|
|
4095
|
+
}
|
|
4096
|
+
};
|
|
4097
|
+
for (const info of ctx.componentInfo.values()) {
|
|
4098
|
+
if (!info.returnJsx) continue;
|
|
4099
|
+
for (const statement of info.node.body.body || []) {
|
|
4100
|
+
if (statement.type === 'ReturnStatement' && isPlainHostRoot(statement.argument)) {
|
|
4101
|
+
visitHost(statement.argument);
|
|
4102
|
+
}
|
|
4103
|
+
}
|
|
4104
|
+
}
|
|
4105
|
+
|
|
4106
|
+
// An array deliberately does not introduce a synthetic module Block scope:
|
|
4107
|
+
// sibling function references must remain free for the escape proof.
|
|
4108
|
+
const escaped = collectFreeIdentifiers(body, [], allowed);
|
|
4109
|
+
for (const name of candidates) {
|
|
4110
|
+
if (escaped.has(name)) {
|
|
4111
|
+
candidates.delete(name);
|
|
4112
|
+
continue;
|
|
4113
|
+
}
|
|
4114
|
+
// A declaration binds its own name inside its body. Scan that disjoint
|
|
4115
|
+
// subtree once more so self-assigned properties cannot hide behind it.
|
|
4116
|
+
const declaration = ctx.moduleFunctionDeclarations.get(name);
|
|
4117
|
+
if (collectFreeIdentifiers(declaration.body, [], allowed).has(name)) candidates.delete(name);
|
|
4118
|
+
}
|
|
4119
|
+
return candidates;
|
|
4120
|
+
}
|
|
4121
|
+
|
|
3719
4122
|
// Conservative semantic boundary for compiler-owned component-region memoization.
|
|
3720
4123
|
// The cached region assumes React Compiler's pure-render / immutable-snapshot
|
|
3721
4124
|
// contract, but still fails closed for constructs whose commit or retry behavior
|
|
@@ -3764,11 +4167,12 @@ function containsDeferredRefRead(root) {
|
|
|
3764
4167
|
return found;
|
|
3765
4168
|
}
|
|
3766
4169
|
|
|
3767
|
-
//
|
|
3768
|
-
//
|
|
4170
|
+
// Imported component tags rendered anywhere in the body — the auto-memo
|
|
4171
|
+
// region's runtime witnesses. (Deferred-ref reads are no longer collected
|
|
4172
|
+
// body-wide here: they gate per call site and per local through
|
|
4173
|
+
// collectAutoMemoLocalHazards.)
|
|
3769
4174
|
function scanComponentBody(root, importedNames) {
|
|
3770
4175
|
const importedComponents = new Set();
|
|
3771
|
-
let readsDeferredRef = false;
|
|
3772
4176
|
const seen = new WeakSet();
|
|
3773
4177
|
function walk(node) {
|
|
3774
4178
|
if (!node || typeof node !== 'object') return;
|
|
@@ -3778,7 +4182,6 @@ function scanComponentBody(root, importedNames) {
|
|
|
3778
4182
|
}
|
|
3779
4183
|
if (seen.has(node)) return;
|
|
3780
4184
|
seen.add(node);
|
|
3781
|
-
if (!readsDeferredRef && isDeferredRefRead(node)) readsDeferredRef = true;
|
|
3782
4185
|
const imported = importedComponentTag(node, importedNames);
|
|
3783
4186
|
if (imported !== null) importedComponents.add(imported);
|
|
3784
4187
|
for (const key in node) {
|
|
@@ -3787,7 +4190,7 @@ function scanComponentBody(root, importedNames) {
|
|
|
3787
4190
|
}
|
|
3788
4191
|
}
|
|
3789
4192
|
walk(root);
|
|
3790
|
-
return { importedComponents
|
|
4193
|
+
return { importedComponents };
|
|
3791
4194
|
}
|
|
3792
4195
|
|
|
3793
4196
|
// A JSX member chain bottoms out at a JSXIdentifier, so the base test below
|
|
@@ -3823,9 +4226,137 @@ function containsImportedMemberRead(root, importedNames) {
|
|
|
3823
4226
|
return found;
|
|
3824
4227
|
}
|
|
3825
4228
|
|
|
4229
|
+
/**
|
|
4230
|
+
* Component locals whose values a region's dependency tuple cannot witness.
|
|
4231
|
+
*
|
|
4232
|
+
* A region dep on a LOCAL compares the local's per-render value. Locals
|
|
4233
|
+
* re-initialize on every body entry, so a plain value local — however it is
|
|
4234
|
+
* conditionally reassigned during render — is always an exact witness of
|
|
4235
|
+
* itself. What it CANNOT witness is a value drawn from mutable state whose
|
|
4236
|
+
* container identity hides the drift:
|
|
4237
|
+
*
|
|
4238
|
+
* - a ref read (`.current` / any computed member — `isDeferredRefRead`'s
|
|
4239
|
+
* definition): ref contents mutate outside render without scheduling one,
|
|
4240
|
+
* and a stable-identity closure (`useCallback(() => ref.current, [])`)
|
|
4241
|
+
* hands a skipped subtree a LIVE read whose changes nothing witnesses —
|
|
4242
|
+
* so the classification descends into nested function values;
|
|
4243
|
+
* - an imported binding's member read: a live import's property can move
|
|
4244
|
+
* while the import's identity (the only thing a dep can hold) stays
|
|
4245
|
+
* fixed.
|
|
4246
|
+
*
|
|
4247
|
+
* A name is tainted when such a read reaches its value through ANY feed —
|
|
4248
|
+
* declaration initializers at every nesting depth, destructuring patterns
|
|
4249
|
+
* that carry the read themselves (`const { current: el } = ref`, a default or
|
|
4250
|
+
* computed key reading a ref or live import), assignment right-hand sides, or
|
|
4251
|
+
* a for-of/for-in source — directly or through another tainted local (fixed
|
|
4252
|
+
* point, so declaration order and arrow-hoisted back-references cannot hide a
|
|
4253
|
+
* hazard). Nested and loop-scoped names are not component locals, so tainting
|
|
4254
|
+
* them is inert at call sites directly (unknown names already fail closed),
|
|
4255
|
+
* but it is what lets a hazard PROPAGATE through them into a top-level local
|
|
4256
|
+
* (`let out; for (const x of ref.current.items) out = x`). Shadowing is
|
|
4257
|
+
* folded by name — an inner binding's hazard taints its outer namesake —
|
|
4258
|
+
* which only ever declines more. Props params and same-module function
|
|
4259
|
+
* declarations never taint: the former are the witnessed snapshot itself, the
|
|
4260
|
+
* latter are immutable identities whose render-time CALLS the per-site
|
|
4261
|
+
* render-call gate already rejects.
|
|
4262
|
+
*
|
|
4263
|
+
* The direct forms of these reads inside a call site's own props are rejected
|
|
4264
|
+
* per site (containsAutoMemoUnsafeStructure / containsImportedMemberRead);
|
|
4265
|
+
* this set covers the same reads laundered through a local.
|
|
4266
|
+
*/
|
|
4267
|
+
function collectAutoMemoLocalHazards(stmts, importedNames) {
|
|
4268
|
+
const hazards = new Set();
|
|
4269
|
+
// name -> union of free identifiers across its clean value sources; a name
|
|
4270
|
+
// with a directly hazardous source moves to `hazards` and leaves this map.
|
|
4271
|
+
const sourceFrees = new Map();
|
|
4272
|
+
const feed = (pattern, rhs) => {
|
|
4273
|
+
if (!rhs) return;
|
|
4274
|
+
const names = new Set();
|
|
4275
|
+
collectBindings(pattern, names);
|
|
4276
|
+
if (names.size === 0) return;
|
|
4277
|
+
// The pattern side can carry the read itself: a `current`/computed
|
|
4278
|
+
// binding key destructures ref contents off the source, and a default's
|
|
4279
|
+
// expression evaluates at destructure time. Both walks descend the whole
|
|
4280
|
+
// pattern, so nested shapes and defaults are covered together.
|
|
4281
|
+
const direct =
|
|
4282
|
+
containsDeferredRefRead(rhs) ||
|
|
4283
|
+
containsImportedMemberRead(rhs, importedNames) ||
|
|
4284
|
+
containsDeferredRefRead(pattern) ||
|
|
4285
|
+
containsImportedMemberRead(pattern, importedNames);
|
|
4286
|
+
// Default expressions inside the pattern read values too; their free
|
|
4287
|
+
// identifiers (which include the pattern's own bound names — inert
|
|
4288
|
+
// self-loops) join the clean-source union.
|
|
4289
|
+
const frees = direct
|
|
4290
|
+
? null
|
|
4291
|
+
: [...collectFreeIdentifiers(rhs, []), ...collectFreeIdentifiers(pattern, [])];
|
|
4292
|
+
for (const name of names) {
|
|
4293
|
+
if (hazards.has(name)) continue;
|
|
4294
|
+
if (direct) {
|
|
4295
|
+
hazards.add(name);
|
|
4296
|
+
sourceFrees.delete(name);
|
|
4297
|
+
continue;
|
|
4298
|
+
}
|
|
4299
|
+
let set = sourceFrees.get(name);
|
|
4300
|
+
if (set === undefined) sourceFrees.set(name, (set = new Set()));
|
|
4301
|
+
for (const id of frees) set.add(id);
|
|
4302
|
+
}
|
|
4303
|
+
};
|
|
4304
|
+
// One walk feeds every binding form at every depth, including inside
|
|
4305
|
+
// nested function values: a deferred write targets a PREVIOUS render's
|
|
4306
|
+
// binding and can never leak into the next render's dep snapshot, but
|
|
4307
|
+
// walking uniformly is cheaper than proving which writes are deferred, and
|
|
4308
|
+
// only hazardous sources taint anyway.
|
|
4309
|
+
const seen = new WeakSet();
|
|
4310
|
+
(function walk(n) {
|
|
4311
|
+
if (!n) return;
|
|
4312
|
+
if (Array.isArray(n)) {
|
|
4313
|
+
for (const x of n) walk(x);
|
|
4314
|
+
return;
|
|
4315
|
+
}
|
|
4316
|
+
if (typeof n !== 'object' || !n.type || seen.has(n)) return;
|
|
4317
|
+
seen.add(n);
|
|
4318
|
+
if (n.type === 'VariableDeclaration') {
|
|
4319
|
+
for (const d of n.declarations || []) feed(d.id, d.init);
|
|
4320
|
+
} else if (n.type === 'AssignmentExpression' && n.left?.type !== 'MemberExpression') {
|
|
4321
|
+
feed(n.left, n.right);
|
|
4322
|
+
} else if ((n.type === 'ForOfStatement' || n.type === 'ForInStatement') && n.left) {
|
|
4323
|
+
// The loop source feeds the bound names whether the left declares
|
|
4324
|
+
// (`for (const x of src)` — declarator inits are null, so the
|
|
4325
|
+
// declaration arm above cannot see src) or reuses an outer binding.
|
|
4326
|
+
if (n.left.type === 'VariableDeclaration') {
|
|
4327
|
+
for (const d of n.left.declarations || []) feed(d.id, n.right);
|
|
4328
|
+
} else {
|
|
4329
|
+
feed(n.left, n.right);
|
|
4330
|
+
}
|
|
4331
|
+
}
|
|
4332
|
+
for (const key in n) {
|
|
4333
|
+
if (AST_WALK_SKIP_KEYS.has(key)) continue;
|
|
4334
|
+
walk(n[key]);
|
|
4335
|
+
}
|
|
4336
|
+
})(stmts);
|
|
4337
|
+
let changed = true;
|
|
4338
|
+
while (changed) {
|
|
4339
|
+
changed = false;
|
|
4340
|
+
for (const [name, free] of sourceFrees) {
|
|
4341
|
+
for (const id of free) {
|
|
4342
|
+
if (!hazards.has(id)) continue;
|
|
4343
|
+
hazards.add(name);
|
|
4344
|
+
sourceFrees.delete(name);
|
|
4345
|
+
changed = true;
|
|
4346
|
+
break;
|
|
4347
|
+
}
|
|
4348
|
+
}
|
|
4349
|
+
}
|
|
4350
|
+
return hazards;
|
|
4351
|
+
}
|
|
4352
|
+
|
|
3826
4353
|
function containsAutoMemoUnsafeStructure(stmts) {
|
|
3827
4354
|
let found = false;
|
|
3828
4355
|
const seen = new WeakSet();
|
|
4356
|
+
// Spread bags on HOST elements, marked admissible by the owning element's
|
|
4357
|
+
// visit below. Parents walk before their attributes, so membership is
|
|
4358
|
+
// decided before the spread node itself is reached.
|
|
4359
|
+
const hostSpreads = new WeakSet();
|
|
3829
4360
|
function walk(n) {
|
|
3830
4361
|
if (found || !n) return;
|
|
3831
4362
|
if (Array.isArray(n)) {
|
|
@@ -3857,13 +4388,29 @@ function containsAutoMemoUnsafeStructure(stmts) {
|
|
|
3857
4388
|
t === 'YieldExpression' ||
|
|
3858
4389
|
t === 'ThrowStatement' ||
|
|
3859
4390
|
t === 'TryStatement' ||
|
|
3860
|
-
t === 'JSXTryExpression'
|
|
3861
|
-
t === 'SpreadAttribute' ||
|
|
3862
|
-
t === 'JSXSpreadAttribute'
|
|
4391
|
+
t === 'JSXTryExpression'
|
|
3863
4392
|
) {
|
|
3864
4393
|
found = true;
|
|
3865
4394
|
return;
|
|
3866
4395
|
}
|
|
4396
|
+
if (t === 'SpreadAttribute' || t === 'JSXSpreadAttribute') {
|
|
4397
|
+
// A HOST element's spread bag is one setSpread binding re-diffed on every
|
|
4398
|
+
// region entry. Its argument is composed of deps the region guard already
|
|
4399
|
+
// witnesses (unwitnessed names fail closed independently), so a skip is
|
|
4400
|
+
// the no-op a re-entry would have been — the same immutable-snapshot
|
|
4401
|
+
// argument that admits `class={e.cls}` member reads; runtime keys the
|
|
4402
|
+
// walk cannot see (ref/on*/value) diff per entry with full parity to
|
|
4403
|
+
// their direct-attribute forms. A COMPONENT tag's spread instead builds
|
|
4404
|
+
// the child's props snapshot (getter evaluation, hidden prop names) and
|
|
4405
|
+
// stays rejected, matching the callSiteOk exclusion — which
|
|
4406
|
+
// isAutoMemoPropsParam's rest admission relies on. The argument still
|
|
4407
|
+
// walks below: accessors, computed members, and ref reads inside it keep
|
|
4408
|
+
// failing closed.
|
|
4409
|
+
if (!hostSpreads.has(n)) {
|
|
4410
|
+
found = true;
|
|
4411
|
+
return;
|
|
4412
|
+
}
|
|
4413
|
+
}
|
|
3867
4414
|
if (t === 'UnaryExpression' && n.operator === 'delete') {
|
|
3868
4415
|
found = true;
|
|
3869
4416
|
return;
|
|
@@ -3931,6 +4478,13 @@ function containsAutoMemoUnsafeStructure(stmts) {
|
|
|
3931
4478
|
found = true;
|
|
3932
4479
|
return;
|
|
3933
4480
|
}
|
|
4481
|
+
if (!isComponentTag(n)) {
|
|
4482
|
+
for (const attr of n.attributes || n.openingElement?.attributes || []) {
|
|
4483
|
+
if (attr.type === 'SpreadAttribute' || attr.type === 'JSXSpreadAttribute') {
|
|
4484
|
+
hostSpreads.add(attr);
|
|
4485
|
+
}
|
|
4486
|
+
}
|
|
4487
|
+
}
|
|
3934
4488
|
}
|
|
3935
4489
|
for (const key in n) {
|
|
3936
4490
|
if (AST_WALK_SKIP_KEYS.has(key)) continue;
|
|
@@ -3941,6 +4495,54 @@ function containsAutoMemoUnsafeStructure(stmts) {
|
|
|
3941
4495
|
return found;
|
|
3942
4496
|
}
|
|
3943
4497
|
|
|
4498
|
+
/**
|
|
4499
|
+
* Can this props parameter stand in for autoMemo's "one ordinary props
|
|
4500
|
+
* snapshot"? A bare Identifier always can. A destructuring ObjectPattern can
|
|
4501
|
+
* when destructuring it evaluates NO expressions and reads NOTHING mutable:
|
|
4502
|
+
* every binding it introduces is already a component local
|
|
4503
|
+
* (collectComponentLocals walks patterns), so the body's free-identifier and
|
|
4504
|
+
* capture analysis is complete for it — the pattern itself is the only part
|
|
4505
|
+
* those walks never see, because they take the body root, not the params.
|
|
4506
|
+
*
|
|
4507
|
+
* Fail closed on exactly the shapes whose evaluation escapes that analysis or
|
|
4508
|
+
* reads outside the snapshot:
|
|
4509
|
+
* - defaults (`{ a = expr }`): `expr` evaluates at destructure time, and its
|
|
4510
|
+
* reads are invisible to the body walks;
|
|
4511
|
+
* - computed keys (`{ [k]: v }`): same, for the key expression;
|
|
4512
|
+
* - a `current` binding at any depth: the same mutable-ref hazard the body
|
|
4513
|
+
* walk rejects for ObjectPattern declarations (a ref's identity is not a
|
|
4514
|
+
* complete witness for its contents);
|
|
4515
|
+
* - array patterns: destructuring runs the iterator protocol, which is
|
|
4516
|
+
* arbitrary user code on a non-Array prop;
|
|
4517
|
+
* - rest (`...rest`) with anything but a plain Identifier target.
|
|
4518
|
+
* Rest itself is admitted: object rest is an engine-level own-property copy of
|
|
4519
|
+
* the same snapshot, and spread-bearing call sites never receive a region
|
|
4520
|
+
* anyway (callSiteOk excludes them), so a getter-bearing props object cannot
|
|
4521
|
+
* reach a cached region.
|
|
4522
|
+
*/
|
|
4523
|
+
function isAutoMemoPropsParam(param) {
|
|
4524
|
+
if (param == null) return false;
|
|
4525
|
+
if (param.type === 'Identifier') return true;
|
|
4526
|
+
if (param.type !== 'ObjectPattern') return false;
|
|
4527
|
+
const admissible = (pattern) => {
|
|
4528
|
+
if (pattern == null || pattern.type !== 'ObjectPattern') return false;
|
|
4529
|
+
for (const p of pattern.properties || []) {
|
|
4530
|
+
if (p.type === 'RestElement') {
|
|
4531
|
+
if (p.argument?.type !== 'Identifier') return false;
|
|
4532
|
+
continue;
|
|
4533
|
+
}
|
|
4534
|
+
if (p.type !== 'Property' || p.computed) return false;
|
|
4535
|
+
const key = p.key?.name ?? p.key?.value;
|
|
4536
|
+
if (key === 'current') return false;
|
|
4537
|
+
const value = p.value;
|
|
4538
|
+
if (value?.type === 'Identifier') continue;
|
|
4539
|
+
if (!admissible(value)) return false;
|
|
4540
|
+
}
|
|
4541
|
+
return true;
|
|
4542
|
+
};
|
|
4543
|
+
return admissible(param);
|
|
4544
|
+
}
|
|
4545
|
+
|
|
3944
4546
|
/**
|
|
3945
4547
|
* Bundling MOVES an argument's evaluation out of the arrow body and into the
|
|
3946
4548
|
* component body, so it now runs on mount and on every update instead of once
|
|
@@ -4370,6 +4972,111 @@ function isSsrMarkerlessForItem(node) {
|
|
|
4370
4972
|
return jsxChildren.length === 1 && isPlainHostRoot(jsxChildren[0]);
|
|
4371
4973
|
}
|
|
4372
4974
|
|
|
4975
|
+
// Direct host-row mounting skips component-render bookkeeping. Keep the proof
|
|
4976
|
+
// narrower than markerless item rendering so opaque host lifecycles fail closed.
|
|
4977
|
+
const HOST_MOUNT_SAFE_TAGS = new Set([
|
|
4978
|
+
'a',
|
|
4979
|
+
'article',
|
|
4980
|
+
'aside',
|
|
4981
|
+
'b',
|
|
4982
|
+
'br',
|
|
4983
|
+
'div',
|
|
4984
|
+
'em',
|
|
4985
|
+
'footer',
|
|
4986
|
+
'h1',
|
|
4987
|
+
'h2',
|
|
4988
|
+
'h3',
|
|
4989
|
+
'h4',
|
|
4990
|
+
'h5',
|
|
4991
|
+
'h6',
|
|
4992
|
+
'header',
|
|
4993
|
+
'hr',
|
|
4994
|
+
'i',
|
|
4995
|
+
'li',
|
|
4996
|
+
'main',
|
|
4997
|
+
'nav',
|
|
4998
|
+
'ol',
|
|
4999
|
+
'p',
|
|
5000
|
+
'section',
|
|
5001
|
+
'small',
|
|
5002
|
+
'span',
|
|
5003
|
+
'strong',
|
|
5004
|
+
'table',
|
|
5005
|
+
'tbody',
|
|
5006
|
+
'td',
|
|
5007
|
+
'tfoot',
|
|
5008
|
+
'th',
|
|
5009
|
+
'thead',
|
|
5010
|
+
'tr',
|
|
5011
|
+
'ul',
|
|
5012
|
+
]);
|
|
5013
|
+
|
|
5014
|
+
const HOST_MOUNT_UNSAFE_ATTRIBUTES = new Set([
|
|
5015
|
+
'autofocus',
|
|
5016
|
+
'checked',
|
|
5017
|
+
'dangerouslysetinnerhtml',
|
|
5018
|
+
'defaultchecked',
|
|
5019
|
+
'defaultvalue',
|
|
5020
|
+
'form',
|
|
5021
|
+
'formaction',
|
|
5022
|
+
'formenctype',
|
|
5023
|
+
'formmethod',
|
|
5024
|
+
'formnovalidate',
|
|
5025
|
+
'formtarget',
|
|
5026
|
+
'innerhtml',
|
|
5027
|
+
'is',
|
|
5028
|
+
'multiple',
|
|
5029
|
+
'ref',
|
|
5030
|
+
'selected',
|
|
5031
|
+
'slot',
|
|
5032
|
+
'src',
|
|
5033
|
+
'srcset',
|
|
5034
|
+
'style',
|
|
5035
|
+
'value',
|
|
5036
|
+
]);
|
|
5037
|
+
|
|
5038
|
+
function isHostMountSafeTree(root) {
|
|
5039
|
+
const seen = new WeakSet();
|
|
5040
|
+
function walk(node) {
|
|
5041
|
+
if (node == null || typeof node !== 'object') return true;
|
|
5042
|
+
if (Array.isArray(node)) return node.every(walk);
|
|
5043
|
+
if (seen.has(node)) return true;
|
|
5044
|
+
seen.add(node);
|
|
5045
|
+
const type = node.type;
|
|
5046
|
+
if (
|
|
5047
|
+
type === 'ArrowFunctionExpression' ||
|
|
5048
|
+
type === 'FunctionExpression' ||
|
|
5049
|
+
type === 'FunctionDeclaration'
|
|
5050
|
+
) {
|
|
5051
|
+
// Delegated event callbacks run after the live row has mounted.
|
|
5052
|
+
return true;
|
|
5053
|
+
}
|
|
5054
|
+
if (type === 'JSXFragment' || type === 'Fragment') return false;
|
|
5055
|
+
if (type === 'Element' || type === 'JSXElement') {
|
|
5056
|
+
const tag = node.id || node.openingElement?.name;
|
|
5057
|
+
if (
|
|
5058
|
+
(tag?.type !== 'Identifier' && tag?.type !== 'JSXIdentifier') ||
|
|
5059
|
+
!HOST_MOUNT_SAFE_TAGS.has(tag.name)
|
|
5060
|
+
) {
|
|
5061
|
+
return false;
|
|
5062
|
+
}
|
|
5063
|
+
for (const attribute of node.attributes || node.openingElement?.attributes || []) {
|
|
5064
|
+
if (attribute.type !== 'Attribute' && attribute.type !== 'JSXAttribute') return false;
|
|
5065
|
+
const name = jsxAttrRawName(attribute);
|
|
5066
|
+
if (typeof name !== 'string' || HOST_MOUNT_UNSAFE_ATTRIBUTES.has(name.toLowerCase())) {
|
|
5067
|
+
return false;
|
|
5068
|
+
}
|
|
5069
|
+
}
|
|
5070
|
+
}
|
|
5071
|
+
for (const key in node) {
|
|
5072
|
+
if (AST_WALK_SKIP_KEYS.has(key)) continue;
|
|
5073
|
+
if (!walk(node[key])) return false;
|
|
5074
|
+
}
|
|
5075
|
+
return true;
|
|
5076
|
+
}
|
|
5077
|
+
return walk(root);
|
|
5078
|
+
}
|
|
5079
|
+
|
|
4373
5080
|
/**
|
|
4374
5081
|
* Whether a function can return from its own body before reaching its compiled
|
|
4375
5082
|
* output. Nested functions are separate execution scopes and do not affect the
|
|
@@ -6162,15 +6869,22 @@ function compileInternal(source, filename, options, analyzedAst, mode, bundlerMe
|
|
|
6162
6869
|
exactOrigins: new Map(), // see registerExactOrigin
|
|
6163
6870
|
originAliases: [], // see registerOriginAlias
|
|
6164
6871
|
hoistedTemplates: [], // { name, ast, html, ns, frag, origins }
|
|
6872
|
+
internedTemplates: new Map(), // HTML -> one template or namespace/fragment variants
|
|
6165
6873
|
hoistedHelpers: [], // statement NODES (sub-components, hook Symbols, key fns) + hook-slot-base markers
|
|
6166
6874
|
delegatedEvents: new Set(), // bubble event names seen in JSX — auto-emits delegateEvents(...)
|
|
6167
6875
|
capturedEvents: new Set(), // capture-phase event names (onXxxCapture) — auto-emits delegateCaptureEvents(...)
|
|
6876
|
+
unownedDelegatedEvents: new Set(),
|
|
6877
|
+
unownedCapturedEvents: new Set(),
|
|
6168
6878
|
cssInjections: [], // { hash, css } — one entry per component with a <style> block
|
|
6879
|
+
ownedCssInjections: new Set(),
|
|
6880
|
+
componentOwners: [],
|
|
6881
|
+
currentComponentOwner: null,
|
|
6169
6882
|
currentComponentLocals: null, // Set<string> while compiling a component body; null otherwise
|
|
6170
6883
|
currentMapTemps: null, // receiver/method temps owned by the current emitted function
|
|
6171
6884
|
currentAutoMemoOffset: 0, // flat compiler-cache cell offset for the body being emitted
|
|
6172
6885
|
currentAutoMemoCacheName: null, // collision-free local bound to the body's cache array
|
|
6173
6886
|
currentAutoMemoCommittedName: null, // committed cache snapshot (copy-on-write source)
|
|
6887
|
+
currentAutoCalculatedRenderableRefs: null, // proven non-escaping calculation holes, inherited by lexical child bodies
|
|
6174
6888
|
nextAutoMemoCacheId: 0, // unique non-index slots property per compiled render function
|
|
6175
6889
|
inlineHookMemo: inlineHookMemoEnabled, // de-callbacked useMemo/useCallback + pu creations
|
|
6176
6890
|
_puInlineLowering: false, // true only while a body pipeline ends in inlineHookMemoPass
|
|
@@ -6375,8 +7089,54 @@ function compileInternal(source, filename, options, analyzedAst, mode, bundlerMe
|
|
|
6375
7089
|
ctx.moduleFunctionDeclarations = collectImmutableModuleFunctions(ast.body);
|
|
6376
7090
|
// M3 inherit-range exclusion set (see inheritSoleCompRoot).
|
|
6377
7091
|
ctx._octaneBoundaryNames = collectOctaneBoundaryNames(ast.body);
|
|
6378
|
-
|
|
6379
|
-
|
|
7092
|
+
const productionEffects = !ctx.hmr && !ctx.dev && !ctx.profile;
|
|
7093
|
+
const exportedComponents = ast.body.filter(
|
|
7094
|
+
(statement) =>
|
|
7095
|
+
(statement.type === 'ExportNamedDeclaration' ||
|
|
7096
|
+
statement.type === 'ExportDefaultDeclaration') &&
|
|
7097
|
+
isComponentFunction(statement.declaration),
|
|
7098
|
+
);
|
|
7099
|
+
// Prelude registration historically precedes every authored module effect.
|
|
7100
|
+
// An effect above any component could observe its stylesheet, delegated
|
|
7101
|
+
// listeners, or transition capability, so retain the entire module prelude.
|
|
7102
|
+
let effectBeforeComponent = false;
|
|
7103
|
+
let precedingModuleEffect = false;
|
|
7104
|
+
if (productionEffects && exportedComponents.length > 1) {
|
|
7105
|
+
for (const statement of ast.body) {
|
|
7106
|
+
const declaration =
|
|
7107
|
+
statement.type === 'ExportNamedDeclaration' || statement.type === 'ExportDefaultDeclaration'
|
|
7108
|
+
? statement.declaration
|
|
7109
|
+
: statement;
|
|
7110
|
+
if (isComponentFunction(declaration)) {
|
|
7111
|
+
if (precedingModuleEffect) {
|
|
7112
|
+
effectBeforeComponent = true;
|
|
7113
|
+
break;
|
|
7114
|
+
}
|
|
7115
|
+
continue;
|
|
7116
|
+
}
|
|
7117
|
+
if (
|
|
7118
|
+
statement.type === 'ImportDeclaration' ||
|
|
7119
|
+
statement.type === 'ExportAllDeclaration' ||
|
|
7120
|
+
(statement.type === 'ExportNamedDeclaration' && declaration == null) ||
|
|
7121
|
+
declaration?.type === 'FunctionDeclaration' ||
|
|
7122
|
+
declaration?.type === 'TSInterfaceDeclaration' ||
|
|
7123
|
+
declaration?.type === 'TSTypeAliasDeclaration' ||
|
|
7124
|
+
statement.type === 'EmptyStatement'
|
|
7125
|
+
) {
|
|
7126
|
+
continue;
|
|
7127
|
+
}
|
|
7128
|
+
precedingModuleEffect = true;
|
|
7129
|
+
}
|
|
7130
|
+
}
|
|
7131
|
+
ctx.componentEffectOwnership =
|
|
7132
|
+
productionEffects && exportedComponents.length > 1 && !effectBeforeComponent;
|
|
7133
|
+
const viewTransitions = classifyViewTransitionOwnership(
|
|
7134
|
+
ast.body,
|
|
7135
|
+
productionEffects,
|
|
7136
|
+
ctx.componentEffectOwnership,
|
|
7137
|
+
);
|
|
7138
|
+
ctx._usesViewTransition = viewTransitions.global;
|
|
7139
|
+
ctx.viewTransitionOwners = viewTransitions.owners;
|
|
6380
7140
|
|
|
6381
7141
|
// List of exported components needing HMR wrapping. Each entry: { name,
|
|
6382
7142
|
// exportKind: 'default' | 'named' }. We emit the `Comp = hmr(Comp)` lines
|
|
@@ -6415,6 +7175,7 @@ function compileInternal(source, filename, options, analyzedAst, mode, bundlerMe
|
|
|
6415
7175
|
eligible: false,
|
|
6416
7176
|
autoMemoSafe: false,
|
|
6417
7177
|
autoMemoCallsitesSafe: true,
|
|
7178
|
+
autoMemoLocalHazards: null,
|
|
6418
7179
|
autoMemoCaptures: [],
|
|
6419
7180
|
autoMemoComponentDeps: [],
|
|
6420
7181
|
autoMemoImportedComponents: [],
|
|
@@ -6425,6 +7186,19 @@ function compileInternal(source, filename, options, analyzedAst, mode, bundlerMe
|
|
|
6425
7186
|
});
|
|
6426
7187
|
}
|
|
6427
7188
|
}
|
|
7189
|
+
// Return-based JSX functions never attach fetch-tree warm plans, and a sole
|
|
7190
|
+
// same-module component already rejects a self-only warm edge below. Avoid a
|
|
7191
|
+
// whole-module graph walk unless a compiled-void body can actually reach a
|
|
7192
|
+
// distinct same-module declaration.
|
|
7193
|
+
if (
|
|
7194
|
+
ctx.componentInfo.size > 1 &&
|
|
7195
|
+
[...ctx.componentInfo.values()].some((info) => info.node.body?.type === 'JSXCodeBlock')
|
|
7196
|
+
) {
|
|
7197
|
+
classifySameModuleWarmPotential(ctx);
|
|
7198
|
+
}
|
|
7199
|
+
ctx.privateUnescapedComponents = ctx.autoMemo
|
|
7200
|
+
? collectPrivateUnescapedComponents(ast.body, ctx)
|
|
7201
|
+
: new Set();
|
|
6428
7202
|
for (const [, info] of ctx.componentInfo) {
|
|
6429
7203
|
const compNode = info.node;
|
|
6430
7204
|
const locals = collectComponentLocals(compNode);
|
|
@@ -6434,16 +7208,26 @@ function compileInternal(source, filename, options, analyzedAst, mode, bundlerMe
|
|
|
6434
7208
|
if (compNode.body.render) stmts.push(compNode.body.render);
|
|
6435
7209
|
const root = b.block(stmts);
|
|
6436
7210
|
const free = collectFreeIdentifiers(root, locals);
|
|
6437
|
-
const { importedComponents: autoMemoImportedComponents
|
|
7211
|
+
const { importedComponents: autoMemoImportedComponents } = scanComponentBody(
|
|
6438
7212
|
root,
|
|
6439
7213
|
ctx.importedNames,
|
|
6440
7214
|
);
|
|
6441
|
-
//
|
|
6442
|
-
// short-circuits ahead of
|
|
7215
|
+
// The callee-side proof below asks this of the same root. Kept lazy because
|
|
7216
|
+
// the short-circuits ahead of its use skip it entirely for some components.
|
|
6443
7217
|
let importedMemberRead = null;
|
|
6444
7218
|
const readsImportedMember = () =>
|
|
6445
7219
|
(importedMemberRead ??= containsImportedMemberRead(root, ctx.importedNames));
|
|
6446
|
-
|
|
7220
|
+
// Call-site regions inside this body are gated per site: the site's own
|
|
7221
|
+
// props are walked directly (containsAutoMemoUnsafeStructure /
|
|
7222
|
+
// containsImportedMemberRead over the JSX node), and ref/imported-member
|
|
7223
|
+
// reads laundered through a body local are carried by this hazard set —
|
|
7224
|
+
// a site whose free identifiers touch a tainted local falls back. The
|
|
7225
|
+
// free-name loop below still vetoes the whole body for names no dep can
|
|
7226
|
+
// witness at all (boundary wrappers, namespaces, ambient module state).
|
|
7227
|
+
const autoMemoLocalHazards = ctx.autoMemo
|
|
7228
|
+
? collectAutoMemoLocalHazards(stmts, ctx.importedNames)
|
|
7229
|
+
: null;
|
|
7230
|
+
let autoMemoCallsitesSafe = true;
|
|
6447
7231
|
for (const name of free) {
|
|
6448
7232
|
if (
|
|
6449
7233
|
ctx._octaneBoundaryNames.has(name) ||
|
|
@@ -6457,13 +7241,14 @@ function compileInternal(source, filename, options, analyzedAst, mode, bundlerMe
|
|
|
6457
7241
|
break;
|
|
6458
7242
|
}
|
|
6459
7243
|
}
|
|
6460
|
-
// autoMemo
|
|
6461
|
-
//
|
|
6462
|
-
//
|
|
6463
|
-
//
|
|
7244
|
+
// autoMemo models a pure component as a function of one ordinary props
|
|
7245
|
+
// snapshot. A destructuring param is that same snapshot read once at
|
|
7246
|
+
// entry, admitted only while the pattern evaluates no expressions of its
|
|
7247
|
+
// own — see isAutoMemoPropsParam for the exact fail-closed shapes
|
|
7248
|
+
// (defaults, computed keys, `current`, array patterns).
|
|
6464
7249
|
const ordinaryPropsParam =
|
|
6465
7250
|
(compNode.params?.length ?? 0) <= 1 &&
|
|
6466
|
-
(compNode.params?.length !== 1 || compNode.params[0]
|
|
7251
|
+
(compNode.params?.length !== 1 || isAutoMemoPropsParam(compNode.params[0]));
|
|
6467
7252
|
let autoMemoSafe =
|
|
6468
7253
|
ordinaryPropsParam &&
|
|
6469
7254
|
!containsRenderCall(stmts, ctx) &&
|
|
@@ -6501,6 +7286,7 @@ function compileInternal(source, filename, options, analyzedAst, mode, bundlerMe
|
|
|
6501
7286
|
}
|
|
6502
7287
|
info.autoMemoSafe = autoMemoSafe;
|
|
6503
7288
|
info.autoMemoCallsitesSafe = autoMemoCallsitesSafe;
|
|
7289
|
+
info.autoMemoLocalHazards = autoMemoLocalHazards;
|
|
6504
7290
|
info.autoMemoCaptures = autoMemoSafe ? autoMemoCaptures.sort() : [];
|
|
6505
7291
|
info.autoMemoComponentDeps = autoMemoSafe ? autoMemoComponentDeps.sort() : [];
|
|
6506
7292
|
info.autoMemoImportedComponents = autoMemoSafe ? [...autoMemoImportedComponents].sort() : [];
|
|
@@ -6736,6 +7522,8 @@ function compileInternal(source, filename, options, analyzedAst, mode, bundlerMe
|
|
|
6736
7522
|
}
|
|
6737
7523
|
}
|
|
6738
7524
|
|
|
7525
|
+
finalizeComponentInitializers(ctx, bodyNodes);
|
|
7526
|
+
|
|
6739
7527
|
// Auto-emit delegateEvents([...]) / delegateCaptureEvents([...]) once at module
|
|
6740
7528
|
// scope for every (bubble / capture) event seen.
|
|
6741
7529
|
if (ctx.delegatedEvents.size > 0) {
|
|
@@ -6777,22 +7565,24 @@ function compileInternal(source, filename, options, analyzedAst, mode, bundlerMe
|
|
|
6777
7565
|
),
|
|
6778
7566
|
);
|
|
6779
7567
|
}
|
|
6780
|
-
const styleNodes = ctx.cssInjections
|
|
6781
|
-
|
|
6782
|
-
|
|
6783
|
-
|
|
6784
|
-
|
|
6785
|
-
|
|
6786
|
-
|
|
6787
|
-
|
|
6788
|
-
|
|
6789
|
-
b.
|
|
6790
|
-
|
|
7568
|
+
const styleNodes = ctx.cssInjections
|
|
7569
|
+
.filter((i) => !ctx.ownedCssInjections.has(i))
|
|
7570
|
+
.map((i) => {
|
|
7571
|
+
// Point the authored `<style>` block at the CSS this injection carries.
|
|
7572
|
+
// Both sides are whole units — the block and the stylesheet it became —
|
|
7573
|
+
// which is the useful pairing for a scoped style.
|
|
7574
|
+
const anchor = claimCssOrigins(ctx, i);
|
|
7575
|
+
return inheritOriginLoc(
|
|
7576
|
+
b.stmt(
|
|
7577
|
+
b.call(
|
|
7578
|
+
'_$injectStyle',
|
|
7579
|
+
b.literal(i.hash, JSON.stringify(i.hash)),
|
|
7580
|
+
b.literal(i.css, JSON.stringify(i.css)),
|
|
7581
|
+
),
|
|
6791
7582
|
),
|
|
6792
|
-
|
|
6793
|
-
|
|
6794
|
-
);
|
|
6795
|
-
});
|
|
7583
|
+
anchor ?? moduleOrigin,
|
|
7584
|
+
);
|
|
7585
|
+
});
|
|
6796
7586
|
const templateNodes = ctx.hoistedTemplates.map((t) => {
|
|
6797
7587
|
const args = [b.literal(t.html, JSON.stringify(t.html))];
|
|
6798
7588
|
if (t.ns || t.frag) args.push(b.literal(t.ns | 0));
|
|
@@ -10152,6 +10942,103 @@ function singleRootInitializer(ctx, component) {
|
|
|
10152
10942
|
return markPure(b.call('_$__s', component));
|
|
10153
10943
|
}
|
|
10154
10944
|
|
|
10945
|
+
function finalizeComponentInitializers(ctx, bodyNodes) {
|
|
10946
|
+
if (!ctx.componentEffectOwnership || ctx.componentOwners.length === 0) return;
|
|
10947
|
+
|
|
10948
|
+
const delegatedOwners = new Map();
|
|
10949
|
+
const capturedOwners = new Map();
|
|
10950
|
+
const ownableStyles = new Set();
|
|
10951
|
+
for (const owner of ctx.componentOwners) {
|
|
10952
|
+
for (const event of owner.delegatedEvents) {
|
|
10953
|
+
delegatedOwners.set(event, (delegatedOwners.get(event) ?? 0) + 1);
|
|
10954
|
+
}
|
|
10955
|
+
for (const event of owner.capturedEvents) {
|
|
10956
|
+
capturedOwners.set(event, (capturedOwners.get(event) ?? 0) + 1);
|
|
10957
|
+
}
|
|
10958
|
+
for (const style of owner.styles) ownableStyles.add(style);
|
|
10959
|
+
}
|
|
10960
|
+
// The prelude emitted all stylesheets in authored order. Moving only owned
|
|
10961
|
+
// sheets behind an unowned style map or local component reverses that cascade.
|
|
10962
|
+
const preserveModuleStyleOrder = ctx.cssInjections.some((style) => !ownableStyles.has(style));
|
|
10963
|
+
|
|
10964
|
+
const replacements = new Map();
|
|
10965
|
+
for (const owner of ctx.componentOwners) {
|
|
10966
|
+
const calls = [];
|
|
10967
|
+
if (owner.transition && !ctx._usesViewTransition) {
|
|
10968
|
+
ctx.runtimeNeeded.add('__vtSeen');
|
|
10969
|
+
calls.push(b.call(rtAlias('__vtSeen')));
|
|
10970
|
+
}
|
|
10971
|
+
|
|
10972
|
+
const delegated = [...owner.delegatedEvents]
|
|
10973
|
+
.filter((event) => delegatedOwners.get(event) === 1 && !ctx.unownedDelegatedEvents.has(event))
|
|
10974
|
+
.sort();
|
|
10975
|
+
if (delegated.length !== 0) {
|
|
10976
|
+
for (const event of delegated) ctx.delegatedEvents.delete(event);
|
|
10977
|
+
ctx.runtimeNeeded.add('delegateEvents');
|
|
10978
|
+
calls.push(
|
|
10979
|
+
b.call(
|
|
10980
|
+
'_$delegateEvents',
|
|
10981
|
+
b.array(delegated.map((event) => b.literal(event, JSON.stringify(event)))),
|
|
10982
|
+
),
|
|
10983
|
+
);
|
|
10984
|
+
}
|
|
10985
|
+
|
|
10986
|
+
const captured = [...owner.capturedEvents]
|
|
10987
|
+
.filter((event) => capturedOwners.get(event) === 1 && !ctx.unownedCapturedEvents.has(event))
|
|
10988
|
+
.sort();
|
|
10989
|
+
if (captured.length !== 0) {
|
|
10990
|
+
for (const event of captured) ctx.capturedEvents.delete(event);
|
|
10991
|
+
ctx.runtimeNeeded.add('delegateCaptureEvents');
|
|
10992
|
+
calls.push(
|
|
10993
|
+
b.call(
|
|
10994
|
+
'_$delegateCaptureEvents',
|
|
10995
|
+
b.array(captured.map((event) => b.literal(event, JSON.stringify(event)))),
|
|
10996
|
+
),
|
|
10997
|
+
);
|
|
10998
|
+
}
|
|
10999
|
+
|
|
11000
|
+
if (!preserveModuleStyleOrder) {
|
|
11001
|
+
for (const style of owner.styles) {
|
|
11002
|
+
ctx.ownedCssInjections.add(style);
|
|
11003
|
+
const origin = claimCssOrigins(ctx, style);
|
|
11004
|
+
calls.push(
|
|
11005
|
+
inheritOriginLoc(
|
|
11006
|
+
b.call(
|
|
11007
|
+
'_$injectStyle',
|
|
11008
|
+
b.literal(style.hash, JSON.stringify(style.hash)),
|
|
11009
|
+
b.literal(style.css, JSON.stringify(style.css)),
|
|
11010
|
+
),
|
|
11011
|
+
origin ?? owner.origin,
|
|
11012
|
+
),
|
|
11013
|
+
);
|
|
11014
|
+
}
|
|
11015
|
+
}
|
|
11016
|
+
if (calls.length === 0) continue;
|
|
11017
|
+
|
|
11018
|
+
const declarator = owner.declaration.declarations[0];
|
|
11019
|
+
const initializer = inheritOriginLoc(
|
|
11020
|
+
markPure(b.call(b.arrow([], b.sequence([...calls, declarator.init])))),
|
|
11021
|
+
owner.origin,
|
|
11022
|
+
);
|
|
11023
|
+
replacements.set(owner.declaration, {
|
|
11024
|
+
...owner.declaration,
|
|
11025
|
+
declarations: [{ ...declarator, init: initializer }],
|
|
11026
|
+
});
|
|
11027
|
+
}
|
|
11028
|
+
|
|
11029
|
+
if (replacements.size === 0) return;
|
|
11030
|
+
for (let index = 0; index < bodyNodes.length; index++) {
|
|
11031
|
+
const statement = bodyNodes[index];
|
|
11032
|
+
const replacement = replacements.get(statement);
|
|
11033
|
+
if (replacement !== undefined) {
|
|
11034
|
+
bodyNodes[index] = replacement;
|
|
11035
|
+
} else if (statement.type === 'ExportNamedDeclaration') {
|
|
11036
|
+
const declaration = replacements.get(statement.declaration);
|
|
11037
|
+
if (declaration !== undefined) bodyNodes[index] = { ...statement, declaration };
|
|
11038
|
+
}
|
|
11039
|
+
}
|
|
11040
|
+
}
|
|
11041
|
+
|
|
10155
11042
|
function compileComponent(node, ctx, options) {
|
|
10156
11043
|
const name = node.id.name;
|
|
10157
11044
|
rejectAsyncOrGenerator(node, name);
|
|
@@ -10160,6 +11047,19 @@ function compileComponent(node, ctx, options) {
|
|
|
10160
11047
|
const isDefault = !!node.default;
|
|
10161
11048
|
const hmrWrap = !!(options && options.hmrWrap);
|
|
10162
11049
|
const returnedOutput = node.body?.type === 'JSXCodeBlock' && hasOwnValueReturn(node);
|
|
11050
|
+
const owner =
|
|
11051
|
+
ctx.componentEffectOwnership && isExported
|
|
11052
|
+
? {
|
|
11053
|
+
name,
|
|
11054
|
+
origin: node,
|
|
11055
|
+
delegatedEvents: new Set(),
|
|
11056
|
+
capturedEvents: new Set(),
|
|
11057
|
+
styles: [],
|
|
11058
|
+
transition: ctx.viewTransitionOwners.has(name),
|
|
11059
|
+
declaration: null,
|
|
11060
|
+
}
|
|
11061
|
+
: null;
|
|
11062
|
+
const firstStyle = ctx.cssInjections.length;
|
|
10163
11063
|
|
|
10164
11064
|
// Scoped `<style>` block. New TSRX surfaces each style block as a
|
|
10165
11065
|
// `JSXStyleElement` child of the rendered tree (parser pre-computes the
|
|
@@ -10189,11 +11089,15 @@ function compileComponent(node, ctx, options) {
|
|
|
10189
11089
|
// locals.
|
|
10190
11090
|
const prevLocals = ctx.currentComponentLocals;
|
|
10191
11091
|
const prevAutoMemoCallsitesSafe = ctx.currentAutoMemoCallsitesSafe;
|
|
11092
|
+
const prevAutoMemoLocalHazards = ctx.currentAutoMemoLocalHazards;
|
|
10192
11093
|
const prevKnownStr = ctx.knownStringLocals;
|
|
10193
11094
|
const prevProfileComponentId = ctx.currentProfileComponentId;
|
|
11095
|
+
const previousComponentOwner = ctx.currentComponentOwner;
|
|
10194
11096
|
ctx.currentComponentLocals = collectComponentLocals(node);
|
|
10195
11097
|
ctx.currentAutoMemoCallsitesSafe = ctx.componentInfo.get(name)?.autoMemoCallsitesSafe !== false;
|
|
11098
|
+
ctx.currentAutoMemoLocalHazards = ctx.componentInfo.get(name)?.autoMemoLocalHazards ?? null;
|
|
10196
11099
|
ctx.knownStringLocals = collectKnownStringLocals(node);
|
|
11100
|
+
ctx.currentComponentOwner = owner;
|
|
10197
11101
|
if (ctx.profile) ctx.currentProfileComponentId = profileComponentId(ctx, name, node);
|
|
10198
11102
|
let fnNode;
|
|
10199
11103
|
try {
|
|
@@ -10209,9 +11113,12 @@ function compileComponent(node, ctx, options) {
|
|
|
10209
11113
|
} finally {
|
|
10210
11114
|
ctx.currentComponentLocals = prevLocals;
|
|
10211
11115
|
ctx.currentAutoMemoCallsitesSafe = prevAutoMemoCallsitesSafe;
|
|
11116
|
+
ctx.currentAutoMemoLocalHazards = prevAutoMemoLocalHazards;
|
|
10212
11117
|
ctx.knownStringLocals = prevKnownStr;
|
|
10213
11118
|
ctx.currentProfileComponentId = prevProfileComponentId;
|
|
11119
|
+
ctx.currentComponentOwner = previousComponentOwner;
|
|
10214
11120
|
}
|
|
11121
|
+
if (owner !== null) owner.styles = ctx.cssInjections.slice(firstStyle);
|
|
10215
11122
|
|
|
10216
11123
|
// Parallel-use warm plan: attached to the INNER function object (not the
|
|
10217
11124
|
// module const) so the component's own body — where the function-
|
|
@@ -10252,6 +11159,11 @@ function compileComponent(node, ctx, options) {
|
|
|
10252
11159
|
sourceBeforeNode !== '' &&
|
|
10253
11160
|
new RegExp(`\\b${name.replace(/\$/g, '\\$')}\\b`).test(sourceBeforeNode);
|
|
10254
11161
|
if (referencedAboveDeclaration) {
|
|
11162
|
+
if (owner !== null) {
|
|
11163
|
+
for (const event of owner.delegatedEvents) ctx.unownedDelegatedEvents.add(event);
|
|
11164
|
+
for (const event of owner.capturedEvents) ctx.unownedCapturedEvents.add(event);
|
|
11165
|
+
if (owner.transition) ctx._usesViewTransition = true;
|
|
11166
|
+
}
|
|
10255
11167
|
// Every stamp is `typeof`-guarded: route code-splitters (TanStack's) may
|
|
10256
11168
|
// EXTRACT the declaration into its own module and leave these statements
|
|
10257
11169
|
// behind — `typeof` on the then-undeclared identifier short-circuits
|
|
@@ -10341,6 +11253,10 @@ function compileComponent(node, ctx, options) {
|
|
|
10341
11253
|
b.declaration(declKind, [b.declarator(b.id(name, node.id ?? node), valueExpr)]),
|
|
10342
11254
|
node,
|
|
10343
11255
|
);
|
|
11256
|
+
if (owner !== null) {
|
|
11257
|
+
owner.declaration = declNode;
|
|
11258
|
+
ctx.componentOwners.push(owner);
|
|
11259
|
+
}
|
|
10344
11260
|
if (isDefault) {
|
|
10345
11261
|
if (options && options.hmrMutable) {
|
|
10346
11262
|
return {
|
|
@@ -10371,6 +11287,8 @@ function compileComponent(node, ctx, options) {
|
|
|
10371
11287
|
*/
|
|
10372
11288
|
function compileFunctionBody(node, ctx, name, parentNs = 'html', cssHash = null, options = null) {
|
|
10373
11289
|
const returnedOutput = options?.returnedOutput === true;
|
|
11290
|
+
const previousAutoCalculatedRenderableRefs = ctx.currentAutoCalculatedRenderableRefs;
|
|
11291
|
+
let autoCalculatedDeclarations = null;
|
|
10374
11292
|
const prevMapTemps = ctx.currentMapTemps;
|
|
10375
11293
|
const mapTemps = [];
|
|
10376
11294
|
ctx.currentMapTemps = mapTemps;
|
|
@@ -10444,7 +11362,8 @@ function compileFunctionBody(node, ctx, name, parentNs = 'html', cssHash = null,
|
|
|
10444
11362
|
);
|
|
10445
11363
|
}
|
|
10446
11364
|
workingStatements = removeMountEventCallbackDeclarations(statements, mountCallbackSinks).map(
|
|
10447
|
-
(s) =>
|
|
11365
|
+
(s) =>
|
|
11366
|
+
rewriteAutoCallback(s, stableSet, ctx.currentComponentLocals, ctx, bodyInvariantLocals),
|
|
10448
11367
|
);
|
|
10449
11368
|
}
|
|
10450
11369
|
if (returnedOutput) {
|
|
@@ -10496,9 +11415,21 @@ function compileFunctionBody(node, ctx, name, parentNs = 'html', cssHash = null,
|
|
|
10496
11415
|
if (ctx.currentComponentLocals && ctx.mode !== 'server') {
|
|
10497
11416
|
const renderReadNames = collectRenderReadNames(jsxNodes, ctx);
|
|
10498
11417
|
if (renderReadNames.size > 0) {
|
|
10499
|
-
workingStatements = workingStatements.map((statement) =>
|
|
10500
|
-
|
|
10501
|
-
|
|
11418
|
+
workingStatements = workingStatements.map((statement) => {
|
|
11419
|
+
const rewritten = rewriteAutoCalculation(
|
|
11420
|
+
statement,
|
|
11421
|
+
ctx.currentComponentLocals,
|
|
11422
|
+
renderReadNames,
|
|
11423
|
+
ctx,
|
|
11424
|
+
);
|
|
11425
|
+
if (ctx.autoMemo && rewritten !== statement) {
|
|
11426
|
+
(autoCalculatedDeclarations ??= new Map()).set(
|
|
11427
|
+
statement.declarations[0].id.name,
|
|
11428
|
+
statement,
|
|
11429
|
+
);
|
|
11430
|
+
}
|
|
11431
|
+
return rewritten;
|
|
11432
|
+
});
|
|
10502
11433
|
}
|
|
10503
11434
|
}
|
|
10504
11435
|
workingStatements = parallelUseMemoizePass(workingStatements, ctx, name, creations, [], null);
|
|
@@ -10607,6 +11538,19 @@ function compileFunctionBody(node, ctx, name, parentNs = 'html', cssHash = null,
|
|
|
10607
11538
|
// node itself always does.
|
|
10608
11539
|
const prevFnOrigin = ctx._fnOrigin;
|
|
10609
11540
|
ctx._fnOrigin = node.loc ? node : (node.id ?? prevFnOrigin);
|
|
11541
|
+
if (autoCalculatedDeclarations !== null) {
|
|
11542
|
+
const refs = collectAutoCalculatedRenderableRefs(
|
|
11543
|
+
statements,
|
|
11544
|
+
jsxNodes,
|
|
11545
|
+
autoCalculatedDeclarations,
|
|
11546
|
+
);
|
|
11547
|
+
if (refs !== null) {
|
|
11548
|
+
ctx.currentAutoCalculatedRenderableRefs = {
|
|
11549
|
+
nodes: refs,
|
|
11550
|
+
parent: previousAutoCalculatedRenderableRefs,
|
|
11551
|
+
};
|
|
11552
|
+
}
|
|
11553
|
+
}
|
|
10610
11554
|
// Located origin for the function SHELL itself: synthetic helper shapes
|
|
10611
11555
|
// (hoisted @for bodies, `__tsrx$N` sub-templates) carry no loc of their
|
|
10612
11556
|
// own — their scaffolding inherits the nearest located enclosing origin.
|
|
@@ -10639,6 +11583,7 @@ function compileFunctionBody(node, ctx, name, parentNs = 'html', cssHash = null,
|
|
|
10639
11583
|
ctx.currentBodyIsComponentScope = prevBodyIsComponentScope;
|
|
10640
11584
|
ctx._inheritBody = prevInheritBody;
|
|
10641
11585
|
ctx._fnOrigin = prevFnOrigin;
|
|
11586
|
+
ctx.currentAutoCalculatedRenderableRefs = previousAutoCalculatedRenderableRefs;
|
|
10642
11587
|
ctx._foldedDirectiveCalls = prevFDC;
|
|
10643
11588
|
ctx._valueDirectiveLowering = prevValueDirectiveLowering;
|
|
10644
11589
|
|
|
@@ -12390,6 +13335,12 @@ function buildWarmArtifacts(node, ctx, componentName, creations, warmChildren) {
|
|
|
12390
13335
|
const warmKids = warmChildren.filter(
|
|
12391
13336
|
(w) =>
|
|
12392
13337
|
guardOk(w.guards, w.locals) &&
|
|
13338
|
+
// Server and universal-renderer warm plans have independent compilation
|
|
13339
|
+
// contexts. Only the DOM client's closed same-module graph can prove
|
|
13340
|
+
// that this edge and every descendant are synchronously render-only.
|
|
13341
|
+
(ctx.mode === 'server' ||
|
|
13342
|
+
ctx._universalRuntimeUnit != null ||
|
|
13343
|
+
ctx.componentInfo.get(w.compName)?.warmPotential !== false) &&
|
|
12393
13344
|
!paramNames.has(w.compName) &&
|
|
12394
13345
|
!(locals && locals.has(w.compName)) &&
|
|
12395
13346
|
!(w.locals && w.locals.has(w.compName)) &&
|
|
@@ -13051,7 +14002,17 @@ function authoredHookMemoOf(stmt) {
|
|
|
13051
14002
|
if (containsHookShapedCall(fn.body)) return null;
|
|
13052
14003
|
if (fn.body.type === 'BlockStatement' && !blockBodyInlineSafe(fn.body)) return null;
|
|
13053
14004
|
}
|
|
13054
|
-
return {
|
|
14005
|
+
return {
|
|
14006
|
+
name,
|
|
14007
|
+
decl,
|
|
14008
|
+
kind: stmt.kind,
|
|
14009
|
+
fn,
|
|
14010
|
+
deps,
|
|
14011
|
+
generatedInvariant:
|
|
14012
|
+
name === 'useCallback' &&
|
|
14013
|
+
callee._octaneGenerated === true &&
|
|
14014
|
+
callee._octaneLifetimeInvariant === true,
|
|
14015
|
+
};
|
|
13055
14016
|
}
|
|
13056
14017
|
|
|
13057
14018
|
// Compute statements writing the site's result into `target` (an expression
|
|
@@ -13127,7 +14088,7 @@ function lowerPuMemoDecl(stmt, ctx) {
|
|
|
13127
14088
|
function lowerAuthoredHookMemo(stmt, ctx) {
|
|
13128
14089
|
const entry = authoredHookMemoOf(stmt);
|
|
13129
14090
|
if (entry === null) return null;
|
|
13130
|
-
const { name, decl, kind, fn, deps } = entry;
|
|
14091
|
+
const { name, decl, kind, fn, deps, generatedInvariant } = entry;
|
|
13131
14092
|
// Explicit `null` deps: recompute every render — no cache at all. A
|
|
13132
14093
|
// useCallback degenerates to the factory itself; a useMemo evaluates
|
|
13133
14094
|
// inline (via the shared block machinery when the body is a block).
|
|
@@ -13143,6 +14104,24 @@ function lowerAuthoredHookMemo(stmt, ctx) {
|
|
|
13143
14104
|
}
|
|
13144
14105
|
const names = hookMemoNames(ctx);
|
|
13145
14106
|
const base = ctx.currentHookMemoOffset;
|
|
14107
|
+
if (generatedInvariant) {
|
|
14108
|
+
// A compiler-owned callback whose captures are lifetime-invariant cannot
|
|
14109
|
+
// miss after its first render. Its function value doubles as the init flag,
|
|
14110
|
+
// and publishing it immediately preserves identity across suspension.
|
|
14111
|
+
ctx.currentHookMemoOffset = base + 1;
|
|
14112
|
+
const valueCell = () => b.member(b.id(names.cache), hkNumLit(base), true);
|
|
14113
|
+
return [
|
|
14114
|
+
{
|
|
14115
|
+
...stmt,
|
|
14116
|
+
declarations: [
|
|
14117
|
+
{
|
|
14118
|
+
...decl,
|
|
14119
|
+
init: b.logical('??', valueCell(), b.assignment('=', valueCell(), fn)),
|
|
14120
|
+
},
|
|
14121
|
+
],
|
|
14122
|
+
},
|
|
14123
|
+
];
|
|
14124
|
+
}
|
|
13146
14125
|
const k = deps.length;
|
|
13147
14126
|
ctx.currentHookMemoOffset = base + k + 2;
|
|
13148
14127
|
const cellRef = (i) => b.member(b.id(names.cache), hkNumLit(i), true);
|
|
@@ -13502,19 +14481,42 @@ function compileReturnJsxFunction(node, ctx, options) {
|
|
|
13502
14481
|
const prevValueDirectiveLowering = ctx._valueDirectiveLowering;
|
|
13503
14482
|
const prevLocals = ctx.currentComponentLocals;
|
|
13504
14483
|
const prevAutoMemoCallsitesSafe = ctx.currentAutoMemoCallsitesSafe;
|
|
14484
|
+
const prevAutoMemoLocalHazards = ctx.currentAutoMemoLocalHazards;
|
|
13505
14485
|
const prevMapTemps = ctx.currentMapTemps;
|
|
13506
14486
|
const mapTemps = [];
|
|
13507
14487
|
ctx._valueDirectiveLowering = lowerBodyValueDirective;
|
|
13508
14488
|
ctx.currentComponentLocals = collectComponentLocals(node);
|
|
13509
14489
|
ctx.currentAutoMemoCallsitesSafe = ctx.componentInfo.get(name)?.autoMemoCallsitesSafe !== false;
|
|
14490
|
+
ctx.currentAutoMemoLocalHazards = ctx.componentInfo.get(name)?.autoMemoLocalHazards ?? null;
|
|
13510
14491
|
ctx.currentMapTemps = mapTemps;
|
|
13511
14492
|
let newStatements;
|
|
13512
14493
|
try {
|
|
13513
|
-
|
|
14494
|
+
const authoredStatements = node.body.body || [];
|
|
14495
|
+
const renderedRoots = authoredStatements
|
|
14496
|
+
.filter((statement) => statement.type === 'ReturnStatement' && isJsxNode(statement.argument))
|
|
14497
|
+
.map((statement) => statement.argument);
|
|
14498
|
+
const renderReadNames = collectRenderReadNames(renderedRoots, ctx);
|
|
14499
|
+
let renderScopeEstablished = false;
|
|
14500
|
+
newStatements = authoredStatements.map((sourceStatement) => {
|
|
14501
|
+
// Return-JSX functions keep their ordinary callable ABI. Introducing a
|
|
14502
|
+
// cache into a hookless function would make an existing direct call
|
|
14503
|
+
// require a render scope, so only declarations following an authored,
|
|
14504
|
+
// unconditional Octane hook may use the existing calculation lowering.
|
|
14505
|
+
const calculated = renderScopeEstablished
|
|
14506
|
+
? rewriteAutoCalculation(sourceStatement, ctx.currentComponentLocals, renderReadNames, ctx)
|
|
14507
|
+
: sourceStatement;
|
|
14508
|
+
if (!renderScopeEstablished && sourceStatement.type === 'VariableDeclaration') {
|
|
14509
|
+
renderScopeEstablished = (sourceStatement.declarations || []).some(
|
|
14510
|
+
(declaration) => stableHookCallName(unwrapTsExpr(declaration.init)) !== null,
|
|
14511
|
+
);
|
|
14512
|
+
} else if (!renderScopeEstablished && sourceStatement.type === 'ExpressionStatement') {
|
|
14513
|
+
renderScopeEstablished =
|
|
14514
|
+
stableHookCallName(unwrapTsExpr(sourceStatement.expression)) !== null;
|
|
14515
|
+
}
|
|
13514
14516
|
// A return-based component's undefined output is ambiguous with the compiled
|
|
13515
14517
|
// void-body signal at runtime. Preserve JSX roots for the specialized lowering
|
|
13516
14518
|
// below, but normalize every other owned return to an explicit empty value.
|
|
13517
|
-
const s = normalizeOwnRenderableReturns(
|
|
14519
|
+
const s = normalizeOwnRenderableReturns(calculated, true);
|
|
13518
14520
|
const prepared =
|
|
13519
14521
|
s.type === 'ReturnStatement' && s.argument && isJsxNode(s.argument)
|
|
13520
14522
|
? s
|
|
@@ -13533,8 +14535,33 @@ function compileReturnJsxFunction(node, ctx, options) {
|
|
|
13533
14535
|
ctx._valueDirectiveLowering = prevValueDirectiveLowering;
|
|
13534
14536
|
ctx.currentComponentLocals = prevLocals;
|
|
13535
14537
|
ctx.currentAutoMemoCallsitesSafe = prevAutoMemoCallsitesSafe;
|
|
14538
|
+
ctx.currentAutoMemoLocalHazards = prevAutoMemoLocalHazards;
|
|
13536
14539
|
ctx.currentMapTemps = prevMapTemps;
|
|
13537
14540
|
}
|
|
14541
|
+
if (
|
|
14542
|
+
ctx.autoMemo &&
|
|
14543
|
+
ctx.privateUnescapedComponents.has(name) &&
|
|
14544
|
+
(node.params?.length ?? 0) === 0 &&
|
|
14545
|
+
(node.body.body?.length ?? 0) === 1 &&
|
|
14546
|
+
isPlainHostRoot(node.body.body[0]?.argument) &&
|
|
14547
|
+
compInlinedSubs.length === 0 &&
|
|
14548
|
+
mapTemps.length === 0 &&
|
|
14549
|
+
cssHash === null
|
|
14550
|
+
) {
|
|
14551
|
+
const descriptor = newStatements[0]?.argument;
|
|
14552
|
+
const renderer = descriptor?.arguments?.[0];
|
|
14553
|
+
const props = descriptor?.arguments?.[1];
|
|
14554
|
+
if (
|
|
14555
|
+
descriptor?.type === 'CallExpression' &&
|
|
14556
|
+
descriptor.callee?.type === 'Identifier' &&
|
|
14557
|
+
descriptor.callee.name === '_$createElement' &&
|
|
14558
|
+
descriptor.arguments.length === 2 &&
|
|
14559
|
+
renderer?.type === 'Identifier' &&
|
|
14560
|
+
isStaticFragmentRendererProps(props, ctx)
|
|
14561
|
+
) {
|
|
14562
|
+
ctx.componentInfo.get(name).staticFragmentRenderer = { name: renderer.name, props };
|
|
14563
|
+
}
|
|
14564
|
+
}
|
|
13538
14565
|
// The rebuilt function shell maps to the authored declaration. Hoisted
|
|
13539
14566
|
// helper fns (compInlinedSubs — filled by the statement mapping above) are
|
|
13540
14567
|
// function DECLARATION nodes embedded at the top of the body, matching the
|
|
@@ -13714,6 +14741,57 @@ function objectProp(hn, valNode) {
|
|
|
13714
14741
|
return b.prop('init', b.id(hn), valNode);
|
|
13715
14742
|
}
|
|
13716
14743
|
|
|
14744
|
+
// A bare, immutable same-module component needs no descriptor when its JSX is
|
|
14745
|
+
// consumed immediately by a returned host. Keep every value/props boundary on
|
|
14746
|
+
// the ordinary path: only this attribute-free call can remain in the template
|
|
14747
|
+
// without moving authored expression evaluation into its hoisted renderer.
|
|
14748
|
+
function isStaticFragmentRendererProps(props, ctx) {
|
|
14749
|
+
if (props?.type !== 'ObjectExpression') return false;
|
|
14750
|
+
for (const property of props.properties || []) {
|
|
14751
|
+
if (
|
|
14752
|
+
property.type !== 'Property' ||
|
|
14753
|
+
property.kind !== 'init' ||
|
|
14754
|
+
property.computed ||
|
|
14755
|
+
property.method ||
|
|
14756
|
+
property.shorthand
|
|
14757
|
+
) {
|
|
14758
|
+
return false;
|
|
14759
|
+
}
|
|
14760
|
+
const descriptor = property.value;
|
|
14761
|
+
const component = descriptor?.arguments?.[0];
|
|
14762
|
+
const componentProps = descriptor?.arguments?.[1];
|
|
14763
|
+
if (
|
|
14764
|
+
descriptor?.type !== 'CallExpression' ||
|
|
14765
|
+
descriptor.callee?.type !== 'Identifier' ||
|
|
14766
|
+
descriptor.callee.name !== '_$createElement' ||
|
|
14767
|
+
descriptor.arguments.length !== 2 ||
|
|
14768
|
+
component?.type !== 'Identifier' ||
|
|
14769
|
+
!ctx.privateUnescapedComponents.has(component.name) ||
|
|
14770
|
+
componentProps?.type !== 'ObjectExpression' ||
|
|
14771
|
+
componentProps.properties.length !== 0
|
|
14772
|
+
) {
|
|
14773
|
+
return false;
|
|
14774
|
+
}
|
|
14775
|
+
}
|
|
14776
|
+
return true;
|
|
14777
|
+
}
|
|
14778
|
+
|
|
14779
|
+
function isStaticReturnedFragmentComponent(node, ctx) {
|
|
14780
|
+
if (!ctx.autoMemo || ctx._foldCtx?.immediateRenderedOutput !== true) return false;
|
|
14781
|
+
const name = tagBindingName(node);
|
|
14782
|
+
if (
|
|
14783
|
+
name === null ||
|
|
14784
|
+
ctx.currentComponentLocals == null ||
|
|
14785
|
+
ctx.currentComponentLocals.has(name) ||
|
|
14786
|
+
!ctx.privateUnescapedComponents.has(name) ||
|
|
14787
|
+
ctx.componentInfo.get(name)?.staticFragmentRenderer === undefined
|
|
14788
|
+
) {
|
|
14789
|
+
return false;
|
|
14790
|
+
}
|
|
14791
|
+
const attrs = node.attributes || node.openingElement?.attributes || [];
|
|
14792
|
+
return attrs.length === 0 && (node.children || []).length === 0;
|
|
14793
|
+
}
|
|
14794
|
+
|
|
13717
14795
|
// Walk a host element or JSX fragment, replacing each DYNAMIC part (an
|
|
13718
14796
|
// attribute/child expression) with `props.hN` and collecting
|
|
13719
14797
|
// `{ hN: <originalExpr> }` into `holeProps`. Static structure (tag, literal attrs,
|
|
@@ -13831,6 +14909,22 @@ function extractFragment(node, ctx, holeProps, parentNs = 'html') {
|
|
|
13831
14909
|
// a hole too, since it may be a local/member/dynamic tag that the hoisted
|
|
13832
14910
|
// renderer cannot reference directly.
|
|
13833
14911
|
newChildren.push(extractFragmentComponent(child, ctx, holeProps, childNs));
|
|
14912
|
+
} else if (isComponentTag(child) && isStaticReturnedFragmentComponent(child, ctx)) {
|
|
14913
|
+
// The original function remains callable and still returns its authored
|
|
14914
|
+
// descriptor. Only this nonescaping call can reuse its already-hoisted,
|
|
14915
|
+
// hookless fragment directly through the existing lite component ABI.
|
|
14916
|
+
const extracted = extractFragment(child, ctx, holeProps, childNs);
|
|
14917
|
+
newChildren.push({
|
|
14918
|
+
...extracted,
|
|
14919
|
+
openingElement: {
|
|
14920
|
+
...extracted.openingElement,
|
|
14921
|
+
metadata: {
|
|
14922
|
+
...extracted.openingElement.metadata,
|
|
14923
|
+
staticFragmentRenderer: ctx.componentInfo.get(tagBindingName(child))
|
|
14924
|
+
.staticFragmentRenderer,
|
|
14925
|
+
},
|
|
14926
|
+
},
|
|
14927
|
+
});
|
|
13834
14928
|
} else if (isComponentTag(child)) {
|
|
13835
14929
|
const hn = `h${holeProps.length}`;
|
|
13836
14930
|
holeProps.push(
|
|
@@ -16108,6 +17202,7 @@ function emitAutoMemoRegion(
|
|
|
16108
17202
|
contextAware,
|
|
16109
17203
|
depNode,
|
|
16110
17204
|
initValue = null,
|
|
17205
|
+
restoreCachedContext = false,
|
|
16111
17206
|
) {
|
|
16112
17207
|
const cell = allocAutoMemoCell(ctx, dependencies.length + (contextAware ? 1 : 0));
|
|
16113
17208
|
const contextIndex = contextAware ? cell.base + dependencies.length : null;
|
|
@@ -16157,7 +17252,15 @@ function emitAutoMemoRegion(
|
|
|
16157
17252
|
}
|
|
16158
17253
|
ctx.runtimeNeeded.add('compilerCacheContext');
|
|
16159
17254
|
const cacheContextCall = () =>
|
|
16160
|
-
|
|
17255
|
+
restoreCachedContext
|
|
17256
|
+
? b.call(
|
|
17257
|
+
'_$compilerCacheContext',
|
|
17258
|
+
b.id('__s'),
|
|
17259
|
+
b.literal(slotIndex),
|
|
17260
|
+
cacheAt(contextIndex),
|
|
17261
|
+
b.literal(true),
|
|
17262
|
+
)
|
|
17263
|
+
: b.call('_$compilerCacheContext', b.id('__s'), b.literal(slotIndex), cacheAt(contextIndex));
|
|
16161
17264
|
return b.block([
|
|
16162
17265
|
...depDecls,
|
|
16163
17266
|
b.if(
|
|
@@ -16773,10 +17876,11 @@ function planJsx(
|
|
|
16773
17876
|
const elVar = ensureVar(c.hostPath || []);
|
|
16774
17877
|
c.elVar = elVar;
|
|
16775
17878
|
const org = c.origin ?? planOrigin;
|
|
16776
|
-
|
|
17879
|
+
const key = `_${hostKey}$${c.id}`;
|
|
17880
|
+
if (!noTemplate && bag.host(key, elVar)) {
|
|
16777
17881
|
mountLines.push(
|
|
16778
17882
|
inheritOriginLoc(
|
|
16779
|
-
b.stmt(b.assignment('=', b.id(bag.local(
|
|
17883
|
+
b.stmt(b.assignment('=', b.id(bag.local(key)), hostVarNode(elVar))),
|
|
16780
17884
|
org,
|
|
16781
17885
|
),
|
|
16782
17886
|
);
|
|
@@ -17035,11 +18139,26 @@ function planJsx(
|
|
|
17035
18139
|
const pushAfterStmt = (id, org, node) => pushAfter(id, inheritOriginLoc(node, org));
|
|
17036
18140
|
for (const fc of forCalls) {
|
|
17037
18141
|
const isMappedList = fc.mapMethodExpr !== null;
|
|
17038
|
-
|
|
18142
|
+
const forHelper = isMappedList
|
|
18143
|
+
? fc.hostMountSafe
|
|
18144
|
+
? 'fastMapSlot'
|
|
18145
|
+
: 'mapSlot'
|
|
18146
|
+
: fc.keyedSelectionIndex >= 0
|
|
18147
|
+
? fc.hostMountSafe
|
|
18148
|
+
? 'fastKeyedForBlock'
|
|
18149
|
+
: 'keyedForBlock'
|
|
18150
|
+
: fc.hostMountSafe
|
|
18151
|
+
? 'fastForBlock'
|
|
18152
|
+
: 'forBlock';
|
|
18153
|
+
ctx.runtimeNeeded.add(forHelper);
|
|
18154
|
+
if (isMappedList && fc.hostMountSafe && fc.autoMemoDeps !== null) {
|
|
18155
|
+
// Memoized maps query the original native-array guard before dispatch.
|
|
18156
|
+
ctx.runtimeNeeded.add('mapSlot');
|
|
18157
|
+
}
|
|
17039
18158
|
const slotIndex = fc.slotIndex;
|
|
17040
18159
|
const org = fc.origin ?? planOrigin;
|
|
17041
18160
|
registerDirectiveOrigin(ctx, org, [
|
|
17042
|
-
|
|
18161
|
+
rtAlias(forHelper),
|
|
17043
18162
|
fc.keyHelper,
|
|
17044
18163
|
fc.bodyHelper,
|
|
17045
18164
|
fc.emptyHelper,
|
|
@@ -17052,13 +18171,19 @@ function planJsx(
|
|
|
17052
18171
|
// for survivors when deps unchanged this render),
|
|
17053
18172
|
// bit 3 = indexIndependent (body binds no `index` → a pure reorder
|
|
17054
18173
|
// 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
|
|
18174
|
+
// bit 4 = SSR emitted markerless direct-host items; hydrate them by root,
|
|
18175
|
+
// bit 5 = conditional item bodies require their active scope,
|
|
18176
|
+
// bits 6+ = the zero-based keyedForBlock selection dependency index.
|
|
18177
|
+
// keyedForBlock itself identifies a selection, including dependency zero;
|
|
18178
|
+
// ordinary forBlock reuses bit 5 without an argument or tuple allocation.
|
|
17056
18179
|
const flags =
|
|
17057
18180
|
(fc.pure ? 1 : 0) |
|
|
17058
18181
|
(fc.singleRoot ? 2 : 0) |
|
|
17059
18182
|
(fc.depEligible ? 4 : 0) |
|
|
17060
18183
|
(fc.indexIndependent ? 8 : 0) |
|
|
17061
|
-
(fc.ssrMarkerless ? 16 : 0)
|
|
18184
|
+
(fc.ssrMarkerless ? 16 : 0) |
|
|
18185
|
+
(fc.requiresScope ? 32 : 0) |
|
|
18186
|
+
(fc.keyedSelectionIndex >= 0 ? fc.keyedSelectionIndex << 6 : 0);
|
|
17062
18187
|
// Arg layout: forBlock(__s, slot, host, items, keyFn, body, flags?, deps?,
|
|
17063
18188
|
// emptyBody?, anchor?, ownEnd?).
|
|
17064
18189
|
// Optional args backfill positionally: `flags`/`deps` placeholders
|
|
@@ -17117,7 +18242,7 @@ function planJsx(
|
|
|
17117
18242
|
if (fc.autoMemoDeps !== null) {
|
|
17118
18243
|
const mappedCall = b.stmt(
|
|
17119
18244
|
b.call(
|
|
17120
|
-
|
|
18245
|
+
rtAlias(forHelper),
|
|
17121
18246
|
b.id('__s'),
|
|
17122
18247
|
b.literal(slotIndex),
|
|
17123
18248
|
hostExpr(),
|
|
@@ -17159,7 +18284,7 @@ function planJsx(
|
|
|
17159
18284
|
org,
|
|
17160
18285
|
b.stmt(
|
|
17161
18286
|
b.call(
|
|
17162
|
-
|
|
18287
|
+
rtAlias(forHelper),
|
|
17163
18288
|
b.id('__s'),
|
|
17164
18289
|
b.literal(slotIndex),
|
|
17165
18290
|
hostExpr(),
|
|
@@ -17188,7 +18313,7 @@ function planJsx(
|
|
|
17188
18313
|
slotIndex,
|
|
17189
18314
|
b.stmt(
|
|
17190
18315
|
b.call(
|
|
17191
|
-
|
|
18316
|
+
rtAlias(forHelper),
|
|
17192
18317
|
b.id('__s'),
|
|
17193
18318
|
b.literal(slotIndex),
|
|
17194
18319
|
hostExpr(),
|
|
@@ -17207,7 +18332,7 @@ function planJsx(
|
|
|
17207
18332
|
org,
|
|
17208
18333
|
b.stmt(
|
|
17209
18334
|
b.call(
|
|
17210
|
-
|
|
18335
|
+
rtAlias(forHelper),
|
|
17211
18336
|
b.id('__s'),
|
|
17212
18337
|
b.literal(slotIndex),
|
|
17213
18338
|
hostExpr(),
|
|
@@ -17327,6 +18452,59 @@ function planJsx(
|
|
|
17327
18452
|
continue;
|
|
17328
18453
|
}
|
|
17329
18454
|
ctx.runtimeNeeded.add('setText');
|
|
18455
|
+
const updateHole = () =>
|
|
18456
|
+
b.if(
|
|
18457
|
+
b.logical('||', b.id('_o'), b.binary('!==', chp(), V())),
|
|
18458
|
+
b.block([
|
|
18459
|
+
b.const('_t', chv()),
|
|
18460
|
+
b.if(
|
|
18461
|
+
andChain([
|
|
18462
|
+
b.binary('!=', b.id('_t'), b.literal(null)),
|
|
18463
|
+
b.unary('!', b.id('_o')),
|
|
18464
|
+
b.binary('!==', V(), b.literal(null)),
|
|
18465
|
+
]),
|
|
18466
|
+
b.stmt(b.call('_$setText', b.id('_t'), V())),
|
|
18467
|
+
b.stmt(
|
|
18468
|
+
b.assignment(
|
|
18469
|
+
'=',
|
|
18470
|
+
chv(),
|
|
18471
|
+
b.call(
|
|
18472
|
+
'_$childTextHole',
|
|
18473
|
+
b.id('__s'),
|
|
18474
|
+
b.literal(slotIndex),
|
|
18475
|
+
hostExpr(),
|
|
18476
|
+
V(),
|
|
18477
|
+
b.id('_t'),
|
|
18478
|
+
),
|
|
18479
|
+
),
|
|
18480
|
+
),
|
|
18481
|
+
),
|
|
18482
|
+
b.stmt(b.assignment('=', chp(), V())),
|
|
18483
|
+
]),
|
|
18484
|
+
null,
|
|
18485
|
+
);
|
|
18486
|
+
const ordinary = updateHole();
|
|
18487
|
+
let update = ordinary;
|
|
18488
|
+
if (cc.autoMemoValue === true) {
|
|
18489
|
+
// Only a compiler-owned, non-escaping plain data array may skip
|
|
18490
|
+
// descriptor reconciliation. Accessor-backed, nested, or deferred
|
|
18491
|
+
// children must still be observed on every parent render; the
|
|
18492
|
+
// runtime caches eligibility by immutable snapshot identity.
|
|
18493
|
+
ctx.runtimeNeeded.add('compilerCacheArray');
|
|
18494
|
+
const cached = emitAutoMemoRegion(
|
|
18495
|
+
ctx,
|
|
18496
|
+
['_v'],
|
|
18497
|
+
slotIndex,
|
|
18498
|
+
updateHole(),
|
|
18499
|
+
// Array → scalar → the same array must reconstruct the list.
|
|
18500
|
+
b.binary('!==', chp(), V()),
|
|
18501
|
+
true,
|
|
18502
|
+
undefined,
|
|
18503
|
+
null,
|
|
18504
|
+
true,
|
|
18505
|
+
);
|
|
18506
|
+
update = b.if(b.call('_$compilerCacheArray', V(), chp()), cached, ordinary);
|
|
18507
|
+
}
|
|
17330
18508
|
pushAfterStmt(
|
|
17331
18509
|
cc.id,
|
|
17332
18510
|
org,
|
|
@@ -17344,36 +18522,7 @@ function planJsx(
|
|
|
17344
18522
|
),
|
|
17345
18523
|
),
|
|
17346
18524
|
),
|
|
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
|
-
),
|
|
18525
|
+
update,
|
|
17377
18526
|
]),
|
|
17378
18527
|
);
|
|
17379
18528
|
continue;
|
|
@@ -17809,6 +18958,7 @@ function bagLetter(i) {
|
|
|
17809
18958
|
function makeBag() {
|
|
17810
18959
|
const fields = [];
|
|
17811
18960
|
const byKey = new Map();
|
|
18961
|
+
const byHost = new Map();
|
|
17812
18962
|
const reg = (key, constExpr) => {
|
|
17813
18963
|
let r = byKey.get(key);
|
|
17814
18964
|
if (r === undefined) {
|
|
@@ -17831,6 +18981,16 @@ function makeBag() {
|
|
|
17831
18981
|
return {
|
|
17832
18982
|
/** Mount-write target for `key` — the pre-declared local. */
|
|
17833
18983
|
local: (key) => reg(key, undefined).local,
|
|
18984
|
+
/** Register one immutable DOM-host field; aliases reuse its first mount write. */
|
|
18985
|
+
host: (key, host) => {
|
|
18986
|
+
const existing = byHost.get(host);
|
|
18987
|
+
if (existing !== undefined) {
|
|
18988
|
+
byKey.set(key, existing);
|
|
18989
|
+
return false;
|
|
18990
|
+
}
|
|
18991
|
+
byHost.set(host, reg(key, undefined));
|
|
18992
|
+
return true;
|
|
18993
|
+
},
|
|
17834
18994
|
/** Seed `key` with a constant expression (no local, no mount write). */
|
|
17835
18995
|
constField: (key, expr) => {
|
|
17836
18996
|
reg(key, expr);
|
|
@@ -17975,8 +19135,10 @@ function emitDeferredMount(bind, elVar, bag) {
|
|
|
17975
19135
|
if (!(bind.kind === 'class' && bind.fresh)) {
|
|
17976
19136
|
bag.constField(bind.kind === 'style' ? `_sty$${bind.id}` : `_prev$${bind.id}`, 'undefined');
|
|
17977
19137
|
}
|
|
19138
|
+
const key = `_el$${bind.id}`;
|
|
19139
|
+
if (!bag.host(key, elVar)) return null;
|
|
17978
19140
|
return inheritOriginLoc(
|
|
17979
|
-
b.stmt(b.assignment('=', b.id(bag.local(
|
|
19141
|
+
b.stmt(b.assignment('=', b.id(bag.local(key)), hostVarNode(elVar))),
|
|
17980
19142
|
bindingOrigin(bind),
|
|
17981
19143
|
);
|
|
17982
19144
|
}
|
|
@@ -17994,6 +19156,9 @@ function emitBindingMount(bind, elVar, bag) {
|
|
|
17994
19156
|
const st = (node) => inheritOriginLoc(node, org);
|
|
17995
19157
|
const el = () => hostVarNode(elVar);
|
|
17996
19158
|
const local = (key) => b.id(bag.local(key));
|
|
19159
|
+
const hostKey = `_el$${bind.id}`;
|
|
19160
|
+
const mountHost = () =>
|
|
19161
|
+
bag.host(hostKey, elVar) ? [b.stmt(b.assignment('=', local(hostKey), el()))] : [];
|
|
17997
19162
|
const V = () => b.id('_v');
|
|
17998
19163
|
// The tokens that NAME this binding's lowering carry the authored attribute
|
|
17999
19164
|
// name; everything else in the call maps to the value expression.
|
|
@@ -18011,10 +19176,7 @@ function emitBindingMount(bind, elVar, bag) {
|
|
|
18011
19176
|
return st(b.stmt(b.call('_$markNativeChangeDiagnosticStatic', el())));
|
|
18012
19177
|
}
|
|
18013
19178
|
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
|
-
];
|
|
19179
|
+
return [...mountHost().map(st), st(b.stmt(b.call('_$queueNativeChangeDiagnostic', el())))];
|
|
18018
19180
|
}
|
|
18019
19181
|
switch (bind.kind) {
|
|
18020
19182
|
case 'textOnlyChild': {
|
|
@@ -18036,7 +19198,7 @@ function emitBindingMount(bind, elVar, bag) {
|
|
|
18036
19198
|
b.block([
|
|
18037
19199
|
b.const('_v', bind.expr),
|
|
18038
19200
|
b.stmt(b.call('_$setDangerouslySetInnerHTML', el(), V())),
|
|
18039
|
-
|
|
19201
|
+
...mountHost(),
|
|
18040
19202
|
b.stmt(b.assignment('=', local(`_prev$${bind.id}`), V())),
|
|
18041
19203
|
]),
|
|
18042
19204
|
);
|
|
@@ -18065,7 +19227,7 @@ function emitBindingMount(bind, elVar, bag) {
|
|
|
18065
19227
|
return st(
|
|
18066
19228
|
b.block([
|
|
18067
19229
|
b.stmt(b.call('_$setDangerouslySetInnerHTMLSources', el(), sources)),
|
|
18068
|
-
|
|
19230
|
+
...mountHost(),
|
|
18069
19231
|
]),
|
|
18070
19232
|
);
|
|
18071
19233
|
}
|
|
@@ -18074,14 +19236,11 @@ function emitBindingMount(bind, elVar, bag) {
|
|
|
18074
19236
|
b.id(bag.local(`${spread ? '_sp' : '_prev'}$${binding.id}`)),
|
|
18075
19237
|
);
|
|
18076
19238
|
return st(
|
|
18077
|
-
b.block([
|
|
18078
|
-
b.stmt(b.call('_$setFormControlSources', el(), sources)),
|
|
18079
|
-
b.stmt(b.assignment('=', local(`_el$${bind.id}`), el())),
|
|
18080
|
-
]),
|
|
19239
|
+
b.block([b.stmt(b.call('_$setFormControlSources', el(), sources)), ...mountHost()]),
|
|
18081
19240
|
);
|
|
18082
19241
|
}
|
|
18083
19242
|
case 'hostCommit': {
|
|
18084
|
-
const
|
|
19243
|
+
const hostMount = mountHost();
|
|
18085
19244
|
const propsLocal = local(`_host$${bind.id}`);
|
|
18086
19245
|
const sources = commitSourceRows(bind.sources, (binding, spread) =>
|
|
18087
19246
|
b.id(bag.local(`${spread ? '_sp' : '_prev'}$${binding.id}`)),
|
|
@@ -18112,7 +19271,7 @@ function emitBindingMount(bind, elVar, bag) {
|
|
|
18112
19271
|
);
|
|
18113
19272
|
return st(
|
|
18114
19273
|
b.block([
|
|
18115
|
-
|
|
19274
|
+
...hostMount,
|
|
18116
19275
|
b.stmt(
|
|
18117
19276
|
b.assignment(
|
|
18118
19277
|
'=',
|
|
@@ -18155,7 +19314,7 @@ function emitBindingMount(bind, elVar, bag) {
|
|
|
18155
19314
|
b.block([
|
|
18156
19315
|
b.const('_v', bind.expr),
|
|
18157
19316
|
b.stmt(b.call(callee(), el(), nameLit(), V())),
|
|
18158
|
-
|
|
19317
|
+
...mountHost(),
|
|
18159
19318
|
b.stmt(b.assignment('=', local(`_prev$${bind.id}`), V())),
|
|
18160
19319
|
]),
|
|
18161
19320
|
);
|
|
@@ -18172,12 +19331,7 @@ function emitBindingMount(bind, elVar, bag) {
|
|
|
18172
19331
|
// update must re-run every render to reassert drift (React's
|
|
18173
19332
|
// controlled contract). Not in DEFERRABLE_MOUNT_KINDS: the mount
|
|
18174
19333
|
// 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
|
-
);
|
|
19334
|
+
return st(b.block([b.stmt(b.call(callee(), el(), bind.expr)), ...mountHost()]));
|
|
18181
19335
|
}
|
|
18182
19336
|
case 'autoFocus': {
|
|
18183
19337
|
// Mount-only (React ignores later autoFocus changes); the focus
|
|
@@ -18187,11 +19341,7 @@ function emitBindingMount(bind, elVar, bag) {
|
|
|
18187
19341
|
case 'class': {
|
|
18188
19342
|
// On SVG/MathML hosts the `className` property is read-only — fall back
|
|
18189
19343
|
// 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
|
-
];
|
|
19344
|
+
const body = [b.const('_v', bind.expr), b.stmt(b.call(callee(), el(), V())), ...mountHost()];
|
|
18195
19345
|
if (!bind.fresh) body.push(b.stmt(b.assignment('=', local(`_prev$${bind.id}`), V())));
|
|
18196
19346
|
return st(b.block(body));
|
|
18197
19347
|
}
|
|
@@ -18200,7 +19350,7 @@ function emitBindingMount(bind, elVar, bag) {
|
|
|
18200
19350
|
b.block([
|
|
18201
19351
|
b.const('_v', bind.expr),
|
|
18202
19352
|
b.stmt(b.call(callee(), el(), V(), undefinedNode())),
|
|
18203
|
-
|
|
19353
|
+
...mountHost(),
|
|
18204
19354
|
b.stmt(b.assignment('=', local(`_sty$${bind.id}`), V())),
|
|
18205
19355
|
]),
|
|
18206
19356
|
);
|
|
@@ -18223,7 +19373,7 @@ function emitBindingMount(bind, elVar, bag) {
|
|
|
18223
19373
|
: [];
|
|
18224
19374
|
// Register the mount locals BEFORE the cleanup closure resolves their
|
|
18225
19375
|
// bag letters (letter() throws for a never-mounted field).
|
|
18226
|
-
const
|
|
19376
|
+
const hostMount = mountHost();
|
|
18227
19377
|
const spLocal = local(`_sp$${bind.id}`);
|
|
18228
19378
|
const cleanup = b.arrow(
|
|
18229
19379
|
[],
|
|
@@ -18250,7 +19400,7 @@ function emitBindingMount(bind, elVar, bag) {
|
|
|
18250
19400
|
b.block([
|
|
18251
19401
|
b.const('_v', bind.expr),
|
|
18252
19402
|
b.stmt(b.call('_$setSpread', el(), V(), undefinedNode(), b.id('__s'), ...flags)),
|
|
18253
|
-
|
|
19403
|
+
...hostMount,
|
|
18254
19404
|
b.stmt(b.assignment('=', spLocal, V())),
|
|
18255
19405
|
cleanupsPush(cleanup),
|
|
18256
19406
|
]),
|
|
@@ -18264,7 +19414,7 @@ function emitBindingMount(bind, elVar, bag) {
|
|
|
18264
19414
|
b.assignment('=', b.member(el(), slotKeyLiteral(bind), true), value),
|
|
18265
19415
|
);
|
|
18266
19416
|
if (bind.mountOnly) return st(slotAssign);
|
|
18267
|
-
return [
|
|
19417
|
+
return [...mountHost().map(st), st(slotAssign)];
|
|
18268
19418
|
}
|
|
18269
19419
|
case 'formAction': {
|
|
18270
19420
|
// <form action={fn}> / <button formAction={fn}>: wire the submit handler
|
|
@@ -18274,7 +19424,7 @@ function emitBindingMount(bind, elVar, bag) {
|
|
|
18274
19424
|
b.block([
|
|
18275
19425
|
b.const('_v', bind.expr),
|
|
18276
19426
|
b.stmt(b.call(callee(), el(), nameLit(), V(), undefinedNode())),
|
|
18277
|
-
|
|
19427
|
+
...mountHost(),
|
|
18278
19428
|
b.stmt(b.assignment('=', local(`_prev$${bind.id}`), V())),
|
|
18279
19429
|
]),
|
|
18280
19430
|
);
|
|
@@ -18314,6 +19464,7 @@ function emitBindingMount(bind, elVar, bag) {
|
|
|
18314
19464
|
// cleanup read because updates re-point it. Assigning both bag locals inside
|
|
18315
19465
|
// the queue call evaluates each mount value once while avoiding throwaway
|
|
18316
19466
|
// temporaries; Suspense still receives the exact ref/target pair.
|
|
19467
|
+
const initializeHost = bag.host(hostKey, elVar);
|
|
18317
19468
|
return st(
|
|
18318
19469
|
b.block([
|
|
18319
19470
|
b.stmt(
|
|
@@ -18321,7 +19472,7 @@ function emitBindingMount(bind, elVar, bag) {
|
|
|
18321
19472
|
'_$queueRefAttach',
|
|
18322
19473
|
b.id('__s'),
|
|
18323
19474
|
b.assignment('=', local(`_ref$${bind.id}`), bind.expr),
|
|
18324
|
-
b.assignment('=', local(
|
|
19475
|
+
initializeHost ? b.assignment('=', local(hostKey), el()) : local(hostKey),
|
|
18325
19476
|
),
|
|
18326
19477
|
),
|
|
18327
19478
|
cleanupsPush(
|
|
@@ -19616,8 +20767,21 @@ function emitElementHtml(
|
|
|
19616
20767
|
// is unreachable from the output: the key's map position resolves to
|
|
19617
20768
|
// the HANDLER expression, and the name itself emits nothing.
|
|
19618
20769
|
registerExactOrigin(ctx, attr.name, attr.name?.end, [`'${slotKey}'`, `"${slotKey}"`]);
|
|
19619
|
-
if (capture)
|
|
19620
|
-
|
|
20770
|
+
if (capture) {
|
|
20771
|
+
ctx.capturedEvents.add(eventName);
|
|
20772
|
+
if (ctx.currentComponentOwner !== null) {
|
|
20773
|
+
ctx.currentComponentOwner.capturedEvents.add(eventName);
|
|
20774
|
+
} else {
|
|
20775
|
+
ctx.unownedCapturedEvents.add(eventName);
|
|
20776
|
+
}
|
|
20777
|
+
} else {
|
|
20778
|
+
ctx.delegatedEvents.add(eventName);
|
|
20779
|
+
if (ctx.currentComponentOwner !== null) {
|
|
20780
|
+
ctx.currentComponentOwner.delegatedEvents.add(eventName);
|
|
20781
|
+
} else {
|
|
20782
|
+
ctx.unownedDelegatedEvents.add(eventName);
|
|
20783
|
+
}
|
|
20784
|
+
}
|
|
19621
20785
|
// Hot-path optimisation: `() => fn(arg, …)` arrows with zero params get
|
|
19622
20786
|
// compiled to a `{ fn, args }` bundle so the runtime can identity-diff
|
|
19623
20787
|
// fn + each arg and skip the property reassignment when nothing
|
|
@@ -20613,7 +21777,7 @@ function soleRenderPropChild(children) {
|
|
|
20613
21777
|
// expression remains AST throughout so a nested `() => @{…}` sub-template
|
|
20614
21778
|
// hoists (and server-mode `use(thenable)` calls get their stable keys).
|
|
20615
21779
|
function makeChildCall(expr, ctx, componentName, inlinedSubs, cssHash) {
|
|
20616
|
-
|
|
21780
|
+
const child = {
|
|
20617
21781
|
id: ctx.nextHelperId++,
|
|
20618
21782
|
loc: devLoc(ctx, expr),
|
|
20619
21783
|
origin: expr,
|
|
@@ -20625,6 +21789,27 @@ function makeChildCall(expr, ctx, componentName, inlinedSubs, cssHash) {
|
|
|
20625
21789
|
inlinedSubs,
|
|
20626
21790
|
),
|
|
20627
21791
|
};
|
|
21792
|
+
if (
|
|
21793
|
+
ctx.autoMemo &&
|
|
21794
|
+
// Imported calculations do not expose their return type. Restrict the
|
|
21795
|
+
// array guard to deferred component-child bodies, where cached descriptor
|
|
21796
|
+
// lists otherwise cross an expensive value-position boundary; an ordinary
|
|
21797
|
+
// scalar directly in its owning component must not acquire this region.
|
|
21798
|
+
ctx.currentBodyIsComponentScope !== true &&
|
|
21799
|
+
expr?.type === 'Identifier'
|
|
21800
|
+
) {
|
|
21801
|
+
for (
|
|
21802
|
+
let proof = ctx.currentAutoCalculatedRenderableRefs;
|
|
21803
|
+
proof !== null;
|
|
21804
|
+
proof = proof.parent
|
|
21805
|
+
) {
|
|
21806
|
+
if (proof.nodes.has(expr)) {
|
|
21807
|
+
child.autoMemoValue = true;
|
|
21808
|
+
break;
|
|
21809
|
+
}
|
|
21810
|
+
}
|
|
21811
|
+
}
|
|
21812
|
+
return child;
|
|
20628
21813
|
}
|
|
20629
21814
|
|
|
20630
21815
|
const AST_STRUCTURAL_KEY_SKIP = new Set([
|
|
@@ -20761,7 +21946,10 @@ function makeCompCall(
|
|
|
20761
21946
|
) {
|
|
20762
21947
|
const id = ctx.nextHelperId++;
|
|
20763
21948
|
const compName = tagBindingName(node);
|
|
20764
|
-
const
|
|
21949
|
+
const staticFragmentRenderer = node.openingElement?.metadata?.staticFragmentRenderer;
|
|
21950
|
+
const compNode = staticFragmentRenderer
|
|
21951
|
+
? inheritOriginLoc(b.id(staticFragmentRenderer.name), node.openingElement?.name || node.id)
|
|
21952
|
+
: tagExprNode(node);
|
|
20765
21953
|
// `</Card>` emits nothing — lend it the opening name's generated ranges so a
|
|
20766
21954
|
// closing tag is reachable for components the way it is for host elements.
|
|
20767
21955
|
registerOriginAlias(ctx, node.closingElement?.name, node.id || node.openingElement?.name);
|
|
@@ -20878,7 +22066,7 @@ function makeCompCall(
|
|
|
20878
22066
|
}
|
|
20879
22067
|
|
|
20880
22068
|
// The props object as a node; the call-site emit embeds it directly.
|
|
20881
|
-
const propsExpr = inheritOriginLoc(b.object(propNodes), node);
|
|
22069
|
+
const propsExpr = staticFragmentRenderer?.props ?? inheritOriginLoc(b.object(propNodes), node);
|
|
20882
22070
|
|
|
20883
22071
|
// Design (c) v0: decide whether the call site can use componentSlotLite
|
|
20884
22072
|
// (Scope-only, no Block / no Comment markers / no CompSlot wrapper).
|
|
@@ -20909,7 +22097,12 @@ function makeCompCall(
|
|
|
20909
22097
|
// elides iff the callee carries the definition-site `$$singleRoot` stamp
|
|
20910
22098
|
// (docs/comment-marker-elision-plan.md M1).
|
|
20911
22099
|
let maybeSingleRoot = false;
|
|
20912
|
-
if (
|
|
22100
|
+
if (staticFragmentRenderer) {
|
|
22101
|
+
// The renderer is already a void, hookless component body. A memo boundary
|
|
22102
|
+
// would force a full Block and could hide a hookful descendant's update.
|
|
22103
|
+
liteEligible = true;
|
|
22104
|
+
voidComponent = true;
|
|
22105
|
+
} else if (ctx.componentInfo && compName !== null) {
|
|
20913
22106
|
const callSiteOk = !hasSpreadProp && !hasChildrenProp;
|
|
20914
22107
|
const calleeInfo = ctx.componentInfo.get(compName);
|
|
20915
22108
|
if (calleeInfo) {
|
|
@@ -20946,7 +22139,13 @@ function makeCompCall(
|
|
|
20946
22139
|
}
|
|
20947
22140
|
for (const name of free) {
|
|
20948
22141
|
if (name === compName) continue;
|
|
20949
|
-
if (
|
|
22142
|
+
if (
|
|
22143
|
+
ctx.importNamespaceNames.has(name) ||
|
|
22144
|
+
// A local laundering a ref or live-import member read (see
|
|
22145
|
+
// collectAutoMemoLocalHazards) — its per-render value is not a
|
|
22146
|
+
// complete witness, so the site keeps ordinary entry semantics.
|
|
22147
|
+
ctx.currentAutoMemoLocalHazards?.has(name) === true
|
|
22148
|
+
) {
|
|
20950
22149
|
depsSafe = false;
|
|
20951
22150
|
} else if (ctx.currentComponentLocals.has(name) || ctx.importedNames.has(name)) {
|
|
20952
22151
|
if (!callsiteDeps.coveredRoots.has(name)) deps.add(name);
|
|
@@ -21196,6 +22395,96 @@ function makeSwitchCall(node, ctx, inlinedSubs, parentNs = 'html', cssHash = nul
|
|
|
21196
22395
|
// for-of inside element children → forBlock call
|
|
21197
22396
|
// ===========================================================================
|
|
21198
22397
|
|
|
22398
|
+
/**
|
|
22399
|
+
* Prove that one parent dependency affects a keyed host row only through its
|
|
22400
|
+
* root class's strict comparison with the row key. The runtime can then update
|
|
22401
|
+
* only the old/new selected blocks when every other dependency and the iterable
|
|
22402
|
+
* snapshot remain unchanged.
|
|
22403
|
+
*
|
|
22404
|
+
* This recognizes `selected === item.id` (or its reverse) under `key item.id`,
|
|
22405
|
+
* and the same proof for any other explicit, noncomputed item property.
|
|
22406
|
+
* Dynamic ternary arms, extra references to
|
|
22407
|
+
* `selected` anywhere in the row (including deferred event callbacks), spreads,
|
|
22408
|
+
* and anything other than one direct host root all fail closed.
|
|
22409
|
+
*/
|
|
22410
|
+
function keyedSelectionDepIndex(itemName, keyBody, subStmts, runtimeDepNames, ctx) {
|
|
22411
|
+
if (subStmts.length !== 1 || !isPlainHostRoot(subStmts[0])) return -1;
|
|
22412
|
+
const root = subStmts[0];
|
|
22413
|
+
const attrs = root.attributes || root.openingElement?.attributes || [];
|
|
22414
|
+
if (attrs.some((attr) => attr.type !== 'Attribute' && attr.type !== 'JSXAttribute')) {
|
|
22415
|
+
return -1;
|
|
22416
|
+
}
|
|
22417
|
+
const classAttrs = attrs.filter((attr) => {
|
|
22418
|
+
const name = attr.name?.name || attr.name;
|
|
22419
|
+
return name === 'class' || name === 'className';
|
|
22420
|
+
});
|
|
22421
|
+
if (classAttrs.length !== 1) return -1;
|
|
22422
|
+
const value = classAttrs[0].value;
|
|
22423
|
+
const conditional = unwrapTsExpr(
|
|
22424
|
+
value?.type === 'JSXExpressionContainer' ? value.expression : value,
|
|
22425
|
+
);
|
|
22426
|
+
if (conditional?.type !== 'ConditionalExpression') return -1;
|
|
22427
|
+
const consequent = unwrapTsExpr(conditional.consequent);
|
|
22428
|
+
const alternate = unwrapTsExpr(conditional.alternate);
|
|
22429
|
+
if (
|
|
22430
|
+
consequent?.type !== 'Literal' ||
|
|
22431
|
+
typeof consequent.value !== 'string' ||
|
|
22432
|
+
alternate?.type !== 'Literal' ||
|
|
22433
|
+
typeof alternate.value !== 'string'
|
|
22434
|
+
) {
|
|
22435
|
+
return -1;
|
|
22436
|
+
}
|
|
22437
|
+
|
|
22438
|
+
const test = unwrapTsExpr(conditional.test);
|
|
22439
|
+
if (test?.type !== 'BinaryExpression' || test.operator !== '===') return -1;
|
|
22440
|
+
const key = unwrapTsExpr(keyBody);
|
|
22441
|
+
if (
|
|
22442
|
+
key?.type !== 'MemberExpression' ||
|
|
22443
|
+
key.computed === true ||
|
|
22444
|
+
key.optional === true ||
|
|
22445
|
+
key.object?.type !== 'Identifier' ||
|
|
22446
|
+
key.object.name !== itemName ||
|
|
22447
|
+
key.property?.type !== 'Identifier'
|
|
22448
|
+
) {
|
|
22449
|
+
return -1;
|
|
22450
|
+
}
|
|
22451
|
+
const keyProperty = key.property.name;
|
|
22452
|
+
const isItemKey = (expression) => {
|
|
22453
|
+
const member = unwrapTsExpr(expression);
|
|
22454
|
+
return (
|
|
22455
|
+
member?.type === 'MemberExpression' &&
|
|
22456
|
+
member.computed !== true &&
|
|
22457
|
+
member.optional !== true &&
|
|
22458
|
+
member.object?.type === 'Identifier' &&
|
|
22459
|
+
member.object.name === itemName &&
|
|
22460
|
+
member.property?.type === 'Identifier' &&
|
|
22461
|
+
member.property.name === keyProperty
|
|
22462
|
+
);
|
|
22463
|
+
};
|
|
22464
|
+
const left = unwrapTsExpr(test.left);
|
|
22465
|
+
const right = unwrapTsExpr(test.right);
|
|
22466
|
+
const selected = isItemKey(left) ? right : isItemKey(right) ? left : null;
|
|
22467
|
+
if (
|
|
22468
|
+
selected?.type !== 'Identifier' ||
|
|
22469
|
+
selected.name === itemName ||
|
|
22470
|
+
!ctx.currentComponentLocals?.has(selected.name)
|
|
22471
|
+
) {
|
|
22472
|
+
return -1;
|
|
22473
|
+
}
|
|
22474
|
+
// Ignore this exact comparison operand, then use the existing scope-aware
|
|
22475
|
+
// reference walker to detect any other capture of the same outer binding.
|
|
22476
|
+
// Shadowed callback parameters therefore stay harmless while a real event
|
|
22477
|
+
// closure over the selection correctly disables the specialization.
|
|
22478
|
+
if (
|
|
22479
|
+
collectFreeIdentifiers(b.block(subStmts), new Set([itemName]), new Set([selected])).has(
|
|
22480
|
+
selected.name,
|
|
22481
|
+
)
|
|
22482
|
+
) {
|
|
22483
|
+
return -1;
|
|
22484
|
+
}
|
|
22485
|
+
return runtimeDepNames.indexOf(selected.name);
|
|
22486
|
+
}
|
|
22487
|
+
|
|
21199
22488
|
function makeForCall(node, ctx, inlinedSubs, parentNs = 'html', cssHash = null) {
|
|
21200
22489
|
// `@for await (...)` (async iteration) has no meaning for the runtime's
|
|
21201
22490
|
// synchronous keyed reconciler. The TSRX parser currently rejects the surface
|
|
@@ -21345,16 +22634,16 @@ function makeForCall(node, ctx, inlinedSubs, parentNs = 'html', cssHash = null)
|
|
|
21345
22634
|
// - PURE: body closes over nothing parent-reactive, no hooks, no comps,
|
|
21346
22635
|
// no control flow. Reconciler skips renderBlock when item ref + index
|
|
21347
22636
|
// 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.
|
|
22637
|
+
// - DEP-PURE: body DOES close over parent locals but has no hooks or opaque
|
|
22638
|
+
// components. Proven host-only conditional content is permitted when the
|
|
22639
|
+
// runtime preserves the row's active scope. The compiler emits an explicit
|
|
22640
|
+
// deps array so the reconciler can do ONE equality check per parent render
|
|
22641
|
+
// and, if unchanged, skip item-ref-and-index-stable survivors entirely.
|
|
21354
22642
|
// - NORMAL: anything else → body runs every render.
|
|
21355
22643
|
let pure = false;
|
|
21356
22644
|
const depNames = [];
|
|
21357
22645
|
let depEligible = false;
|
|
22646
|
+
let requiresScope = false;
|
|
21358
22647
|
let itemMemo = false;
|
|
21359
22648
|
let itemMemoContextAware = false;
|
|
21360
22649
|
let itemMemoWitnesses = [];
|
|
@@ -21528,8 +22817,33 @@ function makeForCall(node, ctx, inlinedSubs, parentNs = 'html', cssHash = null)
|
|
|
21528
22817
|
// survivor shortcut has no compiler-cache epoch cell to consult).
|
|
21529
22818
|
if (itemMemoContextAware && autoMemoDeps === null) itemMemo = false;
|
|
21530
22819
|
const hostPure = !hasParentClosure && !hasHook && !hasNestedComp && !hasRenderCall;
|
|
22820
|
+
const structuredHostDepEligible =
|
|
22821
|
+
ctx.autoMemo === true &&
|
|
22822
|
+
hasNestedComp &&
|
|
22823
|
+
hasParentClosure &&
|
|
22824
|
+
!hasHook &&
|
|
22825
|
+
!hasRenderCall &&
|
|
22826
|
+
node.nativeArrayMap === undefined &&
|
|
22827
|
+
autoMemoDeps !== null &&
|
|
22828
|
+
autoMemoDeps.every((name) => seenDeps.has(name) || ctx.importedNames.has(name)) &&
|
|
22829
|
+
!containsAutoMemoContextRead(bodyAst, ctx) &&
|
|
22830
|
+
hasOnlyHostConditionalItemBodies(subStmts);
|
|
22831
|
+
if (structuredHostDepEligible) {
|
|
22832
|
+
// The whole-list cache already witnesses imported projections. A row
|
|
22833
|
+
// survivor now makes the same decision independently, so its existing
|
|
22834
|
+
// dependency tuple must witness those live bindings as well.
|
|
22835
|
+
for (const name of autoMemoDeps) {
|
|
22836
|
+
if (seenDeps.has(name)) continue;
|
|
22837
|
+
seenDeps.add(name);
|
|
22838
|
+
depNames.push(name);
|
|
22839
|
+
}
|
|
22840
|
+
}
|
|
21531
22841
|
const hostDepEligible =
|
|
21532
|
-
!hostPure &&
|
|
22842
|
+
!hostPure &&
|
|
22843
|
+
!hasHook &&
|
|
22844
|
+
hasParentClosure &&
|
|
22845
|
+
(!hasNestedComp || structuredHostDepEligible) &&
|
|
22846
|
+
!hasRenderCall;
|
|
21533
22847
|
if (itemMemo && itemMemoWitnesses.length > 0) {
|
|
21534
22848
|
pure = hostPure;
|
|
21535
22849
|
depEligible = hostDepEligible;
|
|
@@ -21538,6 +22852,7 @@ function makeForCall(node, ctx, inlinedSubs, parentNs = 'html', cssHash = null)
|
|
|
21538
22852
|
pure = hostPure || (itemMemo && depNames.length === 0);
|
|
21539
22853
|
depEligible = !pure && !hasHook && (hostDepEligible || (itemMemo && depNames.length > 0));
|
|
21540
22854
|
}
|
|
22855
|
+
requiresScope = depEligible && structuredHostDepEligible;
|
|
21541
22856
|
depNames.sort();
|
|
21542
22857
|
}
|
|
21543
22858
|
|
|
@@ -21565,6 +22880,23 @@ function makeForCall(node, ctx, inlinedSubs, parentNs = 'html', cssHash = null)
|
|
|
21565
22880
|
envNames === null
|
|
21566
22881
|
? depNames
|
|
21567
22882
|
: [...envNames, ...depNames.filter((name) => !envNames.includes(name))];
|
|
22883
|
+
// Restrict targeted invalidation to the existing production-only pure-render
|
|
22884
|
+
// contract. The whole-list memo proof rules out render-time mutations and
|
|
22885
|
+
// opaque state, while DEP-PURE guarantees a hookless host-only item body.
|
|
22886
|
+
// Native `.map()` uses a distinct mapSlot ABI and stays on its current path.
|
|
22887
|
+
const keyedSelectionIndex =
|
|
22888
|
+
ctx.autoMemo === true &&
|
|
22889
|
+
depEligible &&
|
|
22890
|
+
!requiresScope &&
|
|
22891
|
+
!itemMemo &&
|
|
22892
|
+
autoMemoDeps !== null &&
|
|
22893
|
+
!isDestructured &&
|
|
22894
|
+
!node.index &&
|
|
22895
|
+
!emptyStmts &&
|
|
22896
|
+
node.nativeArrayMap === undefined &&
|
|
22897
|
+
node.key != null
|
|
22898
|
+
? keyedSelectionDepIndex(itemName, keyFn.body, subStmts, runtimeDepNames, ctx)
|
|
22899
|
+
: -1;
|
|
21568
22900
|
let emptyHelperName = 'null';
|
|
21569
22901
|
if (emptyStmts) {
|
|
21570
22902
|
emptyHelperName = hoistBodyHelper(
|
|
@@ -21646,6 +22978,23 @@ function makeForCall(node, ctx, inlinedSubs, parentNs = 'html', cssHash = null)
|
|
|
21646
22978
|
}
|
|
21647
22979
|
}
|
|
21648
22980
|
|
|
22981
|
+
const ssrMarkerless = isSsrMarkerlessForItem(node);
|
|
22982
|
+
const hostRootTag = subStmts[0]?.id?.name ?? subStmts[0]?.openingElement?.name?.name;
|
|
22983
|
+
const hostMountSafe =
|
|
22984
|
+
ctx.autoMemo === true &&
|
|
22985
|
+
ctx._universalRuntimeUnit == null &&
|
|
22986
|
+
(parentNs === 'html' || (parentNs === 'opaque' && HTML_ONLY_TAGS.has(hostRootTag))) &&
|
|
22987
|
+
!emptyStmts &&
|
|
22988
|
+
!isDestructured &&
|
|
22989
|
+
!node.index &&
|
|
22990
|
+
subStmts.length === 1 &&
|
|
22991
|
+
singleRoot &&
|
|
22992
|
+
ssrMarkerless &&
|
|
22993
|
+
!containsComponentCallOrControlFlow(subStmts) &&
|
|
22994
|
+
!containsRenderCall(subStmts) &&
|
|
22995
|
+
!containsAutoMemoUnsafeStructure(subStmts) &&
|
|
22996
|
+
isHostMountSafeTree(subStmts[0]);
|
|
22997
|
+
|
|
21649
22998
|
const mapCall = node.nativeArrayMap || null;
|
|
21650
22999
|
return {
|
|
21651
23000
|
id: ctx.nextHelperId++,
|
|
@@ -21679,18 +23028,21 @@ function makeForCall(node, ctx, inlinedSubs, parentNs = 'html', cssHash = null)
|
|
|
21679
23028
|
pure,
|
|
21680
23029
|
singleRoot,
|
|
21681
23030
|
singleRootExpr,
|
|
21682
|
-
ssrMarkerless
|
|
23031
|
+
ssrMarkerless,
|
|
23032
|
+
hostMountSafe,
|
|
21683
23033
|
// The env union doubles as the deps array: emitted whenever the helpers
|
|
21684
23034
|
// capture anything (Phase 2 — the runtime stamps it as block.extra), and
|
|
21685
23035
|
// ALSO compared for the dep-pure survivor short-circuit when depEligible.
|
|
21686
23036
|
// Component-local entries remain the tuple prefix the helpers destructure;
|
|
21687
23037
|
// any appended import witnesses are comparison-only.
|
|
21688
23038
|
depEligible,
|
|
23039
|
+
requiresScope,
|
|
21689
23040
|
itemMemoWitnesses,
|
|
21690
23041
|
itemMemoFlags,
|
|
21691
23042
|
autoMemoDeps,
|
|
21692
23043
|
autoMemoWitnesses,
|
|
21693
23044
|
autoMemoContextAware,
|
|
23045
|
+
keyedSelectionIndex,
|
|
21694
23046
|
depNames: runtimeDepNames,
|
|
21695
23047
|
// True only when the header binds NO `index <name>` — the body then can't
|
|
21696
23048
|
// observe an item's position, so a pure reorder (same item ref, position
|
|
@@ -21894,10 +23246,41 @@ function bodyContainsJsx(node) {
|
|
|
21894
23246
|
|
|
21895
23247
|
/** @param {TemplateIR} ast */
|
|
21896
23248
|
function allocTemplate(ctx, ast, ns = 0, frag = 0) {
|
|
23249
|
+
const { html, origins } = serializeTemplateIr(ast);
|
|
23250
|
+
const intern = !ctx.hmr && !ctx.dev && !ctx.profile;
|
|
23251
|
+
const bucket = intern ? ctx.internedTemplates.get(html) : undefined;
|
|
23252
|
+
const existing = Array.isArray(bucket)
|
|
23253
|
+
? bucket.find((template) => template.ns === ns && template.frag === frag)
|
|
23254
|
+
: bucket?.ns === ns && bucket.frag === frag
|
|
23255
|
+
? bucket
|
|
23256
|
+
: undefined;
|
|
23257
|
+
if (existing !== undefined) {
|
|
23258
|
+
if (ctx.inspect && origins !== null && existing.origins !== null) {
|
|
23259
|
+
for (const origin of origins) {
|
|
23260
|
+
const canonical = existing.origins.find(
|
|
23261
|
+
(entry) =>
|
|
23262
|
+
entry.kind === origin.kind && entry.start === origin.start && entry.end === origin.end,
|
|
23263
|
+
);
|
|
23264
|
+
if (canonical !== undefined) {
|
|
23265
|
+
registerOriginAlias(
|
|
23266
|
+
ctx,
|
|
23267
|
+
{ start: origin.srcStart, end: origin.srcEnd },
|
|
23268
|
+
{ start: canonical.srcStart },
|
|
23269
|
+
);
|
|
23270
|
+
}
|
|
23271
|
+
}
|
|
23272
|
+
}
|
|
23273
|
+
return existing.name;
|
|
23274
|
+
}
|
|
21897
23275
|
const id = ctx.nextTemplateId++;
|
|
21898
23276
|
const name = `_t$${id}`;
|
|
21899
|
-
const { html, origins }
|
|
21900
|
-
ctx.hoistedTemplates.push(
|
|
23277
|
+
const template = { name, ast, html, ns, frag, origins };
|
|
23278
|
+
ctx.hoistedTemplates.push(template);
|
|
23279
|
+
if (intern) {
|
|
23280
|
+
if (bucket === undefined) ctx.internedTemplates.set(html, template);
|
|
23281
|
+
else if (Array.isArray(bucket)) bucket.push(template);
|
|
23282
|
+
else ctx.internedTemplates.set(html, [bucket, template]);
|
|
23283
|
+
}
|
|
21901
23284
|
return name;
|
|
21902
23285
|
}
|
|
21903
23286
|
|