octane 0.1.9 → 0.1.10

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.
@@ -5051,7 +5051,7 @@ export function compile(source, filename, options) {
5051
5051
  const args = [JSON.stringify(t.html)];
5052
5052
  if (t.ns || t.frag) args.push(String(t.ns | 0));
5053
5053
  if (t.frag) args.push(String(t.frag | 0));
5054
- return `const ${t.name} = _$template(${args.join(', ')});`;
5054
+ return `const ${t.name} = /* @__PURE__ */ _$template(${args.join(', ')});`;
5055
5055
  })
5056
5056
  .join('\n');
5057
5057
  const templatesBlock = templates ? templates + '\n\n' : '';
@@ -5131,18 +5131,18 @@ export function compile(source, filename, options) {
5131
5131
  .join('\n') + '\n';
5132
5132
  }
5133
5133
 
5134
- // Cross-module singleRoot stamps (docs/comment-marker-elision-plan.md M1):
5135
- // a component whose body provably renders ONE plain element carries the
5136
- // marker on its BINDING, so a `componentSlot(…, 2)` call site in ANOTHER
5137
- // module can take the markerless singleRoot path at mount. Emitted at the
5138
- // module tail so it lands on the final binding (incl. the hmr() wrapper —
5139
- // the stamp goes on what importers see). Also feeds the runtime's existing
5134
+ // Cross-module singleRoot fallback stamps (docs/comment-marker-elision-plan.md
5135
+ // M1). Non-HMR initializers carry this metadata inside a tree-shakeable
5136
+ // binding; the tail remains for HMR wrappers and declaration shapes that cannot
5137
+ // safely move the source binding. Also feeds the runtime's existing
5140
5138
  // value-position `$$singleRoot` descriptor check.
5141
5139
  let stampBlock = '';
