octane 0.1.16 → 0.1.17
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 +161 -19
- package/dist/devtools-hook.d.ts +1 -1
- package/dist/devtools-hook.js +1 -1
- package/dist/runtime.d.ts +9 -2
- package/dist/runtime.js +99 -39
- package/dist/runtime.server.d.ts +43 -1
- package/dist/runtime.server.js +34 -7
- package/package.json +1 -1
package/dist/compiler/compile.js
CHANGED
|
@@ -2174,6 +2174,96 @@ function rewriteAutoCallback(stmt, stable, componentLocals, ctx) {
|
|
|
2174
2174
|
return modified ? { ...stmt, declarations: newDecls } : stmt;
|
|
2175
2175
|
}
|
|
2176
2176
|
|
|
2177
|
+
// Auto-calculation: lower a derived `const X = <expr containing a render-time
|
|
2178
|
+
// call>` to `useMemo(() => <expr>, [deps])` so its identity is stable while its
|
|
2179
|
+
// inputs are. Without this, an idiomatic `const visible = todos.filter(...)`
|
|
2180
|
+
// allocates a fresh array every render, and every downstream consumer keyed on
|
|
2181
|
+
// its identity — an autoMemo region's dependency tuple, a memo() child's prop —
|
|
2182
|
+
// misses unconditionally. The region cache is only as good as the stability of
|
|
2183
|
+
// what it keys on.
|
|
2184
|
+
//
|
|
2185
|
+
// A calculation is admitted on exactly the callee rule that governs regions
|
|
2186
|
+
// (plainCalleeIsMemoizable): the receiver of a member call can return a new
|
|
2187
|
+
// answer while its own identity holds, and that hazard does not become safe
|
|
2188
|
+
// because the author named the result. `virtualizer.getVirtualItems()` in
|
|
2189
|
+
// examples/pulseboard is the same shape as `header.column.getIsSorted()` — its
|
|
2190
|
+
// window moves with scroll while the virtualizer instance stays put — so
|
|
2191
|
+
// caching it froze a virtualized list mid-scroll. One rule, both paths.
|
|
2192
|
+
//
|
|
2193
|
+
// The cost of that is real and accepted: `const visible = todos.filter(...)`
|
|
2194
|
+
// stays uncached, because `todos.filter` is a member call and this analysis
|
|
2195
|
+
// cannot yet prove the receiver is an immutable snapshot rather than a live
|
|
2196
|
+
// object. Recovering it needs receiver provenance (does the root trace to a
|
|
2197
|
+
// state binding?), which is a separate change with its own evidence.
|
|
2198
|
+
//
|
|
2199
|
+
// Scope: single-declarator `const` with an Identifier id whose init reaches a
|
|
2200
|
+
// render-time call (isPropCreationExpr — no JSX, no hook-shaped calls), whose
|
|
2201
|
+
// every call is an admitted projection, whose value the render tree actually
|
|
2202
|
+
// reads (memoizing a value nothing renders only adds cells), and whose
|
|
2203
|
+
// dependencies are all component locals. Bodies that already went through Pass
|
|
2204
|
+
// A′ arrive with hook-shaped inits and are skipped.
|
|
2205
|
+
function rewriteAutoCalculation(stmt, componentLocals, renderReadNames, ctx) {
|
|
2206
|
+
if (stmt.type !== 'VariableDeclaration' || stmt.kind !== 'const') return stmt;
|
|
2207
|
+
if (stmt.declarations?.length !== 1) return stmt;
|
|
2208
|
+
const decl = stmt.declarations[0];
|
|
2209
|
+
if (!decl || decl.id?.type !== 'Identifier' || !decl.init) return stmt;
|
|
2210
|
+
if (!renderReadNames.has(decl.id.name)) return stmt;
|
|
2211
|
+
const init = unwrapTsExpr(decl.init);
|
|
2212
|
+
// An arrow/function init is auto-callback's job, not ours.
|
|
2213
|
+
if (FN_TYPES.has(init?.type)) return stmt;
|
|
2214
|
+
if (!isPropCreationExpr(init, ctx)) return stmt;
|
|
2215
|
+
// Every call reached during render must be a proven value projection — the
|
|
2216
|
+
// same admission the region cache uses. A member call fails closed.
|
|
2217
|
+
if (containsRenderCall([init], ctx)) return stmt;
|
|
2218
|
+
const deps = [];
|
|
2219
|
+
const seen = new Set();
|
|
2220
|
+
for (const name of collectFreeIdentifiers(init, [])) {
|
|
2221
|
+
if (name === decl.id.name) return stmt; // self-referential; leave alone
|
|
2222
|
+
if (!componentLocals.has(name)) continue;
|
|
2223
|
+
if (seen.has(name)) continue;
|
|
2224
|
+
seen.add(name);
|
|
2225
|
+
deps.push(name);
|
|
2226
|
+
}
|
|
2227
|
+
// Every free name must be a witnessed component local. An unwitnessed
|
|
2228
|
+
// module/global read would make the cache freeze on something no dependency
|
|
2229
|
+
// can observe, which is a different bargain from the one above.
|
|
2230
|
+
for (const name of collectFreeIdentifiers(init, [])) {
|
|
2231
|
+
if (
|
|
2232
|
+
!componentLocals.has(name) &&
|
|
2233
|
+
!ctx.moduleFunctionDeclarations?.has(name) &&
|
|
2234
|
+
!ctx.importedNames?.has(name)
|
|
2235
|
+
) {
|
|
2236
|
+
return stmt;
|
|
2237
|
+
}
|
|
2238
|
+
}
|
|
2239
|
+
return {
|
|
2240
|
+
...stmt,
|
|
2241
|
+
declarations: [
|
|
2242
|
+
{
|
|
2243
|
+
...decl,
|
|
2244
|
+
init: inheritOriginLoc(
|
|
2245
|
+
b.call(
|
|
2246
|
+
{ ...b.id('useMemo'), _octaneGenerated: true },
|
|
2247
|
+
b.arrow([], init),
|
|
2248
|
+
b.array(deps.map((n) => b.id(n))),
|
|
2249
|
+
),
|
|
2250
|
+
init,
|
|
2251
|
+
),
|
|
2252
|
+
},
|
|
2253
|
+
],
|
|
2254
|
+
};
|
|
2255
|
+
}
|
|
2256
|
+
|
|
2257
|
+
// Names the render tree actually reads, resolved against the scopes the
|
|
2258
|
+
// statement walker cannot see (nested functions, loops, `@for` item bindings,
|
|
2259
|
+
// directive-arm blocks) so a shadowing inner binding never nominates an
|
|
2260
|
+
// unrelated body const.
|
|
2261
|
+
function collectRenderReadNames(jsxNodes, ctx) {
|
|
2262
|
+
const into = new Set();
|
|
2263
|
+
collectUseArgumentRoots(jsxNodes, ctx, into, true, true);
|
|
2264
|
+
return into;
|
|
2265
|
+
}
|
|
2266
|
+
|
|
2177
2267
|
function allocAutoMemoCell(ctx, dependencyCount) {
|
|
2178
2268
|
const base = ctx.currentAutoMemoOffset || 0;
|
|
2179
2269
|
const init = base + dependencyCount;
|
|
@@ -2858,10 +2948,19 @@ function containsRenderCall(stmts, memoCtx = null) {
|
|
|
2858
2948
|
return found;
|
|
2859
2949
|
}
|
|
2860
2950
|
|
|
2861
|
-
|
|
2951
|
+
// A hook call is identified by naming convention — the same signal React and
|
|
2952
|
+
// React Compiler key on — plus React's own `unstable_` staging prefix, which
|
|
2953
|
+
// bindings mirror (`unstable_useRouterState` in @octanejs/remix-router). The
|
|
2954
|
+
// prefix is enumerated rather than matched as "any `_use`" so an ordinary
|
|
2955
|
+
// helper cannot be mistaken for a hook by spelling alone.
|
|
2956
|
+
//
|
|
2957
|
+
// Getting this wrong in the permissive direction is not a staleness bug: a
|
|
2958
|
+
// cache wrapped around a hook call freezes its subscription and its state cell
|
|
2959
|
+
// for the life of the component.
|
|
2960
|
+
const HOOK_NAME_CONVENTION_RE = /^(?:unstable_)?use(?:$|[A-Z])/;
|
|
2862
2961
|
|
|
2863
2962
|
function isHookCalleeName(name) {
|
|
2864
|
-
return
|
|
2963
|
+
return HOOK_NAME_CONVENTION_RE.test(name);
|
|
2865
2964
|
}
|
|
2866
2965
|
|
|
2867
2966
|
/**
|
|
@@ -9386,6 +9485,19 @@ function compileFunctionBody(node, ctx, name, parentNs = 'html', cssHash = null,
|
|
|
9386
9485
|
creations,
|
|
9387
9486
|
puParamNames,
|
|
9388
9487
|
);
|
|
9488
|
+
// Auto-calculation runs AFTER Pass A′ so use()-fed chains keep their
|
|
9489
|
+
// Symbol-slot treatment (their rewritten inits are hook-shaped and are
|
|
9490
|
+
// skipped here), and BEFORE Pass A/B so the numeric-slot useMemo it mints is
|
|
9491
|
+
// lowered by the authored inline tier rather than re-wrapped. Client only:
|
|
9492
|
+
// a server render evaluates each body once, so a cache is pure overhead.
|
|
9493
|
+
if (ctx.currentComponentLocals && ctx.mode !== 'server') {
|
|
9494
|
+
const renderReadNames = collectRenderReadNames(jsxNodes, ctx);
|
|
9495
|
+
if (renderReadNames.size > 0) {
|
|
9496
|
+
workingStatements = workingStatements.map((statement) =>
|
|
9497
|
+
rewriteAutoCalculation(statement, ctx.currentComponentLocals, renderReadNames, ctx),
|
|
9498
|
+
);
|
|
9499
|
+
}
|
|
9500
|
+
}
|
|
9389
9501
|
workingStatements = parallelUseMemoizePass(workingStatements, ctx, name, creations, [], null);
|
|
9390
9502
|
jsxNodes = parallelUseWalkJsx(jsxNodes, ctx, name, creations, warmChildren, [], new Set());
|
|
9391
9503
|
const warm = buildWarmArtifacts(node, ctx, name, creations, warmChildren);
|
|
@@ -10210,14 +10322,17 @@ function isPropCreationExpr(expr, ctx) {
|
|
|
10210
10322
|
function isHookishCallee(callee) {
|
|
10211
10323
|
if (callee.type === 'Identifier') {
|
|
10212
10324
|
return (
|
|
10213
|
-
|
|
10325
|
+
HOOK_NAME_CONVENTION_RE.test(callee.name) ||
|
|
10326
|
+
ctx?._parallelUseAliases?.has(callee.name) === true
|
|
10214
10327
|
);
|
|
10215
10328
|
}
|
|
10216
10329
|
if (
|
|
10217
10330
|
(callee.type === 'MemberExpression' || callee.type === 'OptionalMemberExpression') &&
|
|
10218
10331
|
!callee.computed
|
|
10219
10332
|
) {
|
|
10220
|
-
return
|
|
10333
|
+
return (
|
|
10334
|
+
callee.property.type === 'Identifier' && HOOK_NAME_CONVENTION_RE.test(callee.property.name)
|
|
10335
|
+
);
|
|
10221
10336
|
}
|
|
10222
10337
|
return false;
|
|
10223
10338
|
}
|
|
@@ -10632,8 +10747,14 @@ function memoizeUseFedCreations(stmts, jsxNodes, ctx, componentName, creations,
|
|
|
10632
10747
|
// Deep scan (nested functions and render-tree nodes included — a consumer
|
|
10633
10748
|
// anywhere makes stability load-bearing) for `use(<arg>)` arguments' free
|
|
10634
10749
|
// identifier roots.
|
|
10635
|
-
function collectUseArgumentRoots(
|
|
10636
|
-
|
|
10750
|
+
function collectUseArgumentRoots(
|
|
10751
|
+
root,
|
|
10752
|
+
ctx,
|
|
10753
|
+
into,
|
|
10754
|
+
startInRenderTree = false,
|
|
10755
|
+
seedRenderReads = false,
|
|
10756
|
+
) {
|
|
10757
|
+
walk(root, EMPTY_BOUND_SET, startInRenderTree, false);
|
|
10637
10758
|
|
|
10638
10759
|
// `bound` carries every name introduced by a scope the candidate collector
|
|
10639
10760
|
// cannot reach: nested-function params + internal declarations, loop
|
|
@@ -10645,10 +10766,10 @@ function collectUseArgumentRoots(root, ctx, into, startInRenderTree = false) {
|
|
|
10645
10766
|
// into memoization (which would wrongly stop it recreating per render).
|
|
10646
10767
|
// Over-binding is the safe direction: a dropped seed just leaves that
|
|
10647
10768
|
// chain unmemoized — today's behavior.
|
|
10648
|
-
function walk(n, bound, inRenderTree) {
|
|
10769
|
+
function walk(n, bound, inRenderTree, deferred) {
|
|
10649
10770
|
if (n == null || typeof n !== 'object') return;
|
|
10650
10771
|
if (Array.isArray(n)) {
|
|
10651
|
-
for (const child of n) walk(child, bound, inRenderTree);
|
|
10772
|
+
for (const child of n) walk(child, bound, inRenderTree, deferred);
|
|
10652
10773
|
return;
|
|
10653
10774
|
}
|
|
10654
10775
|
if (
|
|
@@ -10665,7 +10786,10 @@ function collectUseArgumentRoots(root, ctx, into, startInRenderTree = false) {
|
|
|
10665
10786
|
const inner = new Set(bound);
|
|
10666
10787
|
for (const param of n.params || []) collectPatternNames(param, inner);
|
|
10667
10788
|
collectScopeDeclaredNames(n.body, inner);
|
|
10668
|
-
|
|
10789
|
+
// A function VALUE runs at invoke time, not while the template renders,
|
|
10790
|
+
// so names it reads are not render reads. use()-argument seeding still
|
|
10791
|
+
// descends (a use() inside a render prop is a real suspension point).
|
|
10792
|
+
walk(n.body, inner, inRenderTree, true);
|
|
10669
10793
|
return;
|
|
10670
10794
|
}
|
|
10671
10795
|
if (LOOP_TYPES.has(n.type)) {
|
|
@@ -10674,7 +10798,7 @@ function collectUseArgumentRoots(root, ctx, into, startInRenderTree = false) {
|
|
|
10674
10798
|
for (const key in n) {
|
|
10675
10799
|
if (key === 'loc' || key === 'start' || key === 'end' || key.startsWith('_octane'))
|
|
10676
10800
|
continue;
|
|
10677
|
-
walk(n[key], inner, inRenderTree);
|
|
10801
|
+
walk(n[key], inner, inRenderTree, deferred);
|
|
10678
10802
|
}
|
|
10679
10803
|
return;
|
|
10680
10804
|
}
|
|
@@ -10682,7 +10806,7 @@ function collectUseArgumentRoots(root, ctx, into, startInRenderTree = false) {
|
|
|
10682
10806
|
const inner = new Set(bound);
|
|
10683
10807
|
if (n.param) collectPatternNames(n.param, inner);
|
|
10684
10808
|
collectScopeDeclaredNames(n.body, inner);
|
|
10685
|
-
walk(n.body, inner, inRenderTree);
|
|
10809
|
+
walk(n.body, inner, inRenderTree, deferred);
|
|
10686
10810
|
return;
|
|
10687
10811
|
}
|
|
10688
10812
|
if (n.type === 'JSXForExpression') {
|
|
@@ -10696,16 +10820,32 @@ function collectUseArgumentRoots(root, ctx, into, startInRenderTree = false) {
|
|
|
10696
10820
|
collectPatternNames(n.left, inner);
|
|
10697
10821
|
}
|
|
10698
10822
|
if (n.index) collectPatternNames(n.index, inner);
|
|
10699
|
-
walk(n.right, bound, true);
|
|
10700
|
-
walk(n.key, inner, true);
|
|
10701
|
-
walk(n.body, inner, true);
|
|
10702
|
-
walk(n.empty, bound, true);
|
|
10823
|
+
walk(n.right, bound, true, deferred);
|
|
10824
|
+
walk(n.key, inner, true, deferred);
|
|
10825
|
+
walk(n.body, inner, true, deferred);
|
|
10826
|
+
walk(n.empty, bound, true, deferred);
|
|
10827
|
+
return;
|
|
10828
|
+
}
|
|
10829
|
+
if (
|
|
10830
|
+
seedRenderReads &&
|
|
10831
|
+
inRenderTree &&
|
|
10832
|
+
!deferred &&
|
|
10833
|
+
n.type === 'Identifier' &&
|
|
10834
|
+
!bound.has(n.name)
|
|
10835
|
+
) {
|
|
10836
|
+
into.add(n.name);
|
|
10837
|
+
}
|
|
10838
|
+
if ((n.type === 'MemberExpression' || n.type === 'OptionalMemberExpression') && !n.computed) {
|
|
10839
|
+
// `row.length` reads `row`, not a binding called `length`. Skipping the
|
|
10840
|
+
// property also cannot hide a use() call: a non-computed property is an
|
|
10841
|
+
// Identifier by construction.
|
|
10842
|
+
walk(n.object, bound, inRenderTree, deferred);
|
|
10703
10843
|
return;
|
|
10704
10844
|
}
|
|
10705
10845
|
if (inRenderTree && n.type === 'BlockStatement') {
|
|
10706
10846
|
const inner = new Set(bound);
|
|
10707
10847
|
collectScopeDeclaredNames(n, inner);
|
|
10708
|
-
for (const child of n.body || []) walk(child, inner, true);
|
|
10848
|
+
for (const child of n.body || []) walk(child, inner, true, deferred);
|
|
10709
10849
|
return;
|
|
10710
10850
|
}
|
|
10711
10851
|
// JSX reached at a setup VALUE position (`const el = <div>{use(x)}</div>`)
|
|
@@ -10716,7 +10856,7 @@ function collectUseArgumentRoots(root, ctx, into, startInRenderTree = false) {
|
|
|
10716
10856
|
(n.type.startsWith('JSX') || n.type === 'Element' || n.type === 'Tsrx'));
|
|
10717
10857
|
for (const key in n) {
|
|
10718
10858
|
if (key === 'loc' || key === 'start' || key === 'end' || key.startsWith('_octane')) continue;
|
|
10719
|
-
walk(n[key], bound, nextInRenderTree);
|
|
10859
|
+
walk(n[key], bound, nextInRenderTree, deferred);
|
|
10720
10860
|
}
|
|
10721
10861
|
}
|
|
10722
10862
|
}
|
|
@@ -16057,9 +16197,11 @@ function propertyIsEnumerableCall(objNode, name) {
|
|
|
16057
16197
|
);
|
|
16058
16198
|
}
|
|
16059
16199
|
|
|
16060
|
-
// `__s.cleanups.push(<fn>)
|
|
16200
|
+
// `(__s.cleanups ??= []).push(<fn>)` — the scope's cleanup array is lazily allocated (most
|
|
16201
|
+
// scopes never register one), so the registration site owns creating it.
|
|
16061
16202
|
function cleanupsPush(fnNode) {
|
|
16062
|
-
|
|
16203
|
+
const lazyArray = b.assignment('??=', b.member(b.id('__s'), 'cleanups'), b.array([]));
|
|
16204
|
+
return b.stmt(b.call(b.member(lazyArray, 'push'), fnNode));
|
|
16063
16205
|
}
|
|
16064
16206
|
|
|
16065
16207
|
// Mount for a DEFERRED property-write binding: store the element ref + seed the
|
package/dist/devtools-hook.d.ts
CHANGED
package/dist/devtools-hook.js
CHANGED
|
@@ -63,7 +63,7 @@ function buildNode(scope, key) {
|
|
|
63
63
|
id,
|
|
64
64
|
name: nameOf(scope, key),
|
|
65
65
|
kind: scope.kind ?? "component",
|
|
66
|
-
children: scope.children.map((c) => buildNode(c.scope, c.key))
|
|
66
|
+
children: scope.children === null ? [] : scope.children.map((c) => buildNode(c.scope, c.key))
|
|
67
67
|
};
|
|
68
68
|
}
|
|
69
69
|
const hook = {
|
package/dist/runtime.d.ts
CHANGED
|
@@ -52,7 +52,12 @@ export interface Scope {
|
|
|
52
52
|
* `undefined` when null, identical to a Map.get miss.
|
|
53
53
|
*/
|
|
54
54
|
hooks: Map<HookSlot, any> | null;
|
|
55
|
-
|
|
55
|
+
/**
|
|
56
|
+
* Teardown callbacks (ref detaches, listener removal, slot cleanup). Lazily allocated by
|
|
57
|
+
* `pushCleanup` — most scopes never register one, so the array is only paid for on demand,
|
|
58
|
+
* matching `hooks` / `effectSlots` / `_slots`.
|
|
59
|
+
*/
|
|
60
|
+
cleanups: Cleanup[] | null;
|
|
56
61
|
/**
|
|
57
62
|
* This scope's effect slots in hook DECLARATION order (first-enqueue order —
|
|
58
63
|
* the order the hooks ran in the scope's first render). unmountScope walks it
|
|
@@ -69,8 +74,10 @@ export interface Scope {
|
|
|
69
74
|
* (NOT a Map): iteration is a plain indexed for-loop, and lookups are linear
|
|
70
75
|
* scans — faster than `Map.get` for the typical N ≤ 8 case (most components
|
|
71
76
|
* have a handful of static sub-component calls at most).
|
|
77
|
+
*
|
|
78
|
+
* Lazily allocated when the first child scope registers: a leaf component has none.
|
|
72
79
|
*/
|
|
73
|
-
children: ChildScope[];
|
|
80
|
+
children: ChildScope[] | null;
|
|
74
81
|
mounted: boolean;
|
|
75
82
|
/**
|
|
76
83
|
* Slot objects owned by this scope (ifBlockSlot, forBlockSlot, etc.).
|
package/dist/runtime.js
CHANGED
|
@@ -664,7 +664,7 @@ function scheduleRender(block) {
|
|
|
664
664
|
);
|
|
665
665
|
}
|
|
666
666
|
const renderPhaseSelf = CURRENT_BLOCK === block;
|
|
667
|
-
const renderPhaseOther = CURRENT_BLOCK !== null && !renderPhaseSelf && TRANSITION_LISTENER_PUBLISH_DEPTH === 0;
|
|
667
|
+
const renderPhaseOther = CURRENT_BLOCK !== null && !renderPhaseSelf && TRANSITION_LISTENER_PUBLISH_DEPTH === 0 && EFFECT_EVENT_LIFECYCLE_DEPTH === 0;
|
|
668
668
|
if (renderPhaseOther) {
|
|
669
669
|
block.crossRenderUpdate = true;
|
|
670
670
|
warnCrossComponentRenderUpdate(block, CURRENT_BLOCK);
|
|
@@ -1523,9 +1523,9 @@ class BlockImpl {
|
|
|
1523
1523
|
this.currentRenderDeferred = false;
|
|
1524
1524
|
this.inactive = false;
|
|
1525
1525
|
this.hooks = null;
|
|
1526
|
-
this.cleanups =
|
|
1526
|
+
this.cleanups = null;
|
|
1527
1527
|
this.effectSlots = null;
|
|
1528
|
-
this.children =
|
|
1528
|
+
this.children = null;
|
|
1529
1529
|
this._slots = null;
|
|
1530
1530
|
this.refFields = null;
|
|
1531
1531
|
this.$$ctxValues = null;
|
|
@@ -1574,9 +1574,9 @@ class ScopeImpl {
|
|
|
1574
1574
|
this.block = block;
|
|
1575
1575
|
this.parent = parent;
|
|
1576
1576
|
this.hooks = null;
|
|
1577
|
-
this.cleanups =
|
|
1577
|
+
this.cleanups = null;
|
|
1578
1578
|
this.effectSlots = null;
|
|
1579
|
-
this.children =
|
|
1579
|
+
this.children = null;
|
|
1580
1580
|
this._slots = null;
|
|
1581
1581
|
this.refFields = null;
|
|
1582
1582
|
this.slots = [];
|
|
@@ -1868,7 +1868,7 @@ function componentSlotLite(parentScope, slotKey, host, comp, props, anchor) {
|
|
|
1868
1868
|
});
|
|
1869
1869
|
}
|
|
1870
1870
|
parentScope.slots[slotKey] = scope;
|
|
1871
|
-
parentScope.children.push({ key: slotKey, scope });
|
|
1871
|
+
(parentScope.children ??= []).push({ key: slotKey, scope });
|
|
1872
1872
|
} else {
|
|
1873
1873
|
}
|
|
1874
1874
|
const prevScope = CURRENT_SCOPE;
|
|
@@ -1984,15 +1984,17 @@ function unmountScope(scope, detachDom = true) {
|
|
|
1984
1984
|
}
|
|
1985
1985
|
function unmountScopeChildrenAndSlots(scope, detachDom) {
|
|
1986
1986
|
const c = scope.cleanups;
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1987
|
+
if (c !== null)
|
|
1988
|
+
for (let i = c.length - 1; i >= 0; i--) {
|
|
1989
|
+
try {
|
|
1990
|
+
runEffectLifecycleCallback(c[i]);
|
|
1991
|
+
} catch (err) {
|
|
1992
|
+
reportTeardownError(err);
|
|
1993
|
+
}
|
|
1992
1994
|
}
|
|
1993
|
-
}
|
|
1994
1995
|
const children = scope.children;
|
|
1995
|
-
|
|
1996
|
+
if (children !== null)
|
|
1997
|
+
for (let i = 0, n = children.length; i < n; i++) unmountScope(children[i].scope, detachDom);
|
|
1996
1998
|
const slots = scope._slots;
|
|
1997
1999
|
if (slots !== null) {
|
|
1998
2000
|
for (let i = 0, n = slots.length; i < n; i++) {
|
|
@@ -2428,7 +2430,7 @@ function useEffectEvent(fn, slot) {
|
|
|
2428
2430
|
s = { impl: fn, active: true };
|
|
2429
2431
|
ensureHooks(scope).set(slot, s);
|
|
2430
2432
|
const cell2 = s;
|
|
2431
|
-
scope.cleanups.push(() => {
|
|
2433
|
+
(scope.cleanups ??= []).push(() => {
|
|
2432
2434
|
cell2.active = false;
|
|
2433
2435
|
});
|
|
2434
2436
|
} else {
|
|
@@ -2459,6 +2461,15 @@ function createContext(defaultValue) {
|
|
|
2459
2461
|
}
|
|
2460
2462
|
scope.$$ctxValues.set(ctx, props.value);
|
|
2461
2463
|
if (props.children != null) {
|
|
2464
|
+
const dialect = typeof props.children === "function" ? 1 : 2;
|
|
2465
|
+
const previous = scope.hooks?.get(CHILDREN_DIALECT_SLOT);
|
|
2466
|
+
if (previous !== dialect) {
|
|
2467
|
+
if (previous !== void 0) {
|
|
2468
|
+
resetScopeChildren(scope);
|
|
2469
|
+
if (scope.block.disposed) return;
|
|
2470
|
+
}
|
|
2471
|
+
ensureHooks(scope).set(CHILDREN_DIALECT_SLOT, dialect);
|
|
2472
|
+
}
|
|
2462
2473
|
childrenAsBody(props.children)(void 0, scope, void 0);
|
|
2463
2474
|
}
|
|
2464
2475
|
};
|
|
@@ -2496,6 +2507,28 @@ function childrenAsBody(children) {
|
|
|
2496
2507
|
childSlot(s, 0, s.block.parentNode, children, s.block.endMarker);
|
|
2497
2508
|
};
|
|
2498
2509
|
}
|
|
2510
|
+
const CHILDREN_DIALECT_SLOT = /* @__PURE__ */ Symbol("octane.childrenDialect");
|
|
2511
|
+
function resetScopeChildren(scope) {
|
|
2512
|
+
const block = scope.block;
|
|
2513
|
+
if (TEARDOWN_DEPTH === 0) {
|
|
2514
|
+
TEARDOWN_HANDLER = findTryHandler(block.parentBlock) ?? rendererRegionTryHandler(block);
|
|
2515
|
+
}
|
|
2516
|
+
TEARDOWN_DEPTH++;
|
|
2517
|
+
try {
|
|
2518
|
+
unmountScopeChildrenAndSlots(scope, true);
|
|
2519
|
+
const start = block.startMarker;
|
|
2520
|
+
removeRange(start !== null ? start.nextSibling : block.parentNode.firstChild, block.endMarker);
|
|
2521
|
+
scope.children = null;
|
|
2522
|
+
scope.cleanups = null;
|
|
2523
|
+
scope._slots = null;
|
|
2524
|
+
scope.effectSlots = null;
|
|
2525
|
+
scope.hooks = null;
|
|
2526
|
+
scope.slots = [];
|
|
2527
|
+
scope.refFields = null;
|
|
2528
|
+
} finally {
|
|
2529
|
+
if (--TEARDOWN_DEPTH === 0) dispatchTeardownErrors();
|
|
2530
|
+
}
|
|
2531
|
+
}
|
|
2499
2532
|
const HYDRATE_ID_SLOT = /* @__PURE__ */ Symbol("octane.hydrate.id");
|
|
2500
2533
|
const HYDRATE_SETUP_SLOT = /* @__PURE__ */ Symbol("octane.hydrate.setup");
|
|
2501
2534
|
const HYDRATE_NOTIFY_SLOT = /* @__PURE__ */ Symbol("octane.hydrate.notify");
|
|
@@ -3372,7 +3405,7 @@ function bindRendererRegionOwner(props) {
|
|
|
3372
3405
|
RENDERER_REGION_DOM_OWNERS.set(root, bridge);
|
|
3373
3406
|
root.$$ctxCache?.clear();
|
|
3374
3407
|
if (previous === void 0) {
|
|
3375
|
-
root.cleanups.push(() => {
|
|
3408
|
+
(root.cleanups ??= []).push(() => {
|
|
3376
3409
|
const current = RENDERER_REGION_DOM_BINDINGS.get(root);
|
|
3377
3410
|
if (current === void 0) return;
|
|
3378
3411
|
current.release();
|
|
@@ -5270,7 +5303,7 @@ function mountFragmentRef(scope, startMarker, endMarker, ref) {
|
|
|
5270
5303
|
const fi = new FragmentInstance(scope.block, startMarker, endMarker);
|
|
5271
5304
|
fi._currentRef = ref;
|
|
5272
5305
|
queueRefAttach(scope, () => attachRef(fi._currentRef, fi));
|
|
5273
|
-
scope.cleanups.push(() => {
|
|
5306
|
+
(scope.cleanups ??= []).push(() => {
|
|
5274
5307
|
queueRefDetach(fi._currentRef, fi);
|
|
5275
5308
|
fi._destroy();
|
|
5276
5309
|
});
|
|
@@ -5889,7 +5922,7 @@ function headBlock(scope, slot, key, tag, attrs, text) {
|
|
|
5889
5922
|
}
|
|
5890
5923
|
state = { el: el2 };
|
|
5891
5924
|
scope.slots[slot] = state;
|
|
5892
|
-
scope.cleanups.push(() => {
|
|
5925
|
+
(scope.cleanups ??= []).push(() => {
|
|
5893
5926
|
state.el.remove();
|
|
5894
5927
|
scope.slots[slot] = void 0;
|
|
5895
5928
|
});
|
|
@@ -8063,9 +8096,9 @@ function hostComponent(scope, slot, tag, props, childrenBody, anchor) {
|
|
|
8063
8096
|
scope.slots[slot] = state;
|
|
8064
8097
|
const childScope = new ScopeImpl(scope, block);
|
|
8065
8098
|
state.childScope = childScope;
|
|
8066
|
-
scope.children.push({ key: slot, scope: childScope });
|
|
8099
|
+
(scope.children ??= []).push({ key: slot, scope: childScope });
|
|
8067
8100
|
block.parentNode.insertBefore(el2, anchor ?? block.endMarker);
|
|
8068
|
-
scope.cleanups.push(() => queueRefDetach(state.ref, state.el));
|
|
8101
|
+
(scope.cleanups ??= []).push(() => queueRefDetach(state.ref, state.el));
|
|
8069
8102
|
}
|
|
8070
8103
|
const el = state.el;
|
|
8071
8104
|
applyHostProps(el, props, scope, state);
|
|
@@ -8133,6 +8166,19 @@ function applyHostProps(el, props, scope, state) {
|
|
|
8133
8166
|
if (process.env.NODE_ENV !== "production") queueDevFormDiagnostic(el, scope);
|
|
8134
8167
|
}
|
|
8135
8168
|
const DEOPT_DESC = /* @__PURE__ */ Symbol("octane.deoptDesc");
|
|
8169
|
+
function hasDeoptOwnedChild(el) {
|
|
8170
|
+
let n = getFirstChild(el);
|
|
8171
|
+
while (n !== null) {
|
|
8172
|
+
const rangeEnd = n.$$portalEnd;
|
|
8173
|
+
if (rangeEnd != null) {
|
|
8174
|
+
n = nodeAfterPortalRange(n, rangeEnd);
|
|
8175
|
+
continue;
|
|
8176
|
+
}
|
|
8177
|
+
if (n.$$deoptKey !== void 0 || getDeoptDesc(n) !== void 0) return true;
|
|
8178
|
+
n = getNextSibling(n);
|
|
8179
|
+
}
|
|
8180
|
+
return false;
|
|
8181
|
+
}
|
|
8136
8182
|
function getDeoptDesc(n) {
|
|
8137
8183
|
return n[DEOPT_DESC];
|
|
8138
8184
|
}
|
|
@@ -8297,8 +8343,14 @@ function reconcileDeoptChildren(el, children, ownerBlock) {
|
|
|
8297
8343
|
}
|
|
8298
8344
|
return;
|
|
8299
8345
|
}
|
|
8346
|
+
if (next.length === 0 && activeHydration() === null && !hasDeoptOwnedChild(el)) {
|
|
8347
|
+
return;
|
|
8348
|
+
}
|
|
8300
8349
|
const owned = [];
|
|
8301
8350
|
let hasForeign = false;
|
|
8351
|
+
let hydrationOwnsUnstamped;
|
|
8352
|
+
let byKey = null;
|
|
8353
|
+
const unstamped = [];
|
|
8302
8354
|
let scan = getFirstChild(el);
|
|
8303
8355
|
while (scan !== null) {
|
|
8304
8356
|
const rangeEnd = scan.$$portalEnd;
|
|
@@ -8307,22 +8359,25 @@ function reconcileDeoptChildren(el, children, ownerBlock) {
|
|
|
8307
8359
|
scan = nodeAfterPortalRange(scan, rangeEnd);
|
|
8308
8360
|
continue;
|
|
8309
8361
|
}
|
|
8362
|
+
const stampedKey = scan.$$deoptKey;
|
|
8363
|
+
const descriptor = stampedKey === void 0 ? getDeoptDesc(scan) : void 0;
|
|
8364
|
+
if (stampedKey === void 0 && descriptor === void 0 && !(hydrationOwnsUnstamped ??= activeHydration() !== null)) {
|
|
8365
|
+
hasForeign = true;
|
|
8366
|
+
scan = getNextSibling(scan);
|
|
8367
|
+
continue;
|
|
8368
|
+
}
|
|
8310
8369
|
owned.push(scan);
|
|
8311
|
-
|
|
8312
|
-
|
|
8313
|
-
let byKey = null;
|
|
8314
|
-
const unstamped = [];
|
|
8315
|
-
for (let i = 0; i < owned.length; i++) {
|
|
8316
|
-
const n = owned[i];
|
|
8317
|
-
const k = n.$$deoptKey ?? getDeoptDesc(n)?.key;
|
|
8318
|
-
if (k != null) {
|
|
8370
|
+
const key = stampedKey ?? descriptor?.key;
|
|
8371
|
+
if (key != null) {
|
|
8319
8372
|
if (byKey === null) byKey = /* @__PURE__ */ new Map();
|
|
8320
|
-
if (!byKey.has(
|
|
8321
|
-
byKey.set(
|
|
8373
|
+
if (!byKey.has(key)) {
|
|
8374
|
+
byKey.set(key, scan);
|
|
8375
|
+
scan = getNextSibling(scan);
|
|
8322
8376
|
continue;
|
|
8323
8377
|
}
|
|
8324
8378
|
}
|
|
8325
|
-
unstamped.push(
|
|
8379
|
+
unstamped.push(scan);
|
|
8380
|
+
scan = getNextSibling(scan);
|
|
8326
8381
|
}
|
|
8327
8382
|
let up = 0;
|
|
8328
8383
|
const result = [];
|
|
@@ -8351,7 +8406,7 @@ function reconcileDeoptChildren(el, children, ownerBlock) {
|
|
|
8351
8406
|
}
|
|
8352
8407
|
for (let i = 0; i < result.length; i++) {
|
|
8353
8408
|
const want = result[i];
|
|
8354
|
-
const at = hasForeign ? liveOwnedChildAt(el, i) : existing[i] ?? null;
|
|
8409
|
+
const at = hasForeign ? liveOwnedChildAt(el, i, hydrationOwnsUnstamped === true) : existing[i] ?? null;
|
|
8355
8410
|
if (at !== want) el.insertBefore(want, at);
|
|
8356
8411
|
}
|
|
8357
8412
|
}
|
|
@@ -8360,7 +8415,7 @@ function nodeAfterPortalRange(start, end) {
|
|
|
8360
8415
|
while (m !== null && m !== end) m = getNextSibling(m);
|
|
8361
8416
|
return getNextSibling(m ?? start);
|
|
8362
8417
|
}
|
|
8363
|
-
function liveOwnedChildAt(el, index) {
|
|
8418
|
+
function liveOwnedChildAt(el, index, adoptHydrationChildren = false) {
|
|
8364
8419
|
let i = 0;
|
|
8365
8420
|
let scan = getFirstChild(el);
|
|
8366
8421
|
while (scan !== null) {
|
|
@@ -8369,6 +8424,10 @@ function liveOwnedChildAt(el, index) {
|
|
|
8369
8424
|
scan = nodeAfterPortalRange(scan, rangeEnd);
|
|
8370
8425
|
continue;
|
|
8371
8426
|
}
|
|
8427
|
+
if (!adoptHydrationChildren && scan.$$deoptKey === void 0 && getDeoptDesc(scan) === void 0) {
|
|
8428
|
+
scan = getNextSibling(scan);
|
|
8429
|
+
continue;
|
|
8430
|
+
}
|
|
8372
8431
|
if (i === index) return scan;
|
|
8373
8432
|
i++;
|
|
8374
8433
|
scan = getNextSibling(scan);
|
|
@@ -10417,7 +10476,7 @@ function useTransition(slot) {
|
|
|
10417
10476
|
}
|
|
10418
10477
|
};
|
|
10419
10478
|
TRANSITION_LISTENERS.add(listener);
|
|
10420
|
-
scope.cleanups.push(() => TRANSITION_LISTENERS.delete(listener));
|
|
10479
|
+
(scope.cleanups ??= []).push(() => TRANSITION_LISTENERS.delete(listener));
|
|
10421
10480
|
}
|
|
10422
10481
|
return [s.isPending, s.start];
|
|
10423
10482
|
}
|
|
@@ -10519,7 +10578,7 @@ function useFormStatus(slot) {
|
|
|
10519
10578
|
s = { form: null, listener: null };
|
|
10520
10579
|
const slotRef = s;
|
|
10521
10580
|
ensureHooks(scope).set(slot, slotRef);
|
|
10522
|
-
scope.cleanups.push(() => {
|
|
10581
|
+
(scope.cleanups ??= []).push(() => {
|
|
10523
10582
|
if (slotRef.form && slotRef.listener)
|
|
10524
10583
|
FORM_STATUS_LISTENERS.get(slotRef.form)?.delete(slotRef.listener);
|
|
10525
10584
|
});
|
|
@@ -10594,7 +10653,7 @@ function useOptimistic(passthrough, updateFnOrSlot, slot) {
|
|
|
10594
10653
|
if (TRANSITION_PENDING_COUNT === 0 && slotRef.armed) clear();
|
|
10595
10654
|
};
|
|
10596
10655
|
TRANSITION_LISTENERS.add(listener);
|
|
10597
|
-
scope.cleanups.push(() => TRANSITION_LISTENERS.delete(listener));
|
|
10656
|
+
(scope.cleanups ??= []).push(() => TRANSITION_LISTENERS.delete(listener));
|
|
10598
10657
|
}
|
|
10599
10658
|
s.updateFn = updateFn;
|
|
10600
10659
|
let optimistic = passthrough;
|
|
@@ -11214,7 +11273,7 @@ function activityBlock(parentScope, slotKey, domParent, mode, body, anchor, env)
|
|
|
11214
11273
|
}
|
|
11215
11274
|
function forEachSubtreeChild(scope, visit, includeHiddenTry = true) {
|
|
11216
11275
|
const children = scope.children;
|
|
11217
|
-
for (let i = 0, n = children.length; i < n; i++) visit(children[i].scope);
|
|
11276
|
+
if (children !== null) for (let i = 0, n = children.length; i < n; i++) visit(children[i].scope);
|
|
11218
11277
|
const slots = scope._slots;
|
|
11219
11278
|
if (slots !== null) {
|
|
11220
11279
|
for (let i = 0, n = slots.length; i < n; i++) {
|
|
@@ -11938,7 +11997,7 @@ function batchClearItems(state, oldItems) {
|
|
|
11938
11997
|
range.deleteContents();
|
|
11939
11998
|
}
|
|
11940
11999
|
for (let b = state.head; b !== null; b = b.nextSibling) {
|
|
11941
|
-
if (b.cleanups
|
|
12000
|
+
if (b.cleanups !== null || b.children !== null || b._slots !== null) {
|
|
11942
12001
|
unmountBlock(b, false);
|
|
11943
12002
|
} else {
|
|
11944
12003
|
if (b.deoptNode !== null) detachDeoptTreeRefs(b.deoptNode, null);
|
|
@@ -12286,7 +12345,7 @@ function coalesceHydratedRanges(rootBlock, liteRanges) {
|
|
|
12286
12345
|
if (group !== void 0) return group;
|
|
12287
12346
|
}
|
|
12288
12347
|
const registered = scope._slots;
|
|
12289
|
-
if (registered !== null && registered.length === 1 && scope.children
|
|
12348
|
+
if (registered !== null && registered.length === 1 && scope.children === null && registered[0].__kind === "childSlot" && registered[0].compactable && mayBorrowCandidate(registered[0])) {
|
|
12290
12349
|
return ownerGroups.get(registered[0]) ?? null;
|
|
12291
12350
|
}
|
|
12292
12351
|
return null;
|
|
@@ -12363,7 +12422,8 @@ function coalesceHydratedRanges(rootBlock, liteRanges) {
|
|
|
12363
12422
|
}
|
|
12364
12423
|
function visitScopeContents(scope) {
|
|
12365
12424
|
const children = scope.children;
|
|
12366
|
-
|
|
12425
|
+
if (children !== null)
|
|
12426
|
+
for (let i = 0; i < children.length; i++) visitNestedScope(children[i].scope);
|
|
12367
12427
|
const registered = scope._slots;
|
|
12368
12428
|
if (registered === null) return;
|
|
12369
12429
|
for (let i = 0; i < registered.length; i++) visitSlot(registered[i]);
|
package/dist/runtime.server.d.ts
CHANGED
|
@@ -137,6 +137,13 @@ export declare function ssrActivity(mode: string, render: () => string): string;
|
|
|
137
137
|
* hydration recover server/client list-shape mismatches without extra nodes.
|
|
138
138
|
*/
|
|
139
139
|
export declare function ssrForBlock(content: string, hasItems: boolean): string;
|
|
140
|
+
/**
|
|
141
|
+
* @internal Exported for direct testing: a conflating or wrong-width encoding is
|
|
142
|
+
* invisible through `prerender`, because occurrence tracking assigns list items
|
|
143
|
+
* distinct scopes independently of the key bytes. Not re-exported from
|
|
144
|
+
* `octane/server`.
|
|
145
|
+
*/
|
|
146
|
+
export declare function encodeAsyncIdentityString(value: string): string;
|
|
140
147
|
/** Compiler-emitted identity membrane for one @if/@switch/@for instance. */
|
|
141
148
|
export declare function ssrControl<T>(siteKey: string, fn: () => T): T;
|
|
142
149
|
/** Compiler-emitted identity membrane for one arm/item inside ssrControl. */
|
|
@@ -466,7 +473,12 @@ export declare function namespaceHeadElement(headKey: string, tag: string, attrs
|
|
|
466
473
|
* - `html` — the rendered markup. Hoisted document metadata (`<title>`/`<meta>`/
|
|
467
474
|
* `<link>`, collected via `ssrHeadEl`) is folded IN: spliced before `</head>`
|
|
468
475
|
* when the render produced a document, otherwise prepended. (React folds head
|
|
469
|
-
* resources into the document too,
|
|
476
|
+
* resources into the document too, which is why folding is the default.)
|
|
477
|
+
* - `head`, the hoisted metadata on its own, present ONLY under
|
|
478
|
+
* `headChannel: 'separate'`; `html` then excludes it. For hosts that render
|
|
479
|
+
* into a `<head>`-bearing template they own rather than rendering the
|
|
480
|
+
* document, where the fold has no `</head>` to target and would otherwise
|
|
481
|
+
* prepend metadata into the body. See `RenderOptions.headChannel`.
|
|
470
482
|
* - `css` — the scoped stylesheets of the components that rendered, as
|
|
471
483
|
* ready-to-place `<style data-octane="hash">…</style>` tags (one per hash,
|
|
472
484
|
* deduped). Kept as its own field because octane has scoped CSS that React core
|
|
@@ -478,6 +490,7 @@ export declare function namespaceHeadElement(headKey: string, tag: string, attrs
|
|
|
478
490
|
export interface RenderResult {
|
|
479
491
|
html: string;
|
|
480
492
|
css: string;
|
|
493
|
+
head?: string;
|
|
481
494
|
}
|
|
482
495
|
/** Options accepted by the buffered render entry points (React-shaped subset). */
|
|
483
496
|
export interface RenderOptions {
|
|
@@ -502,6 +515,22 @@ export interface RenderOptions {
|
|
|
502
515
|
* renders only (`prerender`).
|
|
503
516
|
*/
|
|
504
517
|
timeoutMs?: number;
|
|
518
|
+
/**
|
|
519
|
+
* Where hoisted `<title>`/`<meta>`/`<link>` go.
|
|
520
|
+
*
|
|
521
|
+
* `'fold'` (default) keeps React's resource-hoisting shape: the metadata is
|
|
522
|
+
* spliced into `html` before `</head>`, or prepended when the render is not a
|
|
523
|
+
* document. `'separate'` withholds it from `html`/the streamed shell and hands
|
|
524
|
+
* it over on its own, `RenderResult.head` for the buffered renderers,
|
|
525
|
+
* `StreamOptions.onHeadReady` for the streaming ones.
|
|
526
|
+
*
|
|
527
|
+
* A host that renders into a `<head>`-bearing template it owns (rather than
|
|
528
|
+
* rendering the document itself) needs `'separate'`: the fold has no
|
|
529
|
+
* `</head>` to find in a body-only render, so it would prepend the metadata
|
|
530
|
+
* into the body, where a `<title>` loses to the template's and a canonical or
|
|
531
|
+
* description is ignored outright.
|
|
532
|
+
*/
|
|
533
|
+
headChannel?: 'fold' | 'separate';
|
|
505
534
|
}
|
|
506
535
|
export declare function setSsrSuspenseTimeout(ms: number): void;
|
|
507
536
|
export declare function getSsrSuspenseTimeout(): number;
|
|
@@ -668,6 +697,19 @@ export interface StreamOptions extends RenderOptions {
|
|
|
668
697
|
onShellReady?: () => void;
|
|
669
698
|
onShellError?: (err: unknown) => void;
|
|
670
699
|
onAllReady?: () => void;
|
|
700
|
+
/**
|
|
701
|
+
* Receives the shell's hoisted `<title>`/`<meta>`/`<link>` under
|
|
702
|
+
* `headChannel: 'separate'`, called once BEFORE the shell is written and
|
|
703
|
+
* therefore before `onShellReady` and before `renderToReadableStream`'s
|
|
704
|
+
* promise resolves, so a host still has time to place the metadata in the
|
|
705
|
+
* template prefix it writes ahead of the render stream. Never called under
|
|
706
|
+
* the default `'fold'`, where the metadata rides the shell.
|
|
707
|
+
*
|
|
708
|
+
* Only the shell's metadata: head elements hoisted from inside a Suspense
|
|
709
|
+
* boundary that streams later are re-created client-side on hydration (see
|
|
710
|
+
* docs/ssr.md).
|
|
711
|
+
*/
|
|
712
|
+
onHeadReady?: (head: string) => void;
|
|
671
713
|
/** Merge externally-produced HTML into the stream (see StreamInjectionSource). */
|
|
672
714
|
injection?: StreamInjectionSource;
|
|
673
715
|
}
|
package/dist/runtime.server.js
CHANGED
|
@@ -756,10 +756,15 @@ function ssrActivity(mode, render) {
|
|
|
756
756
|
function ssrForBlock(content, hasItems) {
|
|
757
757
|
return MARKERS ? (hasItems ? FOR_BLOCK_OPEN_ITEMS : FOR_BLOCK_OPEN_EMPTY) + content + BLOCK_CLOSE : content;
|
|
758
758
|
}
|
|
759
|
+
const ASCII_ASYNC_IDENTITY_UNITS = [];
|
|
760
|
+
for (let code = 0; code < 128; code++) {
|
|
761
|
+
ASCII_ASYNC_IDENTITY_UNITS.push(code.toString(16).padStart(4, "0"));
|
|
762
|
+
}
|
|
759
763
|
function encodeAsyncIdentityString(value) {
|
|
760
764
|
let encoded = "";
|
|
761
765
|
for (let i = 0; i < value.length; i++) {
|
|
762
|
-
|
|
766
|
+
const code = value.charCodeAt(i);
|
|
767
|
+
encoded += code < 128 ? ASCII_ASYNC_IDENTITY_UNITS[code] : code.toString(16).padStart(4, "0");
|
|
763
768
|
}
|
|
764
769
|
return encoded;
|
|
765
770
|
}
|
|
@@ -3035,16 +3040,27 @@ async function runBuffered(component, props, options, nonceAttr) {
|
|
|
3035
3040
|
if (resolved.pu.touched !== void 0) resolved.pu.touched = /* @__PURE__ */ new Set();
|
|
3036
3041
|
}
|
|
3037
3042
|
}
|
|
3038
|
-
function passToResult(pass, nonceAttr) {
|
|
3043
|
+
function passToResult(pass, nonceAttr, separateHead = false) {
|
|
3039
3044
|
let body = pass.body;
|
|
3040
3045
|
if (pass.serial.length > 0) body += serializeSuspenseSeeds(pass.serial, nonceAttr);
|
|
3046
|
+
if (separateHead) {
|
|
3047
|
+
return {
|
|
3048
|
+
html: pass.vtCandidates ? vtSsrStrip(body) : body,
|
|
3049
|
+
css: pass.css,
|
|
3050
|
+
head: pass.vtCandidates ? vtSsrStrip(pass.head) : pass.head
|
|
3051
|
+
};
|
|
3052
|
+
}
|
|
3041
3053
|
const html = spliceHead(body, pass.head);
|
|
3042
3054
|
return { html: pass.vtCandidates ? vtSsrStrip(html) : html, css: pass.css };
|
|
3043
3055
|
}
|
|
3044
3056
|
async function prerender(entryComponent, props, options) {
|
|
3045
3057
|
const component = entryComponent;
|
|
3046
3058
|
const nonceAttr = nonceAttrOf(options);
|
|
3047
|
-
return passToResult(
|
|
3059
|
+
return passToResult(
|
|
3060
|
+
await runBuffered(component, props, options, nonceAttr),
|
|
3061
|
+
nonceAttr,
|
|
3062
|
+
options?.headChannel === "separate"
|
|
3063
|
+
);
|
|
3048
3064
|
}
|
|
3049
3065
|
let HOSTED_FOREIGN_CONTEXT_READER = null;
|
|
3050
3066
|
function readHostedForeignContext(usable, api) {
|
|
@@ -3122,7 +3138,7 @@ function renderToString(entryComponent, props, options) {
|
|
|
3122
3138
|
options?.onError?.(err);
|
|
3123
3139
|
throw err;
|
|
3124
3140
|
}
|
|
3125
|
-
return passToResult(pass, nonceAttr);
|
|
3141
|
+
return passToResult(pass, nonceAttr, options?.headChannel === "separate");
|
|
3126
3142
|
}
|
|
3127
3143
|
function renderToStaticMarkup(entryComponent, props, options) {
|
|
3128
3144
|
const component = entryComponent;
|
|
@@ -3146,6 +3162,13 @@ function renderToStaticMarkup(entryComponent, props, options) {
|
|
|
3146
3162
|
options?.onError?.(err);
|
|
3147
3163
|
throw err;
|
|
3148
3164
|
}
|
|
3165
|
+
if (options?.headChannel === "separate") {
|
|
3166
|
+
return {
|
|
3167
|
+
html: pass.vtCandidates ? vtSsrStrip(pass.body) : pass.body,
|
|
3168
|
+
css: pass.css,
|
|
3169
|
+
head: pass.vtCandidates ? vtSsrStrip(pass.head) : pass.head
|
|
3170
|
+
};
|
|
3171
|
+
}
|
|
3149
3172
|
const html = spliceHead(pass.body, pass.head);
|
|
3150
3173
|
return { html: pass.vtCandidates ? vtSsrStrip(html) : html, css: pass.css };
|
|
3151
3174
|
}
|
|
@@ -3685,6 +3708,9 @@ async function runStream(component, props, options, sink) {
|
|
|
3685
3708
|
emittedCss.add(hash);
|
|
3686
3709
|
leadingStyles += '<style data-octane="' + hash + '"' + nonceAttr + ">" + escapeEntireInlineStyleContent(sheet) + "</style>";
|
|
3687
3710
|
}
|
|
3711
|
+
const separateHead = options?.headChannel === "separate";
|
|
3712
|
+
if (separateHead) options?.onHeadReady?.(pass.vtCandidates ? vtSsrStrip(pass.head) : pass.head);
|
|
3713
|
+
const shellHead = separateHead ? "" : pass.head;
|
|
3688
3714
|
const documentRoot = isDocumentRoot(pass.body);
|
|
3689
3715
|
let shell = documentRoot ? "<!DOCTYPE html>" : "";
|
|
3690
3716
|
let heldDocumentTail = "";
|
|
@@ -3694,12 +3720,12 @@ async function runStream(component, props, options, sink) {
|
|
|
3694
3720
|
heldDocumentTail = pass.body.slice(tailStart);
|
|
3695
3721
|
const bodyHtml = pass.body.slice(0, tailStart);
|
|
3696
3722
|
const headInsert = documentHeadInsertionPoint(bodyHtml);
|
|
3697
|
-
shell += headInsert !== -1 ? bodyHtml.slice(0, headInsert) + leadingStyles +
|
|
3723
|
+
shell += headInsert !== -1 ? bodyHtml.slice(0, headInsert) + leadingStyles + shellHead + bodyHtml.slice(headInsert) : leadingStyles + shellHead + bodyHtml;
|
|
3698
3724
|
} else {
|
|
3699
|
-
shell += leadingStyles +
|
|
3725
|
+
shell += leadingStyles + shellHead + pass.body;
|
|
3700
3726
|
}
|
|
3701
3727
|
} else {
|
|
3702
|
-
shell += leadingStyles +
|
|
3728
|
+
shell += leadingStyles + shellHead + pass.body;
|
|
3703
3729
|
}
|
|
3704
3730
|
if (pass.serial.length > 0) shell += serializeSuspenseSeeds(pass.serial, nonceAttr);
|
|
3705
3731
|
const anyPending = stream.boundaries.size > 0;
|
|
@@ -4272,6 +4298,7 @@ export {
|
|
|
4272
4298
|
createElement,
|
|
4273
4299
|
createHostedServerSession,
|
|
4274
4300
|
createPortal,
|
|
4301
|
+
encodeAsyncIdentityString,
|
|
4275
4302
|
escapeAttr,
|
|
4276
4303
|
escapeHtml,
|
|
4277
4304
|
flushSync,
|