5142
5140
  if (ctx.componentInfo) {
5143
5141
  const stamps = [];
5144
5142
  for (const [name, info] of ctx.componentInfo) {
5145
- if (info.singleRoot === true) stamps.push(`${name}.$$singleRoot = true;`);
5143
+ if (info.singleRoot === true && info.singleRootInitialized !== true) {
5144
+ stamps.push(`${name}.$$singleRoot = true;`);
5145
+ }
5146
5146
  }
5147
5147
  for (const entry of ctx.devFunctionLocs) {
5148
5148
  stamps.push(
@@ -7068,6 +7068,15 @@ function applyCssScoping(componentNode, ctx) {
7068
7068
  return cssHash;
7069
7069
  }
7070
7070
 
7071
+ // Attach definition metadata through the component's initializer rather than a
7072
+ // free-standing module mutation. The call-site annotation is valid because every
7073
+ // caller passes a freshly-created compiler function that cannot yet be observed;
7074
+ // bundlers may therefore discard the initializer together with an unused export.
7075
+ function singleRootInitializer(ctx, component) {
7076
+ ctx.runtimeNeeded.add('markSingleRoot');
7077
+ return `/* @__PURE__ */ _$markSingleRoot(${component})`;
7078
+ }
7079
+
7071
7080
  function compileComponent(node, ctx, options) {
7072
7081
  const name = node.id.name;
7073
7082
  rejectAsyncOrGenerator(node, name);
@@ -7131,12 +7140,15 @@ function compileComponent(node, ctx, options) {
7131
7140
  // module const) so the component's own body — where the function-
7132
7141
  // expression name shadows the const — resolves `_$warmChild(Self, …)` to
7133
7142
  // an object that carries the plan. hmr() forwards `__warm` from the
7134
- // wrapped fn onto its wrapper for cross-module references.
7135
- const warmedFn = ctx._pendingWarm ? `Object.assign(${fn}, { __warm: ${ctx._pendingWarm} })` : fn;
7143
+ // wrapped fn onto its wrapper for cross-module references. Both assignments
7144
+ // below target this freshly-emitted function, so unused initializers may drop.
7145
+ const warmedFn = ctx._pendingWarm
7146
+ ? `/* @__PURE__ */ Object.assign(${fn}, { __warm: ${ctx._pendingWarm} })`
7147
+ : fn;
7136
7148
  ctx._pendingWarm = null;
7137
7149
  const abiFn =
7138
7150
  hmrWrap && returnedOutput
7139
- ? `Object.assign(${warmedFn}, { __octaneReturnedOutput: true })`
7151
+ ? `/* @__PURE__ */ Object.assign(${warmedFn}, { __octaneReturnedOutput: true })`
7140
7152
  : warmedFn;
7141
7153
 
7142
7154
  // HMR-wrap exported components inline so the binding stays a `const` (no
@@ -7146,7 +7158,12 @@ function compileComponent(node, ctx, options) {
7146
7158
  // function-name identity by NAMING the inner FunctionExpression — `hmr`
7147
7159
  // returns a wrapper that delegates to whatever fn is currently committed,
7148
7160
  // and `module.Foo[HMR].update(...)` swaps it on each accept.
7149
- const valueExpr = hmrWrap && isExported ? `_$hmr(${abiFn})` : abiFn;
7161
+ let valueExpr = hmrWrap && isExported ? `_$hmr(${abiFn})` : abiFn;
7162
+ const componentInfo = ctx.componentInfo.get(name);
7163
+ if (!hmrWrap && componentInfo?.singleRoot === true) {
7164
+ valueExpr = singleRootInitializer(ctx, valueExpr);
7165
+ componentInfo.singleRootInitialized = true;
7166
+ }
7150
7167
  const declaration = options && options.hmrMutable ? 'let' : 'const';
7151
7168
  if (isDefault) {
7152
7169
  if (options && options.hmrMutable) {
@@ -9794,13 +9811,15 @@ function lowerHostFragment(node, ctx, compInlinedSubs, parentNs = 'html', cssHas
9794
9811
  // compileFunctionBody → emitElementHtml (via ctx._foldedDirectiveCalls).
9795
9812
  body: { type: 'JSXCodeBlock', body: [], render: rendererEl, foldedDirectives: directiveCalls },
9796
9813
  };
9797
- ctx.hoistedHelpers.push(compileFunctionBody(synthFn, ctx, fragName, parentNs, cssHash));
9814
+ const renderer = compileFunctionBody(synthFn, ctx, fragName, parentNs, cssHash);
9798
9815
  if ((node.type === 'Element' || node.type === 'JSXElement') && !isComponentTag(node)) {
9799
9816
  // A host fragment is a SINGLE root element, so it can mount markerless (the
9800
9817
  // element self-delimits) — matching `@{}`'s inline render exactly (no extra
9801
9818
  // comment markers), which is required for byte-equal DOM when folding `@{}`.
9802
9819
  // A returned JSX fragment may have multiple roots and must retain its range.
9803
- ctx.hoistedHelpers.push(`${fragName}.$$singleRoot = true;`);
9820
+ ctx.hoistedHelpers.push(`const ${fragName} = ${singleRootInitializer(ctx, renderer)};`);
9821
+ } else {
9822
+ ctx.hoistedHelpers.push(renderer);
9804
9823
  }
9805
9824
  ctx.runtimeNeeded.add('createElement');
9806
9825
  return {
@@ -10123,7 +10142,7 @@ function allocHookSymbol(ctx, debugName, profile = null, forceSymbol = false) {
10123
10142
  // and custom-hook boundaries retain the trailing-Symbol ABI consumed by
10124
10143
  // published bindings. Both use the short, path-free description.
10125
10144
  if (ctx._hookHash === undefined) ctx._hookHash = hookSlotHash(ctx.filename);
10126
- symbolExpr = `Symbol(${JSON.stringify(`${ctx._hookHash}#${id}`)})`;
10145
+ symbolExpr = `/* @__PURE__ */ Symbol(${JSON.stringify(`${ctx._hookHash}#${id}`)})`;
10127
10146
  } else {
10128
10147
  // Direct sites in a compiler-created render Scope only need a tiny local
10129
10148
  // integer. Arbitrary callable helpers and custom-hook boundaries can share a
@@ -10132,7 +10151,7 @@ function allocHookSymbol(ctx, debugName, profile = null, forceSymbol = false) {
10132
10151
  if (forceSymbol) {
10133
10152
  const { baseName } = ensureHookSlotBase(ctx);
10134
10153
  const numericExpr = id === 0 ? baseName : `${baseName} + ${id}`;
10135
- symbolExpr = `Symbol(${numericExpr})`;
10154
+ symbolExpr = `/* @__PURE__ */ Symbol(${numericExpr})`;
10136
10155
  } else {
10137
10156
  symbolExpr = String(id);
10138
10157
  }
@@ -720,10 +720,10 @@ function allocHookSymbol(st, owner, local, imported, node) {
720
720
  // Symbol() collapses those paths and collides state across call sites.
721
721
  // Short filename hash + index; no module path in the output (see
722
722
  // compile.js hookSlotHash for the full rationale).
723
- symbolExpr = `Symbol(${JSON.stringify(`${st.hash}#${id}`)})`;
723
+ symbolExpr = `/* @__PURE__ */ Symbol(${JSON.stringify(`${st.hash}#${id}`)})`;
724
724
  } else {
725
725
  const numericExpr = id === 0 ? st.slotBaseName : `${st.slotBaseName} + ${id}`;
726
- symbolExpr = `Symbol(${numericExpr})`;
726
+ symbolExpr = `/* @__PURE__ */ Symbol(${numericExpr})`;
727
727
  }
728
728
  if (st.profile) {
729
729
  const componentId = `${st.profileFilename || '<anon>'}#${owner.name}@${owner.line}:${owner.column}`;
@@ -0,0 +1,30 @@
1
+ export declare const HYDRATE_SUPPORTED_INTERACTION_EVENTS: readonly ["auxclick", "click", "contextmenu", "dblclick", "focusin", "keydown", "keyup", "mousedown", "mouseenter", "mouseover", "mouseup", "pointerdown", "pointerenter", "pointerover", "pointerup"];
2
+ export interface HydrationReplayIntent {
3
+ event: Event;
4
+ path: number[];
5
+ }
6
+ export type HydrationIntentBoundaryStatus = 'hydrated' | 'never' | 'dormant' | 'handles';
7
+ export type HydrationIntentBoundary = (eventType: string, intent?: HydrationReplayIntent) => HydrationIntentBoundaryStatus;
8
+ /** @internal Resolve an event target to an element-only path beneath a marker. */
9
+ export declare function hydrationEventPathWithin(root: Element, target: EventTarget | null): number[] | null;
10
+ /**
11
+ * Install document-level capture for deferred-hydration interaction intent.
12
+ * Calling this function more than once for the same document is a no-op.
13
+ *
14
+ * Applications that can receive input before `hydrateRoot()` should call this
15
+ * from their lightweight client bootstrap. Mounting the first `<Hydrate>`
16
+ * boundary also invokes it as a synchronous fallback.
17
+ */
18
+ export declare function initializeHydrationEventCapture(ownerDocument?: Document): void;
19
+ /** @internal Runtime bridge for a mounted deferred-hydration boundary. */
20
+ export declare function registerHydrationIntentBoundary(marker: Element, boundary: HydrationIntentBoundary): void;
21
+ /** @internal Runtime bridge for a removed deferred-hydration boundary. */
22
+ export declare function unregisterHydrationIntentBoundary(marker: Element, boundary: HydrationIntentBoundary): void;
23
+ /** @internal Consume intent captured before the runtime boundary was registered. */
24
+ export declare function takePendingHydrationIntents(marker: Element): HydrationReplayIntent[] | undefined;
25
+ /** @internal Consume conservative nested-dynamic intent recorded before registration. */
26
+ export declare function takeDelegatedDynamicHydrationIntent(marker: Element): boolean;
27
+ /** @internal Avoid duplicate handling by the boundary-local capture listener. */
28
+ export declare function wasEarlyHydrationIntentHandled(event: Event): boolean;
29
+ /** @internal Preserve nested dynamic intent discovered by a mounted parent. */
30
+ export declare function markDelegatedDynamicHydrationIntent(marker: Element): void;
@@ -0,0 +1,152 @@
1
+ import {
2
+ HYDRATE_DEFAULT_INTERACTION_EVENTS,
3
+ HYDRATE_INTERACTION_EVENTS_ATTR
4
+ } from "./interaction-config.js";
5
+ const HYDRATE_MARKER_SELECTOR = "[data-octane-hydrate-id]";
6
+ const HYDRATE_WHEN_ATTR = "data-octane-hydrate-when";
7
+ function isHydrationNode(target) {
8
+ return target !== null && typeof target.nodeType === "number";
9
+ }
10
+ function isHydrationElement(target) {
11
+ return isHydrationNode(target) && target.nodeType === 1;
12
+ }
13
+ const HYDRATE_SUPPORTED_INTERACTION_EVENTS = [
14
+ "auxclick",
15
+ "click",
16
+ "contextmenu",
17
+ "dblclick",
18
+ "focusin",
19
+ "keydown",
20
+ "keyup",
21
+ "mousedown",
22
+ "mouseenter",
23
+ "mouseover",
24
+ "mouseup",
25
+ "pointerdown",
26
+ "pointerenter",
27
+ "pointerover",
28
+ "pointerup"
29
+ ];
30
+ const HYDRATE_BOUNDARIES = /* @__PURE__ */ new WeakMap();
31
+ const HYDRATE_PENDING_INTENTS = /* @__PURE__ */ new WeakMap();
32
+ const HYDRATE_DELEGATED_DYNAMIC_MARKERS = /* @__PURE__ */ new WeakSet();
33
+ const HYDRATE_HANDLED_INTENT_EVENTS = /* @__PURE__ */ new WeakSet();
34
+ const HYDRATE_INTENT_DOCUMENTS = /* @__PURE__ */ new WeakSet();
35
+ function hydrationEventPathWithin(root, target) {
36
+ if (!isHydrationNode(target)) return null;
37
+ const path = [];
38
+ let node = isHydrationElement(target) ? target : target.parentElement;
39
+ while (node !== root) {
40
+ const parent = node?.parentElement ?? null;
41
+ if (parent === null) return null;
42
+ const index = Array.prototype.indexOf.call(parent.children, node);
43
+ if (index < 0) return null;
44
+ path.push(index);
45
+ node = parent;
46
+ }
47
+ path.reverse();
48
+ return path;
49
+ }
50
+ function markerStatus(marker, eventType) {
51
+ const boundary = HYDRATE_BOUNDARIES.get(marker);
52
+ if (boundary !== void 0) return boundary(eventType);
53
+ const when = marker.getAttribute(HYDRATE_WHEN_ATTR);
54
+ if (when === null) return "hydrated";
55
+ if (when === "never") return "never";
56
+ if (when === "dynamic") return eventType === "click" ? "handles" : "dormant";
57
+ if (when !== "interaction") return "dormant";
58
+ const custom = marker.getAttribute(HYDRATE_INTERACTION_EVENTS_ATTR);
59
+ const events = custom === null ? HYDRATE_DEFAULT_INTERACTION_EVENTS : custom.split(/\s+/).filter(Boolean);
60
+ return events.includes(eventType) ? "handles" : "dormant";
61
+ }
62
+ function handleEarlyHydrationIntent(event) {
63
+ const target = event.target;
64
+ if (!isHydrationElement(target)) return;
65
+ const markers = [];
66
+ let marker = target.closest(HYDRATE_MARKER_SELECTOR);
67
+ let matches = false;
68
+ while (marker !== null) {
69
+ markers.push(marker);
70
+ matches ||= markerStatus(marker, event.type) === "handles";
71
+ marker = marker.parentElement?.closest(HYDRATE_MARKER_SELECTOR) ?? null;
72
+ }
73
+ if (!matches || markers.length === 0) return;
74
+ markers.reverse();
75
+ let candidate = null;
76
+ let candidateBoundary;
77
+ for (let i = 0; i < markers.length; i++) {
78
+ const current = markers[i];
79
+ const status = markerStatus(current, event.type);
80
+ if (status === "hydrated") continue;
81
+ if (status === "never") return;
82
+ candidate = current;
83
+ candidateBoundary = HYDRATE_BOUNDARIES.get(current);
84
+ break;
85
+ }
86
+ if (candidate === null) return;
87
+ for (let i = 0; i < markers.length; i++) {
88
+ const current = markers[i];
89
+ if (current !== candidate && candidate.contains(current) && current.getAttribute(HYDRATE_WHEN_ATTR) === "dynamic" && !HYDRATE_BOUNDARIES.has(current)) {
90
+ HYDRATE_DELEGATED_DYNAMIC_MARKERS.add(current);
91
+ }
92
+ }
93
+ const path = hydrationEventPathWithin(candidate, event.target);
94
+ if (path === null) return;
95
+ const intent = { event, path };
96
+ HYDRATE_HANDLED_INTENT_EVENTS.add(event);
97
+ if (event.bubbles) {
98
+ event.preventDefault();
99
+ event.stopPropagation();
100
+ event.stopImmediatePropagation();
101
+ }
102
+ if (candidateBoundary !== void 0) {
103
+ candidateBoundary(event.type, intent);
104
+ } else {
105
+ const pending = HYDRATE_PENDING_INTENTS.get(candidate) ?? [];
106
+ pending.push(intent);
107
+ HYDRATE_PENDING_INTENTS.set(candidate, pending);
108
+ }
109
+ }
110
+ function initializeHydrationEventCapture(ownerDocument) {
111
+ const targetDocument = ownerDocument ?? (typeof document === "undefined" ? void 0 : document);
112
+ if (targetDocument === void 0 || HYDRATE_INTENT_DOCUMENTS.has(targetDocument)) return;
113
+ HYDRATE_INTENT_DOCUMENTS.add(targetDocument);
114
+ for (let i = 0; i < HYDRATE_SUPPORTED_INTERACTION_EVENTS.length; i++) {
115
+ targetDocument.addEventListener(
116
+ HYDRATE_SUPPORTED_INTERACTION_EVENTS[i],
117
+ handleEarlyHydrationIntent,
118
+ true
119
+ );
120
+ }
121
+ }
122
+ function registerHydrationIntentBoundary(marker, boundary) {
123
+ HYDRATE_BOUNDARIES.set(marker, boundary);
124
+ }
125
+ function unregisterHydrationIntentBoundary(marker, boundary) {
126
+ if (HYDRATE_BOUNDARIES.get(marker) === boundary) HYDRATE_BOUNDARIES.delete(marker);
127
+ }
128
+ function takePendingHydrationIntents(marker) {
129
+ const intents = HYDRATE_PENDING_INTENTS.get(marker);
130
+ HYDRATE_PENDING_INTENTS.delete(marker);
131
+ return intents;
132
+ }
133
+ function takeDelegatedDynamicHydrationIntent(marker) {
134
+ return HYDRATE_DELEGATED_DYNAMIC_MARKERS.delete(marker);
135
+ }
136
+ function wasEarlyHydrationIntentHandled(event) {
137
+ return HYDRATE_HANDLED_INTENT_EVENTS.has(event);
138
+ }
139
+ function markDelegatedDynamicHydrationIntent(marker) {
140
+ HYDRATE_DELEGATED_DYNAMIC_MARKERS.add(marker);
141
+ }
142
+ export {
143
+ HYDRATE_SUPPORTED_INTERACTION_EVENTS,
144
+ hydrationEventPathWithin,
145
+ initializeHydrationEventCapture,
146
+ markDelegatedDynamicHydrationIntent,
147
+ registerHydrationIntentBoundary,
148
+ takeDelegatedDynamicHydrationIntent,
149
+ takePendingHydrationIntents,
150
+ unregisterHydrationIntentBoundary,
151
+ wasEarlyHydrationIntentHandled
152
+ };
@@ -1,5 +1,6 @@
1
1
  export { condition } from './condition.js';
2
2
  export type { HydrationCondition } from './condition.js';
3
+ export { initializeHydrationEventCapture } from './event-capture.js';
3
4
  export { idle } from './idle.js';
4
5
  export type { IdleHydrationOptions } from './idle.js';
5
6
  export { interaction } from './interaction.js';
@@ -1,4 +1,5 @@
1
1
  import { condition } from "./condition.js";
2
+ import { initializeHydrationEventCapture } from "./event-capture.js";
2
3
  import { idle } from "./idle.js";
3
4
  import { interaction } from "./interaction.js";
4
5
  import { load } from "./load.js";
@@ -8,6 +9,7 @@ import { visible } from "./visible.js";
8
9
  export {
9
10
  condition,
10
11
  idle,
12
+ initializeHydrationEventCapture,
11
13
  interaction,
12
14
  load,
13
15
  media,
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export { version } from './version.js';
2
- export { createRoot, hydrateRoot, flushSync, act, type Root, type RootOptions, useState, useReducer, useEffect, useLayoutEffect, useInsertionEffect, useMemo, useCallback, useRef, useId, useImperativeHandle, useEffectEvent, useSyncExternalStore, useDeferredValue, useTransition, useActionState, useFormStatus, useOptimistic, useDebugValue, type FormStatus, startTransition, requestFormReset, memo, lazy, preload, preinit, preconnect, prefetchDNS, createContext, use, useContext, type Context, type ForeignHostContext, Suspense, ErrorBoundary, Hydrate, Activity, ViewTransition, addTransitionType, ViewTransition as unstable_ViewTransition, addTransitionType as unstable_addTransitionType, ViewTransitionPseudoElement, type ViewTransitionProps, type ViewTransitionInstance, Fragment, createPortal, type PortalDescriptor, createElement, cloneElement, isValidElement, isChildrenBlock, Children, type ElementDescriptor, type ComponentBody, __useStateWithGetter, __useReducerWithGetter, __createVoidRoot, bindRendererRegionOwner, EXTERNAL_HYDRATION_PROMISE, HYDRATION_RANGE_BOUNDARY, createHostContextRequest, __vtSeen, template, clone, drainFrag, bag0, bag1, bag2, bag3, bag4, bag5, bag6, bag7, bag8, bag9, bag10, bag11, bag12, bag13, bag14, bag15, bag16, bagOf, evt0, evt0u, evt1, evt1u, evt2, evt2u, evtN, evtNu, htext, htextSwap, child, sibling, setText, setScriptText, setHTML, setDangerouslySetInnerHTML, setDangerouslySetInnerHTMLSources, markDangerouslySetInnerHTMLChildren, setAttribute, setStringData, setBooleanAttribute, setAriaAttribute, setClassName, setClassAttr, normalizeClass, setStyle, setSpread, snapshotSpread, setHostPropSources, queueNativeChangeDiagnostic, markNativeChangeDiagnosticStatic, setFormAction, setValue, setFormControlSources, setChecked, setCheckedCheckable, setSelectValue, setDefaultValue, setDefaultValueUncontrolled, setDefaultChecked, setAutoFocus, attachRef, queueRefAttach, queueRefDetach, injectStyle, headBlock, namespaceHead, namespaceHeadElement, delegateEvents, delegateCaptureEvents, forBlock, ifBlock, tryBlock, switchBlock, activityBlock, componentSlot, componentSlotVoid, componentSlotLite, compilerCacheContext, markChildrenBlock, childSlot, positionalChildren, textSlot, textHole, childTextHole, hostComponent, renderBlock, portal, hookSlots, withSlot, useBatch, warmMemo, warmChild, provideContext, mountFragmentRef, FragmentInstance, hmr, HMR, hasPendingWork, type Scope, type Block, drainPassiveEffects, setIsOctaneActEnvironment, setTransitionFallbackTimeout, getTransitionFallbackTimeout, } from './runtime.js';
2
+ export { initializeHydrationEventCapture } from './hydration/event-capture.js';
3
+ export { createRoot, hydrateRoot, flushSync, act, type Root, type RootOptions, useState, useReducer, useEffect, useLayoutEffect, useInsertionEffect, useMemo, useCallback, useRef, useId, useImperativeHandle, useEffectEvent, useSyncExternalStore, useDeferredValue, useTransition, useActionState, useFormStatus, useOptimistic, useDebugValue, type FormStatus, startTransition, requestFormReset, memo, lazy, preload, preinit, preconnect, prefetchDNS, createContext, use, useContext, type Context, type ForeignHostContext, Suspense, ErrorBoundary, Hydrate, Activity, ViewTransition, addTransitionType, ViewTransition as unstable_ViewTransition, addTransitionType as unstable_addTransitionType, ViewTransitionPseudoElement, type ViewTransitionProps, type ViewTransitionInstance, Fragment, createPortal, type PortalDescriptor, createElement, cloneElement, isValidElement, isChildrenBlock, Children, type ElementDescriptor, type ComponentBody, __useStateWithGetter, __useReducerWithGetter, __createVoidRoot, bindRendererRegionOwner, EXTERNAL_HYDRATION_PROMISE, HYDRATION_RANGE_BOUNDARY, createHostContextRequest, __vtSeen, template, clone, drainFrag, bag0, bag1, bag2, bag3, bag4, bag5, bag6, bag7, bag8, bag9, bag10, bag11, bag12, bag13, bag14, bag15, bag16, bagOf, evt0, evt0u, evt1, evt1u, evt2, evt2u, evtN, evtNu, htext, htextSwap, child, sibling, setText, setScriptText, setHTML, setDangerouslySetInnerHTML, setDangerouslySetInnerHTMLSources, markDangerouslySetInnerHTMLChildren, setAttribute, setStringData, setBooleanAttribute, setAriaAttribute, setClassName, setClassAttr, normalizeClass, setStyle, setSpread, snapshotSpread, setHostPropSources, queueNativeChangeDiagnostic, markNativeChangeDiagnosticStatic, setFormAction, setValue, setFormControlSources, setChecked, setCheckedCheckable, setSelectValue, setDefaultValue, setDefaultValueUncontrolled, setDefaultChecked, setAutoFocus, attachRef, queueRefAttach, queueRefDetach, injectStyle, headBlock, namespaceHead, namespaceHeadElement, delegateEvents, delegateCaptureEvents, forBlock, ifBlock, tryBlock, switchBlock, activityBlock, componentSlot, componentSlotVoid, componentSlotLite, compilerCacheContext, markSingleRoot, markChildrenBlock, childSlot, positionalChildren, textSlot, textHole, childTextHole, hostComponent, renderBlock, portal, hookSlots, withSlot, useBatch, warmMemo, warmChild, provideContext, mountFragmentRef, FragmentInstance, hmr, HMR, hasPendingWork, type Scope, type Block, drainPassiveEffects, setIsOctaneActEnvironment, setTransitionFallbackTimeout, getTransitionFallbackTimeout, } from './runtime.js';
3
4
  export type { HydrateOptions, HydrateProps, HydrateWhen, HydrationInteractionEvent, HydrationInteractionEvents, HydrationPrefetchContext, HydrationPrefetchFunction, HydrationPrefetchStrategy, HydrationPrefetchWaitReason, HydrationStrategy, HydrationWhen, } from './hydration/types.js';
4
5
  export { __serverRpc } from './server-rpc-client.js';
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { version } from "./version.js";
2
+ import { initializeHydrationEventCapture } from "./hydration/event-capture.js";
2
3
  import {
3
4
  createRoot,
4
5
  hydrateRoot,
@@ -137,6 +138,7 @@ import {
137
138
  componentSlotVoid,
138
139
  componentSlotLite,
139
140
  compilerCacheContext,
141
+ markSingleRoot,
140
142
  markChildrenBlock,
141
143
  childSlot,
142
144
  positionalChildren,
@@ -242,6 +244,7 @@ export {
242
244
  htextSwap,
243
245
  hydrateRoot,
244
246
  ifBlock,
247
+ initializeHydrationEventCapture,
245
248
  injectStyle,
246
249
  isChildrenBlock,
247
250
  isValidElement,
@@ -249,6 +252,7 @@ export {
249
252
  markChildrenBlock,
250
253
  markDangerouslySetInnerHTMLChildren,
251
254
  markNativeChangeDiagnosticStatic,
255
+ markSingleRoot,
252
256
  memo,
253
257
  mountFragmentRef,
254
258
  namespaceHead,
package/dist/runtime.d.ts CHANGED
@@ -540,6 +540,13 @@ export declare function createContext<T>(defaultValue: T): Context<T>;
540
540
  * propagation.)
541
541
  */
542
542
  export declare function provideContext<T>(scope: Scope, context: Context<T>, value: T): void;
543
+ /**
544
+ * Compiler-emitted: attach markerless single-host-root metadata while a fresh
545
+ * component function is still being initialized. The compiler only calls this
546
+ * with an otherwise-unobserved function, so unused initializers are removable.
547
+ * @internal
548
+ */
549
+ export declare function markSingleRoot<T extends Function>(component: T): T;
543
550
  /**
544
551
  * Compiler-emitted: tag a children-block render function so `isChildrenBlock` recognises it.
545
552
  * Returns the function for inline use (`{ children: markChildrenBlock(__children$N) }`).
package/dist/runtime.js CHANGED
@@ -39,6 +39,17 @@ import {
39
39
  HYDRATE_DEFAULT_INTERACTION_EVENTS,
40
40
  HYDRATE_INTERACTION_EVENTS_ATTR
41
41
  } from "./hydration/interaction-config.js";
42
+ import {
43
+ HYDRATE_SUPPORTED_INTERACTION_EVENTS,
44
+ hydrationEventPathWithin,
45
+ initializeHydrationEventCapture,
46
+ markDelegatedDynamicHydrationIntent,
47
+ registerHydrationIntentBoundary,
48
+ takeDelegatedDynamicHydrationIntent,
49
+ takePendingHydrationIntents,
50
+ unregisterHydrationIntentBoundary,
51
+ wasEarlyHydrationIntentHandled
52
+ } from "./hydration/event-capture.js";
42
53
  import { sanitizeURL, sanitizeURLAttribute } from "./sanitize-url.js";
43
54
  import {
44
55
  COMPONENT_FLAG_BOUNDARY,
@@ -1350,15 +1361,23 @@ function drainPostPaint() {
1350
1361
  _postPaintCbs = [];
1351
1362
  for (let i = 0; i < cbs.length; i++) cbs[i]();
1352
1363
  }
1353
- let _channel = null;
1354
- if (typeof MessageChannel !== "undefined") {
1355
- _channel = new MessageChannel();
1356
- _channel.port1.onmessage = drainPostPaint;
1364
+ let _channel;
1365
+ function initPostPaintChannel() {
1366
+ if (_channel === void 0) {
1367
+ if (typeof MessageChannel === "undefined") {
1368
+ _channel = null;
1369
+ } else {
1370
+ _channel = new MessageChannel();
1371
+ _channel.port1.onmessage = drainPostPaint;
1372
+ }
1373
+ }
1374
+ return _channel;
1357
1375
  }
1358
1376
  function schedulePostPaint(cb) {
1359
1377
  _postPaintCbs.push(cb);
1360
- if (_channel) {
1361
- requestAnimationFrame(() => _channel.port2.postMessage(0));
1378
+ const channel = initPostPaintChannel();
1379
+ if (channel !== null) {
1380
+ requestAnimationFrame(() => channel.port2.postMessage(0));
1362
1381
  } else {
1363
1382
  requestAnimationFrame(() => setTimeout(drainPostPaint, 0));
1364
1383
  }
@@ -2347,6 +2366,7 @@ function useEffectEvent(fn, slot) {
2347
2366
  }
2348
2367
  const CONTEXT_TAG = /* @__PURE__ */ Symbol.for("octane.context");
2349
2368
  let COMPILER_CACHE_CONTEXT_EPOCH = 0;
2369
+ // @__NO_SIDE_EFFECTS__
2350
2370
  function createContext(defaultValue) {
2351
2371
  const ctx = function ProviderBody(props, scope) {
2352
2372
  if (scope.$$ctxValues === null) scope.$$ctxValues = /* @__PURE__ */ new Map();
@@ -2374,6 +2394,10 @@ function provideContext(scope, context, value) {
2374
2394
  scope.$$ctxValues.set(context, value);
2375
2395
  }
2376
2396
  const CHILDREN_BLOCK = /* @__PURE__ */ Symbol.for("octane.childrenBlock");
2397
+ function markSingleRoot(component) {
2398
+ component.$$singleRoot = true;
2399
+ return component;
2400
+ }
2377
2401
  function markChildrenBlock(fn) {
2378
2402
  if (typeof fn === "function") {
2379
2403
  fn[CHILDREN_BLOCK] = true;
@@ -2392,23 +2416,6 @@ function childrenAsBody(children) {
2392
2416
  const HYDRATE_ID_SLOT = /* @__PURE__ */ Symbol("octane.hydrate.id");
2393
2417
  const HYDRATE_SETUP_SLOT = /* @__PURE__ */ Symbol("octane.hydrate.setup");
2394
2418
  const HYDRATE_NOTIFY_SLOT = /* @__PURE__ */ Symbol("octane.hydrate.notify");
2395
- const HYDRATE_SUPPORTED_INTERACTION_EVENTS = [
2396
- "auxclick",
2397
- "click",
2398
- "contextmenu",
2399
- "dblclick",
2400
- "focusin",
2401
- "keydown",
2402
- "keyup",
2403
- "mousedown",
2404
- "mouseenter",
2405
- "mouseover",
2406
- "mouseup",
2407
- "pointerdown",
2408
- "pointerenter",
2409
- "pointerover",
2410
- "pointerup"
2411
- ];
2412
2419
  const HYDRATE_STRATEGY_TYPES = /* @__PURE__ */ new Set([
2413
2420
  "load",
2414
2421
  "idle",
@@ -2420,11 +2427,6 @@ const HYDRATE_STRATEGY_TYPES = /* @__PURE__ */ new Set([
2420
2427
  "dynamic"
2421
2428
  ]);
2422
2429
  const HYDRATE_MARKER_SELECTOR = "[data-octane-hydrate-id]";
2423
- const HYDRATE_DELEGATED_DYNAMIC_MARKERS = /* @__PURE__ */ new WeakSet();
2424
- const HYDRATE_SLOTS_BY_MARKER = /* @__PURE__ */ new WeakMap();
2425
- const HYDRATE_PENDING_INTENTS = /* @__PURE__ */ new WeakMap();
2426
- const HYDRATE_HANDLED_INTENT_EVENTS = /* @__PURE__ */ new WeakSet();
2427
- const HYDRATE_INTENT_DOCUMENTS = /* @__PURE__ */ new WeakSet();
2428
2430
  function hydrateStrategyType(when) {
2429
2431
  return typeof when === "function" ? "dynamic" : when?._t ?? "dynamic";
2430
2432
  }
@@ -2608,22 +2610,7 @@ function teardownHydrateBoundary(state) {
2608
2610
  cleanupHydrateInstallers(state);
2609
2611
  state.prefetchAbort?.abort();
2610
2612
  resolveHydrateWaiters(state, "abort");
2611
- HYDRATE_SLOTS_BY_MARKER.delete(state.wrapper);
2612
- }
2613
- function eventPathWithin(root, target) {
2614
- if (!(target instanceof Node)) return null;
2615
- const path = [];
2616
- let node = target instanceof Element ? target : target.parentElement;
2617
- while (node !== root) {
2618
- const parent = node?.parentElement ?? null;
2619
- if (parent === null) return null;
2620
- const index = Array.prototype.indexOf.call(parent.children, node);
2621
- if (index < 0) return null;
2622
- path.push(index);
2623
- node = parent;
2624
- }
2625
- path.reverse();
2626
- return path;
2613
+ unregisterHydrationIntentBoundary(state.wrapper, state.intentBoundary);
2627
2614
  }
2628
2615
  function resolveEventPath(root, path) {
2629
2616
  let node = root;
@@ -2646,85 +2633,18 @@ function hydrateStrategyInteractionEvents(strategy) {
2646
2633
  const custom = strategy._a?.()?.[HYDRATE_INTERACTION_EVENTS_ATTR];
2647
2634
  return custom === void 0 ? HYDRATE_DEFAULT_INTERACTION_EVENTS : custom.split(/\s+/).filter(Boolean);
2648
2635
  }
2649
- function markerHandlesEarlyHydrationIntent(marker, eventType) {
2650
- const state = HYDRATE_SLOTS_BY_MARKER.get(marker);
2651
- if (state !== void 0) {
2652
- return hydrateStrategyInteractionEvents(resolveHydrateStrategy(state))?.includes(eventType) ?? false;
2653
- }
2654
- const when = marker.getAttribute(HYDRATE_WHEN_ATTR);
2655
- if (when === "dynamic") return eventType === "click";
2656
- if (when !== "interaction") return false;
2657
- const custom = marker.getAttribute(HYDRATE_INTERACTION_EVENTS_ATTR);
2658
- const events = custom === null ? HYDRATE_DEFAULT_INTERACTION_EVENTS : custom.split(/\s+/).filter(Boolean);
2659
- return events.includes(eventType);
2660
- }
2661
2636
  function queueHydrateIntent(state, intent) {
2662
2637
  if (state.hydrated || resolveHydrateStrategy(state)._t === "never") return;
2663
2638
  state.replays.push(intent);
2664
2639
  requestHydrateBoundary(state);
2665
2640
  }
2666
- function handleEarlyHydrationIntent(event) {
2667
- const target = event.target;
2668
- if (!(target instanceof Element)) return;
2669
- const markers = [];
2670
- let marker = target.closest(HYDRATE_MARKER_SELECTOR);
2671
- let matches = false;
2672
- while (marker !== null) {
2673
- markers.push(marker);
2674
- matches ||= markerHandlesEarlyHydrationIntent(marker, event.type);
2675
- marker = marker.parentElement?.closest(HYDRATE_MARKER_SELECTOR) ?? null;
2676
- }
2677
- if (!matches || markers.length === 0) return;
2678
- markers.reverse();
2679
- let candidate = null;
2680
- let candidateState;
2681
- for (let i = 0; i < markers.length; i++) {
2682
- const current = markers[i];
2683
- const when = current.getAttribute(HYDRATE_WHEN_ATTR);
2684
- if (when === null) continue;
2685
- const state = HYDRATE_SLOTS_BY_MARKER.get(current);
2686
- if (state?.hydrated) continue;
2687
- if (when === "never" || state !== void 0 && resolveHydrateStrategy(state)._t === "never") {
2688
- return;
2689
- }
2690
- candidate = current;
2691
- candidateState = state;
2692
- break;
2693
- }
2694
- if (candidate === null) return;
2695
- for (let i = 0; i < markers.length; i++) {
2696
- const current = markers[i];
2697
- if (current !== candidate && candidate.contains(current) && current.getAttribute(HYDRATE_WHEN_ATTR) === "dynamic" && !HYDRATE_SLOTS_BY_MARKER.has(current)) {
2698
- HYDRATE_DELEGATED_DYNAMIC_MARKERS.add(current);
2699
- }
2700
- }
2701
- const path = eventPathWithin(candidate, event.target);
2702
- if (path === null) return;
2703
- const intent = { event, path };
2704
- HYDRATE_HANDLED_INTENT_EVENTS.add(event);
2705
- if (event.bubbles) {
2706
- event.preventDefault();
2707
- event.stopPropagation();
2708
- event.stopImmediatePropagation();
2709
- }
2710
- if (candidateState !== void 0) {
2711
- queueHydrateIntent(candidateState, intent);
2712
- } else {
2713
- const pending = HYDRATE_PENDING_INTENTS.get(candidate) ?? [];
2714
- pending.push(intent);
2715
- HYDRATE_PENDING_INTENTS.set(candidate, pending);
2716
- }
2717
- }
2718
- function ensureEarlyHydrationIntentCapture(ownerDocument) {
2719
- if (HYDRATE_INTENT_DOCUMENTS.has(ownerDocument)) return;
2720
- HYDRATE_INTENT_DOCUMENTS.add(ownerDocument);
2721
- for (let i = 0; i < HYDRATE_SUPPORTED_INTERACTION_EVENTS.length; i++) {
2722
- ownerDocument.addEventListener(
2723
- HYDRATE_SUPPORTED_INTERACTION_EVENTS[i],
2724
- handleEarlyHydrationIntent,
2725
- true
2726
- );
2727
- }
2641
+ function handleRegisteredHydrationIntent(state, eventType, intent) {
2642
+ if (state.hydrated) return "hydrated";
2643
+ const strategy = resolveHydrateStrategy(state);
2644
+ if (strategy._t === "never") return "never";
2645
+ const status = hydrateStrategyInteractionEvents(strategy)?.includes(eventType) ? "handles" : "dormant";
2646
+ if (intent !== void 0) queueHydrateIntent(state, intent);
2647
+ return status;
2728
2648
  }
2729
2649
  function installHydrateInteraction(state, strategy) {
2730
2650
  const ownEvents = hydrateStrategyInteractionEvents(strategy) ?? // A parent-first replay may carry conservative intent for a dynamic marker
@@ -2740,7 +2660,7 @@ function installHydrateInteraction(state, strategy) {
2740
2660
  }
2741
2661
  if (events.size === 0) return () => void 0;
2742
2662
  const onIntent = (event) => {
2743
- if (HYDRATE_HANDLED_INTENT_EVENTS.has(event)) return;
2663
+ if (wasEarlyHydrationIntentHandled(event)) return;
2744
2664
  if (state.hydrated) return;
2745
2665
  const rawTarget = event.target;
2746
2666
  let target = rawTarget instanceof Element ? rawTarget : rawTarget instanceof Node ? rawTarget.parentElement : null;
@@ -2762,9 +2682,9 @@ function installHydrateInteraction(state, strategy) {
2762
2682
  }
2763
2683
  if (!matches) return;
2764
2684
  for (let i = 0; i < delegatedDynamicMarkers.length; i++) {
2765
- HYDRATE_DELEGATED_DYNAMIC_MARKERS.add(delegatedDynamicMarkers[i]);
2685
+ markDelegatedDynamicHydrationIntent(delegatedDynamicMarkers[i]);
2766
2686
  }
2767
- const path = eventPathWithin(state.wrapper, event.target);
2687
+ const path = hydrationEventPathWithin(state.wrapper, event.target);
2768
2688
  if (path === null) return;
2769
2689
  state.replays.push({ event, path });
2770
2690
  if (event.bubbles) {
@@ -2859,7 +2779,7 @@ function createHydrateSlot(props, scope, boundaryId) {
2859
2779
  const hydration = activeHydration();
2860
2780
  const expected = document.createElement("div");
2861
2781
  const wrapper = hydration === null ? expected : hydration.clone(expected);
2862
- ensureEarlyHydrationIntentCapture(wrapper.ownerDocument);
2782
+ initializeHydrationEventCapture(wrapper.ownerDocument);
2863
2783
  let serverPreserved = hydration !== null && !hydration.isFresh(wrapper) && wrapper.parentNode === parentNode;
2864
2784
  if (wrapper.parentNode !== parentNode) parentNode.insertBefore(wrapper, parentBlock.endMarker);
2865
2785
  if (!wrapper.hasAttribute(HYDRATE_ID_ATTR)) wrapper.setAttribute(HYDRATE_ID_ATTR, boundaryId);
@@ -2909,7 +2829,9 @@ function createHydrateSlot(props, scope, boundaryId) {
2909
2829
  void 0
2910
2830
  );
2911
2831
  block.idState = idState;
2912
- const state = {
2832
+ let state;
2833
+ const intentBoundary = (eventType, intent) => handleRegisteredHydrationIntent(state, eventType, intent);
2834
+ state = {
2913
2835
  __kind: "hydrateBlockSlot",
2914
2836
  __flags: SLOT_FLAG_TEARDOWN,
2915
2837
  __teardown: teardownHydrateBoundary,
@@ -2920,7 +2842,8 @@ function createHydrateSlot(props, scope, boundaryId) {
2920
2842
  parentBlock,
2921
2843
  props,
2922
2844
  boundaryId: wrapper.getAttribute(HYDRATE_ID_ATTR) ?? boundaryId,
2923
- delegatedDynamicIntent: HYDRATE_DELEGATED_DYNAMIC_MARKERS.delete(wrapper),
2845
+ intentBoundary,
2846
+ delegatedDynamicIntent: takeDelegatedDynamicHydrationIntent(wrapper),
2924
2847
  serverPreserved,
2925
2848
  preservedFallbackNodes: null,
2926
2849
  seedRaw,
@@ -2948,11 +2871,10 @@ function createHydrateSlot(props, scope, boundaryId) {
2948
2871
  };
2949
2872
  scope.slots[0] = state;
2950
2873
  registerSlot(scope, state);
2951
- HYDRATE_SLOTS_BY_MARKER.set(wrapper, state);
2874
+ registerHydrationIntentBoundary(wrapper, intentBoundary);
2952
2875
  block.body = hydrateBoundaryBody(state);
2953
2876
  const initialStrategy = resolveHydrateStrategy(state);
2954
- const pendingIntents = HYDRATE_PENDING_INTENTS.get(wrapper);
2955
- HYDRATE_PENDING_INTENTS.delete(wrapper);
2877
+ const pendingIntents = takePendingHydrationIntents(wrapper);
2956
2878
  if (serverPreserved && pendingIntents !== void 0 && initialStrategy._t !== "never") {
2957
2879
  state.replays.push(...pendingIntents);
2958
2880
  requestHydrateBoundary(state);
@@ -3018,7 +2940,6 @@ function notifyHydrateBoundary(state) {
3018
2940
  }
3019
2941
  }
3020
2942
  function initializeHydrateComponent() {
3021
- if (typeof document !== "undefined") ensureEarlyHydrationIntentCapture(document);
3022
2943
  return markComponentFlags(
3023
2944
  function Hydrate2(rawProps, scope) {
3024
2945
  const props = rawProps;
@@ -3208,6 +3129,7 @@ function bindRendererRegionOwner(props) {
3208
3129
  }
3209
3130
  }
3210
3131
  const HOST_CONTEXT_REQUEST_TAG = /* @__PURE__ */ Symbol.for("octane.host-context-request");
3132
+ // @__NO_SIDE_EFFECTS__
3211
3133
  function createHostContextRequest(thenable) {
3212
3134
  return { $$kind: HOST_CONTEXT_REQUEST_TAG, thenable };
3213
3135
  }
@@ -3671,6 +3593,7 @@ function resolveLazyModule(mod) {
3671
3593
  }
3672
3594
  return comp;
3673
3595
  }
3596
+ // @__NO_SIDE_EFFECTS__
3674
3597
  function lazy(load) {
3675
3598
  let status = "uninitialized";
3676
3599
  let result = null;
@@ -3755,7 +3678,7 @@ function useId(slot) {
3755
3678
  }
3756
3679
  return s.id;
3757
3680
  }
3758
- const OPAQUE_TEMPLATE = /* @__PURE__ */ Symbol("octane.opaque-template");
3681
+ const LAZY_TEMPLATE = /* @__PURE__ */ Symbol("octane.lazy-template");
3759
3682
  function parseTemplate(html, ns, frag) {
3760
3683
  const t = document.createElement("template");
3761
3684
  if (ns === 0) {
@@ -3775,13 +3698,16 @@ function parseTemplate(html, ns, frag) {
3775
3698
  }
3776
3699
  return wrapEl.firstChild;
3777
3700
  }
3701
+ // @__NO_SIDE_EFFECTS__
3778
3702
  function template(html, ns = 0, frag = 0) {
3779
- if (ns === 3) {
3780
- return {
3781
- [OPAQUE_TEMPLATE]: { html, frag, parsed: [] }
3782
- };
3783
- }
3784
- return parseTemplate(html, ns === 1 ? 1 : ns === 2 ? 2 : 0, frag);
3703
+ return {
3704
+ [LAZY_TEMPLATE]: {
3705
+ html,
3706
+ ns: ns === 1 ? 1 : ns === 2 ? 2 : ns === 3 ? 3 : 0,
3707
+ frag,
3708
+ parsed: []
3709
+ }
3710
+ };
3785
3711
  }
3786
3712
  let currentHydration = null;
3787
3713
  function activeHydration() {
@@ -4211,14 +4137,19 @@ function parseSeedJson(raw) {
4211
4137
  }
4212
4138
  }
4213
4139
  function clone(node, loc) {
4214
- const opaque = node.nodeType === void 0 ? node[OPAQUE_TEMPLATE] : void 0;
4215
- if (opaque !== void 0) {
4216
- const inherited = CURRENT_SCOPE === null ? void 0 : deoptChildNamespace(CURRENT_SCOPE.block.parentNode);
4217
- const ns = inherited === SVG_NS ? 1 : inherited === MATHML_NS ? 2 : 0;
4218
- let parsed = opaque.parsed[ns];
4140
+ const lazy2 = node.nodeType === void 0 ? node[LAZY_TEMPLATE] : void 0;
4141
+ if (lazy2 !== void 0) {
4142
+ let ns;
4143
+ if (lazy2.ns === 3) {
4144
+ const inherited = CURRENT_SCOPE === null ? void 0 : deoptChildNamespace(CURRENT_SCOPE.block.parentNode);
4145
+ ns = inherited === SVG_NS ? 1 : inherited === MATHML_NS ? 2 : 0;
4146
+ } else {
4147
+ ns = lazy2.ns;
4148
+ }
4149
+ let parsed = lazy2.parsed[ns];
4219
4150
  if (parsed === void 0) {
4220
- parsed = parseTemplate(opaque.html, ns, opaque.frag);
4221
- opaque.parsed[ns] = parsed;
4151
+ parsed = parseTemplate(lazy2.html, ns, lazy2.frag);
4152
+ lazy2.parsed[ns] = parsed;
4222
4153
  }
4223
4154
  const hydration2 = activeHydration();
4224
4155
  return hydration2 === null ? parsed.cloneNode(true) : hydration2.clone(parsed, loc);
@@ -6773,6 +6704,7 @@ function genericPortalBody(value, scope) {
6773
6704
  childSlot(scope, 0, scope.parentNode, value, scope.endMarker);
6774
6705
  }
6775
6706
  const PORTAL_TAG = /* @__PURE__ */ Symbol.for("octane.portal");
6707
+ // @__NO_SIDE_EFFECTS__
6776
6708
  function createPortal(body, target, props = void 0) {
6777
6709
  return { $$kind: PORTAL_TAG, body, target, props };
6778
6710
  }
@@ -8808,6 +8740,7 @@ function shallowEqualPropsExact(a, b) {
8808
8740
  }
8809
8741
  return true;
8810
8742
  }
8743
+ // @__NO_SIDE_EFFECTS__
8811
8744
  function memo(component, arePropsEqual) {
8812
8745
  function memoWrapper(props, scope, extra) {
8813
8746
  return component(props, scope, extra);
@@ -12118,6 +12051,7 @@ export {
12118
12051
  markChildrenBlock,
12119
12052
  markDangerouslySetInnerHTMLChildren,
12120
12053
  markNativeChangeDiagnosticStatic,
12054
+ markSingleRoot,
12121
12055
  memo,
12122
12056
  mountFragmentRef,
12123
12057
  namespaceHead,
@@ -605,10 +605,46 @@ export declare function renderToStaticMarkup(component: ServerComponent, props?:
605
605
  * its pending form (content ships via its segment).
606
606
  */
607
607
  export declare function ssrTry(scope: SSRScope, siteKey: string, tryFn: (arg: unknown, scope: SSRScope) => string, pendFn: ((arg: unknown, scope: SSRScope) => string) | null, catchFn: ((err: unknown, scope: SSRScope, reset: () => void) => string) | null, namespace?: 'html' | 'svg' | 'mathml', propagateSuspense?: boolean): string;
608
+ /**
609
+ * A live source of externally-produced HTML (typically framework data
610
+ * `<script>` tags materializing as loaders settle) merged natively into a
611
+ * streamed render. Octane emits injected HTML verbatim, in push order, each
612
+ * drain as its own transport chunk strictly BETWEEN renderer chunks — never
613
+ * before the shell, and (for document renders) before the held
614
+ * `</body></html>` tail. The stream stays open until `done` settles.
615
+ *
616
+ * This is an Octane extension (React's Fizz owns its data injection
617
+ * internally); it exists so frameworks like TanStack Start can merge their
618
+ * data stream without re-parsing the HTML byte stream for safe insertion
619
+ * points — every boundary between renderer chunks is tag-complete by
620
+ * construction.
621
+ */
622
+ export interface StreamInjectionSource {
623
+ /**
624
+ * Pull all queued HTML (concatenated, verbatim). Called at emission
625
+ * boundaries and after `subscribe` notifications; return '' when empty.
626
+ */
627
+ take(): string;
628
+ /**
629
+ * The source notifies when new HTML is queued; the renderer then drains
630
+ * promptly — even while the render itself is idle awaiting `done`.
631
+ * Returns an unsubscribe function; the renderer unsubscribes on
632
+ * completion, abort, and failure.
633
+ */
634
+ subscribe(notify: () => void): () => void;
635
+ /**
636
+ * The renderer holds the document tail and the stream close until this
637
+ * settles. A rejection fails the stream through the fatal path (after the
638
+ * shell, mirroring abort: degraded terminal completion).
639
+ */
640
+ done: Promise<void>;
641
+ }
608
642
  export interface StreamOptions extends RenderOptions {
609
643
  onShellReady?: () => void;
610
644
  onShellError?: (err: unknown) => void;
611
645
  onAllReady?: () => void;
646
+ /** Merge externally-produced HTML into the stream (see StreamInjectionSource). */
647
+ injection?: StreamInjectionSource;
612
648
  }
613
649
  /**
614
650
  * React `react-dom/server` `renderToPipeableStream` (Node streams). Returns
@@ -3283,6 +3283,12 @@ function withStream(stream, fn) {
3283
3283
  STREAM = prev;
3284
3284
  }
3285
3285
  }
3286
+ const DOCUMENT_TAIL_RE = /^<\/body>(?:\s|<!--[^]*?-->)*<\/html>(?:\s|<!--[^]*?-->)*$/;
3287
+ function documentTailStart(body) {
3288
+ const index = body.lastIndexOf("</body>");
3289
+ if (index === -1) return -1;
3290
+ return DOCUMENT_TAIL_RE.test(body.slice(index)) ? index : -1;
3291
+ }
3286
3292
  function segmentChunk(b, nonceAttr) {
3287
3293
  let seedScript = "";
3288
3294
  if (b.seeds.length > 0) {
@@ -3328,6 +3334,59 @@ async function runStream(component, props, options, sink) {
3328
3334
  stream.activePassBoundaryKeys = previousBoundaryKeys;
3329
3335
  }
3330
3336
  };
3337
+ const injection = options?.injection;
3338
+ if (injection !== void 0) injection.done.then(NOOP, NOOP);
3339
+ let injectionUnsubscribe;
3340
+ let injectionFailure;
3341
+ let injectionFailed = false;
3342
+ let signalInjectionFailure;
3343
+ const failInjection = (err) => {
3344
+ if (injectionFailed) return;
3345
+ injectionFailed = true;
3346
+ injectionFailure = err;
3347
+ signalInjectionFailure?.();
3348
+ };
3349
+ let writeChain = injection === void 0 ? null : Promise.resolve();
3350
+ const write = injection === void 0 ? (chunk, terminal) => sink.write(chunk, terminal) : (chunk, terminal) => {
3351
+ const operation = writeChain.then(() => sink.write(chunk, terminal));
3352
+ writeChain = operation.then(NOOP, NOOP);
3353
+ return operation;
3354
+ };
3355
+ const drainInjection = () => {
3356
+ if (injection === void 0 || injectionFailed) return;
3357
+ let html;
3358
+ try {
3359
+ html = injection.take();
3360
+ } catch (err) {
3361
+ failInjection(err);
3362
+ return;
3363
+ }
3364
+ if (!html) return;
3365
+ return write(html);
3366
+ };
3367
+ const notifyInjection = () => {
3368
+ const drained = drainInjection();
3369
+ if (drained !== void 0) drained.catch(NOOP);
3370
+ };
3371
+ const waitForInjectionDone = () => new Promise((resolve, reject) => {
3372
+ let settled = false;
3373
+ const finish = (fn) => {
3374
+ if (settled) return;
3375
+ settled = true;
3376
+ signal?.removeEventListener("abort", onAbort);
3377
+ signalInjectionFailure = void 0;
3378
+ fn();
3379
+ };
3380
+ const onAbort = () => finish(() => reject(signal.reason));
3381
+ signalInjectionFailure = () => finish(() => reject(injectionFailure));
3382
+ if (injectionFailed) return finish(() => reject(injectionFailure));
3383
+ if (signal?.aborted) return onAbort();
3384
+ signal?.addEventListener("abort", onAbort, { once: true });
3385
+ injection.done.then(
3386
+ () => finish(resolve),
3387
+ (err) => finish(() => reject(err))
3388
+ );
3389
+ });
3331
3390
  const emittedCss = /* @__PURE__ */ new Set();
3332
3391
  const flushedSegments = /* @__PURE__ */ new Set();
3333
3392
  const observedDone = /* @__PURE__ */ new Set();
@@ -3370,15 +3429,15 @@ async function runStream(component, props, options, sink) {
3370
3429
  if (errors.length === 0) return;
3371
3430
  let chunk = "";
3372
3431
  for (const boundary of errors) chunk += boundaryErrorChunk(boundary, nonceAttr);
3373
- const write = sink.write(chunk);
3432
+ const errorWrite = write(chunk);
3374
3433
  const markFlushed = () => {
3375
3434
  for (const boundary of errors) boundary.errorFlushed = true;
3376
3435
  };
3377
- if (write === void 0) {
3436
+ if (errorWrite === void 0) {
3378
3437
  markFlushed();
3379
3438
  return;
3380
3439
  }
3381
- return write.then(markFlushed);
3440
+ return errorWrite.then(markFlushed);
3382
3441
  };
3383
3442
  let pass;
3384
3443
  let shellBoundaryKeys;
@@ -3416,13 +3475,24 @@ async function runStream(component, props, options, sink) {
3416
3475
  emittedCss.add(hash);
3417
3476
  shell += '<style data-octane="' + hash + '"' + nonceAttr + ">" + escapeEntireInlineStyleContent(sheet) + "</style>";
3418
3477
  }
3419
- shell += pass.head + pass.body;
3478
+ let heldDocumentTail = "";
3479
+ if (injection !== void 0) {
3480
+ const tailStart = documentTailStart(pass.body);
3481
+ if (tailStart !== -1) {
3482
+ heldDocumentTail = pass.body.slice(tailStart);
3483
+ shell += pass.head + pass.body.slice(0, tailStart);
3484
+ } else {
3485
+ shell += pass.head + pass.body;
3486
+ }
3487
+ } else {
3488
+ shell += pass.head + pass.body;
3489
+ }
3420
3490
  if (pass.serial.length > 0) shell += serializeSuspenseSeeds(pass.serial, nonceAttr);
3421
3491
  const anyPending = stream.boundaries.size > 0;
3422
3492
  if (anyPending)
3423
3493
  shell += "<script " + STREAM_SCRIPT_ATTR + nonceAttr + ">" + STREAM_RUNTIME_JS + "</script>";
3424
3494
  try {
3425
- const shellWrite = sink.write(pass.vtCandidates ? vtSsrStrip(shell) : shell);
3495
+ const shellWrite = write(pass.vtCandidates ? vtSsrStrip(shell) : shell);
3426
3496
  if (shellWrite !== void 0) await shellWrite;
3427
3497
  } catch (err) {
3428
3498
  options?.onError?.(err);
@@ -3430,6 +3500,14 @@ async function runStream(component, props, options, sink) {
3430
3500
  return;
3431
3501
  }
3432
3502
  sink.shellReady();
3503
+ if (injection !== void 0) {
3504
+ try {
3505
+ injectionUnsubscribe = injection.subscribe(notifyInjection);
3506
+ } catch (err) {
3507
+ failInjection(err);
3508
+ }
3509
+ notifyInjection();
3510
+ }
3433
3511
  let suspended = pass.suspended;
3434
3512
  let attempt = 0;
3435
3513
  try {
@@ -3437,7 +3515,7 @@ async function runStream(component, props, options, sink) {
3437
3515
  if (initiallyDone.length > 0) {
3438
3516
  let chunk = "";
3439
3517
  for (const boundary of initiallyDone) chunk += segmentChunk(boundary, nonceAttr);
3440
- const segmentWrite = sink.write(pass.vtCandidates ? vtSsrStrip(chunk) : chunk);
3518
+ const segmentWrite = write(pass.vtCandidates ? vtSsrStrip(chunk) : chunk);
3441
3519
  if (segmentWrite !== void 0) await segmentWrite;
3442
3520
  for (const boundary of initiallyDone) {
3443
3521
  flushedSegments.add(boundary.id);
@@ -3479,7 +3557,7 @@ async function runStream(component, props, options, sink) {
3479
3557
  const done = reachableDoneSegments();
3480
3558
  for (const b of done) chunk += segmentChunk(b, nonceAttr);
3481
3559
  if (chunk !== "") {
3482
- const segmentWrite = sink.write(pass.vtCandidates ? vtSsrStrip(chunk) : chunk);
3560
+ const segmentWrite = write(pass.vtCandidates ? vtSsrStrip(chunk) : chunk);
3483
3561
  if (segmentWrite !== void 0) await segmentWrite;
3484
3562
  for (const b of done) flushedSegments.add(b.id);
3485
3563
  }
@@ -3496,16 +3574,45 @@ async function runStream(component, props, options, sink) {
3496
3574
  for (const b of stream.boundaries.values()) {
3497
3575
  if (!flushedSegments.has(b.id) && !b.errorFlushed) tail += boundaryErrorChunk(b, nonceAttr);
3498
3576
  }
3577
+ if (heldDocumentTail !== "") tail += heldDocumentTail;
3499
3578
  if (tail !== "") {
3500
3579
  try {
3501
- const terminalWrite = sink.write(tail, true);
3580
+ const terminalWrite = write(tail, true);
3502
3581
  if (terminalWrite !== void 0) await terminalWrite;
3503
3582
  } catch {
3504
3583
  }
3505
3584
  }
3585
+ injectionUnsubscribe?.();
3506
3586
  sink.fatal(err);
3507
3587
  return;
3508
3588
  }
3589
+ if (injection !== void 0) {
3590
+ try {
3591
+ await waitForInjectionDone();
3592
+ const finalDrain = drainInjection();
3593
+ if (finalDrain !== void 0) await finalDrain;
3594
+ if (injectionFailed) throw injectionFailure;
3595
+ if (heldDocumentTail !== "") {
3596
+ const tailChunk = heldDocumentTail;
3597
+ heldDocumentTail = "";
3598
+ const tailWrite = write(tailChunk);
3599
+ if (tailWrite !== void 0) await tailWrite;
3600
+ }
3601
+ } catch (err) {
3602
+ options?.onError?.(err);
3603
+ if (heldDocumentTail !== "") {
3604
+ try {
3605
+ const terminalWrite = write(heldDocumentTail, true);
3606
+ if (terminalWrite !== void 0) await terminalWrite;
3607
+ } catch {
3608
+ }
3609
+ }
3610
+ injectionUnsubscribe?.();
3611
+ sink.fatal(err);
3612
+ return;
3613
+ }
3614
+ injectionUnsubscribe?.();
3615
+ }
3509
3616
  sink.allReady();
3510
3617
  }
3511
3618
  function renderToPipeableStream(component, props, options) {
@@ -21,4 +21,4 @@
21
21
  * should call them.
22
22
  */
23
23
  export { executeServerFunction } from './rpc.js';
24
- export { renderToString, renderToStaticMarkup, renderToPipeableStream, renderToReadableStream, type RenderResult, type RenderOptions, type StreamOptions, setSsrSuspenseTimeout, getSsrSuspenseTimeout, EXTERNAL_HYDRATION_PROMISE, HYDRATION_RANGE_BOUNDARY, useState, useReducer, __useStateWithGetter, __useReducerWithGetter, useEffect, useLayoutEffect, useInsertionEffect, useImperativeHandle, useMemo, useCallback, useRef, useId, useEffectEvent, useTransition, useDeferredValue, useSyncExternalStore, useActionState, useFormStatus, useOptimistic, useDebugValue, memo, lazy, hookSlots, withSlot, startTransition, flushSync, isChildrenBlock, isValidElement, cloneElement, Children, createPortal, requestFormReset, preload, preinit, preconnect, prefetchDNS, Suspense, ErrorBoundary, Hydrate, Fragment, Activity, ViewTransition, ViewTransition as unstable_ViewTransition, addTransitionType, addTransitionType as unstable_addTransitionType, createContext, use, useContext, ssrIsSuspense, type Context, type FormStatus, markChildrenBlock, createElement, positionalChildren, escapeHtml, escapeAttr, ssrText, ssrTextPre, ssrChild, ssrChildText, ssrAttr, normalizeClass, ssrStyle, ssrClass, ssrAttrs, ssrSnapshotSpread, ssrSpread, ssrInnerHtml, ssrScriptInnerHtml, ssrChildrenSources, ssrVoidContent, ssrValueAttr, ssrCheckedAttr, ssrInputAttrs, ssrTextareaValue, ssrTextareaValueSources, ssrSelectAttrs, ssrSelectScope, ssrSelectScopeSources, ssrOptionValueSources, ssrOption, ssrElement, ssrComponent, ssrComponentNS, ssrInNamespace, ssrBlock, ssrActivity, ssrForBlock, ssrControl, ssrArm, ssrTry, ssrPortal, injectStyle, ssrHeadEl, namespaceHead, namespaceHeadElement, puMemo, puBatch, warmMemo, warmChild, } from '../runtime.server.js';
24
+ export { renderToString, renderToStaticMarkup, renderToPipeableStream, renderToReadableStream, type RenderResult, type RenderOptions, type StreamOptions, type StreamInjectionSource, setSsrSuspenseTimeout, getSsrSuspenseTimeout, EXTERNAL_HYDRATION_PROMISE, HYDRATION_RANGE_BOUNDARY, useState, useReducer, __useStateWithGetter, __useReducerWithGetter, useEffect, useLayoutEffect, useInsertionEffect, useImperativeHandle, useMemo, useCallback, useRef, useId, useEffectEvent, useTransition, useDeferredValue, useSyncExternalStore, useActionState, useFormStatus, useOptimistic, useDebugValue, memo, lazy, hookSlots, withSlot, startTransition, flushSync, isChildrenBlock, isValidElement, cloneElement, Children, createPortal, requestFormReset, preload, preinit, preconnect, prefetchDNS, Suspense, ErrorBoundary, Hydrate, Fragment, Activity, ViewTransition, ViewTransition as unstable_ViewTransition, addTransitionType, addTransitionType as unstable_addTransitionType, createContext, use, useContext, ssrIsSuspense, type Context, type FormStatus, markChildrenBlock, createElement, positionalChildren, escapeHtml, escapeAttr, ssrText, ssrTextPre, ssrChild, ssrChildText, ssrAttr, normalizeClass, ssrStyle, ssrClass, ssrAttrs, ssrSnapshotSpread, ssrSpread, ssrInnerHtml, ssrScriptInnerHtml, ssrChildrenSources, ssrVoidContent, ssrValueAttr, ssrCheckedAttr, ssrInputAttrs, ssrTextareaValue, ssrTextareaValueSources, ssrSelectAttrs, ssrSelectScope, ssrSelectScopeSources, ssrOptionValueSources, ssrOption, ssrElement, ssrComponent, ssrComponentNS, ssrInNamespace, ssrBlock, ssrActivity, ssrForBlock, ssrControl, ssrArm, ssrTry, ssrPortal, injectStyle, ssrHeadEl, namespaceHead, namespaceHeadElement, puMemo, puBatch, warmMemo, warmChild, } from '../runtime.server.js';
package/package.json CHANGED
@@ -1,14 +1,9 @@
1
1
  {
2
2
  "name": "octane",
3
- "version": "0.1.9",
3
+ "version": "0.1.10",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
- "sideEffects": [
7
- "./src/index.ts",
8
- "./src/runtime.ts",
9
- "./dist/index.js",
10
- "./dist/runtime.js"
11
- ],
6
+ "sideEffects": false,
12
7
  "engines": {
13
8
  "node": ">=22"
14
9
  },