@askrjs/askr 0.2.4 → 0.3.1

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.
Files changed (117) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/dist/access-CEA2pOry.js +1046 -0
  3. package/dist/actions/index.d.ts +16 -2
  4. package/dist/actions/index.js +5 -5
  5. package/dist/{activity-FirwPXA_.js → activity-oy2YFTFF.js} +10 -16
  6. package/dist/benchmark.js +8 -9
  7. package/dist/boot/index.d.ts +19 -9
  8. package/dist/boot/index.js +1 -1
  9. package/dist/{boot-tWyF6121.js → boot-DLplW9h4.js} +700 -441
  10. package/dist/capabilities-Bbf0mlp9.js +42 -0
  11. package/dist/components/index.d.ts +11 -8
  12. package/dist/components/index.js +1 -1
  13. package/dist/{compose-ref-Bfaf4kUC.js → compose-ref-v0qGlHoN.js} +7 -1
  14. package/dist/control/index.d.ts +20 -2
  15. package/dist/control/index.js +1 -1
  16. package/dist/{control-C-wSaq9V.js → control-BR_mrX__.js} +4 -4
  17. package/dist/core.d.ts +163 -0
  18. package/dist/csp-nonce-BfFWcd0w.js +173 -0
  19. package/dist/data/index.d.ts +74 -2
  20. package/dist/data/index.js +3 -3
  21. package/dist/{data-ghKopAbp.js → data-2edW0jhF.js} +108 -122
  22. package/dist/{data-runtime-7t_Y0iv3.js → data-runtime-CxoY98lX.js} +22 -15
  23. package/dist/{deferred-CxjWCkIn.js → deferred-CbgNhf67.js} +5 -5
  24. package/dist/dom-renderer.d.ts +77 -0
  25. package/dist/domains/actions.d.ts +70 -0
  26. package/dist/domains/component.d.ts +119 -0
  27. package/dist/domains/context.d.ts +73 -0
  28. package/dist/domains/control.d.ts +328 -0
  29. package/dist/domains/data.d.ts +461 -0
  30. package/dist/domains/diagnostics.d.ts +21 -0
  31. package/dist/domains/lifecycle.d.ts +112 -0
  32. package/dist/domains/refs.d.ts +18 -0
  33. package/dist/domains/renderer.d.ts +127 -0
  34. package/dist/domains/routing.d.ts +501 -0
  35. package/dist/domains/scheduler.d.ts +79 -0
  36. package/dist/domains/server.d.ts +112 -0
  37. package/dist/domains/state.d.ts +129 -0
  38. package/dist/domains/telemetry.d.ts +29 -0
  39. package/dist/eager-control.d.ts +8 -0
  40. package/dist/{effect-CNwstBBD.js → effect-C3Zsc1k6.js} +14 -41
  41. package/dist/{types-DWVrKn9N.d.ts → elements.d.ts} +79 -42
  42. package/dist/{for-internal-DyBHfKJA.js → for-state-BDykeOJL.js} +598 -494
  43. package/dist/foundations/icon/index.d.ts +67 -16
  44. package/dist/foundations/icon/index.js +1 -1
  45. package/dist/foundations/index.d.ts +26 -3
  46. package/dist/foundations/index.js +1 -1
  47. package/dist/foundations/interactions/index.d.ts +73 -34
  48. package/dist/foundations/interactions/index.js +1 -1
  49. package/dist/foundations/state/index.d.ts +12 -5
  50. package/dist/foundations/state/index.js +1 -1
  51. package/dist/foundations/structures/index.d.ts +44 -3
  52. package/dist/foundations/structures/index.js +1 -1
  53. package/dist/foundations/utilities/index.d.ts +34 -3
  54. package/dist/foundations/utilities/index.js +1 -1
  55. package/dist/fx/index.d.ts +72 -25
  56. package/dist/fx/index.js +8 -8
  57. package/dist/index.d.ts +121 -4
  58. package/dist/index.js +316 -47
  59. package/dist/{component-internal-C4hKKQBF.js → instance-VAFYVJOa.js} +495 -283
  60. package/dist/{interactions-D102JxBR.js → interactions-CTeACdT_.js} +1 -1
  61. package/dist/jsx-dev-runtime.d.ts +51 -11
  62. package/dist/jsx-globals.d.ts +2 -0
  63. package/dist/jsx-runtime-CPJXAOBF.js +43 -0
  64. package/dist/jsx-runtime.d.ts +17 -3
  65. package/dist/jsx-runtime.js +2 -42
  66. package/dist/jsx.d.ts +108 -0
  67. package/dist/{manifest-D6rfj_jC.js → manifest-D1P3UNWV.js} +4 -9
  68. package/dist/{navigate-ZZu1IV0B.js → navigate-prC5V8XV.js} +125 -472
  69. package/dist/{navigate-F4sHgWbw.d.ts → navigation.d.ts} +8 -8
  70. package/dist/{readable-Di1fzSwm.js → notify-DxxuDPeY.js} +96 -34
  71. package/dist/{query-registry-Bua3cDyb.js → query-registry-LrYpQsDJ.js} +1 -1
  72. package/dist/{compose-ref-DOJHoW5X.d.ts → refs.d.ts} +11 -7
  73. package/dist/{ssr-Doog-AfJ.js → render-resolved-BC0W7_5b.js} +165 -558
  74. package/dist/{renderer-Cl23O0hV.js → renderer-CT0HeY58.js} +6825 -5692
  75. package/dist/{resolution-ClD-K7Lz.js → resolution-3DskUkjG.js} +189 -180
  76. package/dist/{resource-operation-Vz1S5h2k.js → resource-18-sFRl9.js} +98 -58
  77. package/dist/resources/index.d.ts +64 -12
  78. package/dist/resources/index.js +214 -8
  79. package/dist/route-activity.d.ts +21 -0
  80. package/dist/{route-matching-Bo6UqG-5.js → route-matching-CUzZ31gT.js} +68 -82
  81. package/dist/router/index.d.ts +378 -79
  82. package/dist/router/index.js +9 -9
  83. package/dist/router-internal-7nuNEHfG.js +36 -0
  84. package/dist/scope-Cq_iYbTR.js +403 -0
  85. package/dist/{selector-CsKuomw0.js → selector-BSPVkjKP.js} +26 -27
  86. package/dist/{shared-CYBH3F8I.js → shared-kU8yvryp.js} +3 -6
  87. package/dist/{snapshot-source-BB42xgwb.js → snapshot-source-An4iKajG.js} +1 -1
  88. package/dist/ssg/index.d.ts +51 -15
  89. package/dist/ssg/index.js +269 -209
  90. package/dist/ssr/index.d.ts +121 -68
  91. package/dist/ssr/index.js +2 -1
  92. package/dist/ssr-CiNm6Vzl.js +393 -0
  93. package/dist/{ssr-DSHd_Q9K.js → ssr-EPjwJH5R.js} +5 -1
  94. package/dist/{state-BGJgYSyQ.js → state-BXJfudq1.js} +10 -15
  95. package/dist/{structures-DFyK5AdQ.js → structures-CjEzfj4h.js} +37 -57
  96. package/dist/{index-CTpMmzgN.d.ts → structures.d.ts} +57 -36
  97. package/dist/testing/index.d.ts +123 -24
  98. package/dist/testing/index.js +24 -61
  99. package/dist/timer-C7uqnJVt.js +85 -0
  100. package/dist/{index-B1gu0_m0.d.ts → utilities.d.ts} +24 -13
  101. package/dist/{verify-hydration-BZPJ_kYa.js → verify-hydration-ikaxotf7.js} +8 -5
  102. package/package.json +12 -14
  103. package/dist/access-BucjYZcX.js +0 -509
  104. package/dist/activity-DooIuxQO.d.ts +0 -14
  105. package/dist/benchmark.d.ts +0 -26
  106. package/dist/cleanup-TXSsApEl.js +0 -535
  107. package/dist/component-cleanup-jdbgjkT1.js +0 -143
  108. package/dist/component-scope-4I3yEMkb.js +0 -195
  109. package/dist/control-Cxag1yqV.d.ts +0 -8
  110. package/dist/csp-nonce-9NFUb7hJ.js +0 -72
  111. package/dist/css-BuPaTOfe.js +0 -70
  112. package/dist/fastlane-BKZduiBQ.js +0 -166
  113. package/dist/index-Dgjbo2gU.d.ts +0 -1456
  114. package/dist/jsx-runtime-Cg7GqY1S.d.ts +0 -2
  115. package/dist/jsx-runtime-gVIaTPw0.d.ts +0 -38
  116. package/dist/lifecycle-batch-63aXFf2S.js +0 -253
  117. package/dist/lifecycle-operations-Q0k3EFnG.js +0 -287
@@ -1,177 +1,317 @@
1
- import { S as isProductionEnvironment, r as flushRuntimeScheduler, x as isDevelopmentEnvironment, y as logger } from "./access-BucjYZcX.js";
1
+ import { T as isProductionEnvironment, i as getRuntimeCleanup, l as installRuntimeRenderer, r as flushRuntimeScheduler, v as logger, w as isDevelopmentEnvironment } from "./access-CEA2pOry.js";
2
+ import { D as createComponentOwnership, E as cleanupComponentGeneration, F as captureGenerationExecution, N as assertSyncComponentResult, T as cleanupComponent, k as resetComponentWork, n as createComponentInstance, r as executeComponent } from "./instance-VAFYVJOa.js";
3
+ import { a as registerDeferredHydrationBoundary, c as beginHydrationDirectListenerMode, d as commitHydrationListenerTransaction, f as discardHydrationListenerTransaction, g as teardownNodeSubtree, h as removeAllListeners, i as clearDeferredHydrationBoundaries, l as endHydrationDirectListenerMode, n as activateHydrationBoundary$1, o as setStaticChildSlotsCacheEnabled, s as withIntrinsicHydrationAdoption, t as createRendererCapabilities, u as beginHydrationListenerTransaction } from "./renderer-CT0HeY58.js";
4
+ import { b as attachOwnership } from "./scope-Cq_iYbTR.js";
5
+ import { m as cleanupReadableSubscriptionSources } from "./notify-DxxuDPeY.js";
2
6
  import { n as Fragment, t as ELEMENT_TYPE } from "./jsx-DRS-qjiN.js";
3
- import { n as createComponentInstance, r as executeComponent } from "./component-internal-C4hKKQBF.js";
4
- import { t as cleanupComponent } from "./component-cleanup-jdbgjkT1.js";
5
- import { p as isPromiseLike } from "./lifecycle-batch-63aXFf2S.js";
6
- import { _ as teardownNodeSubtree, d as removeAllListeners } from "./cleanup-TXSsApEl.js";
7
- import { r as assertExecutionModel, t as _applyManifest } from "./manifest-D6rfj_jC.js";
7
+ import { r as assertExecutionModel, t as _applyManifest } from "./manifest-D1P3UNWV.js";
8
8
  import { d as stopHydrationRenderPhase, l as startHydrationRenderPhase, y as pageRenderEnvelope } from "./render-context-B7ohlh6t.js";
9
- import { a as setStaticChildSlotsCacheEnabled, c as endHydrationDirectListenerMode, d as discardHydrationListenerTransaction, i as registerDeferredHydrationBoundary, l as beginHydrationListenerTransaction, n as activateHydrationBoundary$1, o as withIntrinsicHydrationAdoption, r as clearDeferredHydrationBoundaries, s as beginHydrationDirectListenerMode, t as installRendererBridge, u as commitHydrationListenerTransaction } from "./renderer-Cl23O0hV.js";
10
- import { r as validateCspNonce, t as CspNonceScope } from "./csp-nonce-9NFUb7hJ.js";
11
- import { i as getDefaultDataRuntime } from "./data-runtime-7t_Y0iv3.js";
12
- import { a as hydrateDataRuntime } from "./query-registry-Bua3cDyb.js";
13
- import { I as lockRouteRegistration, P as hasRegisteredRoutes, _ as clearRouteState, m as _setActiveRouteAuthOptions } from "./route-matching-Bo6UqG-5.js";
14
- import { a as resolveRouteRequest, i as hasRouteRenderData, m as reviveDeferredValue, n as getRouteRenderContext, r as getRouteRenderData } from "./resolution-ClD-K7Lz.js";
15
- import { o as createAppRenderRuntime, r as setServerLocation } from "./activity-FirwPXA_.js";
16
- import { f as registerAppInstance, g as unregisterAppInstance, i as resolveRouteMeta, o as configureScrollRestoration, r as reconcileRouteMeta, t as initializeNavigation } from "./navigate-ZZu1IV0B.js";
17
- import { n as disposeRegisteredDefaultPortalScope, r as getDefaultPortalHost } from "./default-portal-runtime-DvMvWJVp.js";
18
- import "./ssr-DSHd_Q9K.js";
19
- //#region src/boot/hydration.ts
20
- const DEFERRED_PAYLOAD = "__askr_deferred__";
21
- function applyDeferredStreamPatches(rootElement) {
22
- const patches = Array.from(rootElement.querySelectorAll("template[data-askr-deferred-patch]"));
23
- for (const template of patches) {
24
- const id = template.getAttribute("data-askr-deferred-patch");
25
- if (!id) continue;
26
- const boundary = Array.from(rootElement.querySelectorAll("askr-resolve[data-askr-deferred]")).find((candidate) => candidate.getAttribute("data-askr-deferred") === id);
27
- if (boundary) boundary.replaceWith(template.content.cloneNode(true));
28
- template.remove();
29
- for (const script of Array.from(rootElement.querySelectorAll("script[data-askr-deferred-apply]"))) if (script.getAttribute("data-askr-deferred-apply") === id) script.remove();
30
- }
31
- }
32
- function hydrationReviver(_key, value) {
33
- if (!value || typeof value !== "object") return value;
34
- const payload = value;
35
- if (payload[DEFERRED_PAYLOAD] === "fulfilled") return reviveDeferredValue("fulfilled", payload.value, void 0);
36
- if (payload[DEFERRED_PAYLOAD] === "rejected") return reviveDeferredValue("rejected", void 0, payload.error);
37
- return value;
38
- }
39
- function takeHydrationRenderData(rootElement) {
40
- for (const child of Array.from(rootElement.children)) if (child instanceof HTMLScriptElement && child.getAttribute("data-askr-render-data") === "true") {
41
- const raw = child.textContent ?? "";
42
- child.remove();
43
- if (!raw) return pageRenderEnvelope(void 0);
44
- try {
45
- return pageRenderEnvelope(JSON.parse(raw, hydrationReviver));
46
- } catch (err) {
47
- const error = /* @__PURE__ */ new Error("[Askr] Failed to parse embedded SSR render data during hydration.");
48
- error.cause = err;
49
- throw error;
50
- }
9
+ import { r as validateCspNonce, t as CspNonceScope } from "./csp-nonce-BfFWcd0w.js";
10
+ import { i as getDefaultDataRuntime } from "./data-runtime-CxoY98lX.js";
11
+ import { a as hydrateDataRuntime } from "./query-registry-LrYpQsDJ.js";
12
+ import { _ as configureRootUpdateHost, f as registerAppInstance, g as unregisterAppInstance, i as resolveRouteMeta, o as configureScrollRestoration, r as reconcileRouteMeta, t as initializeNavigation } from "./navigate-prC5V8XV.js";
13
+ import { n as disposeRegisteredDefaultPortalScope, r as getDefaultPortalHost, t as clearRegisteredDefaultPortalForInstance } from "./default-portal-runtime-DvMvWJVp.js";
14
+ import { a as clearStagedAppRenderRouteLocation, o as createAppRenderRuntime, r as setServerLocation, s as stageAppRenderRouteLocation } from "./activity-oy2YFTFF.js";
15
+ import { C as reviveDeferredValue, a as resolveRouteRequest, i as hasRouteRenderData, n as getRouteRenderContext, r as getRouteRenderData, y as withoutRouteHydrationMetadata } from "./resolution-3DskUkjG.js";
16
+ import { F as hasRegisteredRoutes, L as lockRouteRegistration, _ as clearRouteState, m as _setActiveRouteAuthOptions } from "./route-matching-CUzZ31gT.js";
17
+ import "./ssr-EPjwJH5R.js";
18
+ //#region src/runtime/component/generation.ts
19
+ /** Reuse an execution record after boot has retired its previous lifetime. */
20
+ function restartComponentGeneration(instance, fn, preserveState) {
21
+ instance.owner = createComponentOwnership(instance);
22
+ instance.fn = fn;
23
+ instance.evaluationGeneration++;
24
+ instance.expectedStateIndices = [];
25
+ instance.firstRenderComplete = false;
26
+ if (!preserveState) {
27
+ resetComponentWork(instance);
28
+ instance.stateValues = [];
29
+ instance.stateIndexCheck = -1;
30
+ instance._currentRenderToken = void 0;
31
+ instance.lastRenderToken = 0;
32
+ instance.errorBoundaryState = void 0;
33
+ instance._portalErrorParent = void 0;
34
+ instance._portalErrorParentGeneration = void 0;
35
+ instance.devWarningsEmitted = void 0;
51
36
  }
52
- return null;
53
- }
54
- function markSkippedElements(root, skipSelectors) {
55
- if (skipSelectors.length === 0) return;
56
- const selectorList = Array.from(new Set(skipSelectors)).join(", ");
57
- root.querySelectorAll(selectorList).forEach((el) => el.setAttribute("data-skip-hydrate", "true"));
58
37
  }
59
- function collectDeferredBelowFoldBoundaries(root, foldY) {
60
- const boundaries = [];
61
- const stack = [];
62
- for (let index = root.children.length - 1; index >= 0; index -= 1) stack.push(root.children[index]);
63
- while (stack.length > 0) {
64
- const element = stack.pop();
65
- if (element.hasAttribute("data-skip-hydrate")) continue;
66
- if (element.getBoundingClientRect().top >= foldY) {
67
- element.setAttribute("data-skip-hydrate", "true");
68
- registerDeferredHydrationBoundary(root, element);
69
- boundaries.push(element);
70
- continue;
38
+ /** Runtime-owned preparation and retirement. A host may restore its own state
39
+ * between disposing provisional work and restoring the previous execution. */
40
+ function captureComponentGeneration(instance) {
41
+ const restoreHostIndex = getRuntimeCleanup().captureComponentHost(instance);
42
+ const snapshot = captureGenerationExecution(instance);
43
+ const reads = instance.owner.reads;
44
+ const readerEntries = Array.from(reads ?? [], (source) => ({
45
+ source,
46
+ reader: source._readers?.get(instance)
47
+ }));
48
+ let settled = false;
49
+ return {
50
+ prepare(fn, props) {
51
+ if (settled || instance.owner !== snapshot.owner) throw new Error("[Askr] component generation is no longer available for preparation");
52
+ const owner = createComponentOwnership(instance);
53
+ owner.mounted = snapshot.owner.mounted;
54
+ instance.owner = owner;
55
+ attachOwnership(owner, snapshot.owner.parent);
56
+ instance.fn = fn;
57
+ instance.props = props;
58
+ resetComponentWork(instance);
59
+ instance.stateValues = [];
60
+ instance.expectedStateIndices = [];
61
+ instance.firstRenderComplete = false;
62
+ instance.stateIndexCheck = -1;
63
+ instance.evaluationGeneration++;
64
+ instance.lifecycleGeneration++;
65
+ instance.mountOperations = [];
66
+ instance.commitOperations = [];
67
+ instance.lifecycleSlots = [];
68
+ instance._currentRenderToken = void 0;
69
+ instance.lastRenderToken = 0;
70
+ },
71
+ rollback(restoreHost) {
72
+ if (settled) return [];
73
+ settled = true;
74
+ const errors = [];
75
+ const provisional = instance.owner;
76
+ if (provisional !== snapshot.owner) {
77
+ try {
78
+ cleanupReadableSubscriptionSources(instance, provisional.reads, provisional.identity);
79
+ } catch (error) {
80
+ errors.push(error);
81
+ }
82
+ provisional.reads = void 0;
83
+ try {
84
+ cleanupComponent(instance);
85
+ } catch (error) {
86
+ errors.push(error);
87
+ }
88
+ }
89
+ try {
90
+ errors.push(...restoreHost());
91
+ } catch (error) {
92
+ errors.push(error);
93
+ }
94
+ Object.assign(instance, snapshot);
95
+ try {
96
+ restoreHostIndex?.();
97
+ } catch (error) {
98
+ errors.push(error);
99
+ }
100
+ snapshot.owner.reads = reads;
101
+ for (const { source, reader } of readerEntries) {
102
+ if (!reader) {
103
+ if (source._readers?.delete(instance) && false);
104
+ continue;
105
+ }
106
+ const readers = source._readers ??= /* @__PURE__ */ new Map();
107
+ readers.has(instance);
108
+ readers.set(instance, reader);
109
+ }
110
+ return errors;
111
+ },
112
+ retire() {
113
+ if (settled) return;
114
+ settled = true;
115
+ cleanupComponentGeneration(instance, snapshot.owner);
71
116
  }
72
- for (let index = element.children.length - 1; index >= 0; index -= 1) stack.push(element.children[index]);
73
- }
74
- return boundaries;
117
+ };
75
118
  }
76
- function activateVisibleDeferredBoundaries(root, boundaries, foldY, activate) {
77
- let activated = false;
78
- let remaining = 0;
79
- for (const element of boundaries) {
80
- if (!element.hasAttribute("data-skip-hydrate")) continue;
81
- if (element.getBoundingClientRect().top < foldY) try {
82
- if (activate(root, element)) activated = true;
83
- } catch {}
84
- if (element.hasAttribute("data-skip-hydrate")) remaining += 1;
85
- }
119
+ //#endregion
120
+ //#region src/renderer/ownership/root-snapshot.ts
121
+ function captureRootTree(root) {
122
+ if (!root) return null;
123
+ const nodes = [];
124
+ const visit = (node) => {
125
+ nodes.push({
126
+ node,
127
+ children: Array.from(node.childNodes),
128
+ attributes: node instanceof Element ? Array.from(node.attributes).map((attribute) => [attribute.name, attribute.value]) : null,
129
+ nodeValue: node.nodeValue
130
+ });
131
+ for (const child of Array.from(node.childNodes)) visit(child);
132
+ };
133
+ visit(root);
86
134
  return {
87
- activated,
88
- remaining
135
+ root,
136
+ nodes
89
137
  };
90
138
  }
91
- function queueIdleWork(work) {
92
- return new Promise((resolve) => {
93
- if (typeof requestIdleCallback !== "undefined") {
94
- requestIdleCallback(() => {
95
- work();
96
- resolve();
97
- }, { timeout: 2e3 });
139
+ function collectProvisionalRootNodes(snapshot) {
140
+ const originalNodes = new Set(snapshot.nodes.map((entry) => entry.node));
141
+ const provisional = [];
142
+ const visit = (node) => {
143
+ if (!originalNodes.has(node)) {
144
+ provisional.push(node);
98
145
  return;
99
146
  }
100
- setTimeout(() => {
101
- work();
102
- resolve();
103
- }, 0);
104
- });
105
- }
106
- function shouldVerifyHydrationMarkup(config) {
107
- const explicit = config.hydrate?.verifyMarkup;
108
- if (typeof explicit === "boolean") return explicit;
109
- return !isProductionEnvironment();
110
- }
111
- async function applySelectiveHydration(rootElement, resolved, path, cleanupStrict, hydrateOptions, source, hooks, interactionReplay) {
112
- const hasBelowFoldDeferral = !!hydrateOptions.deferBelowFold;
113
- let staticChildSlotsCacheSuspended = false;
114
- let releaseSelectiveHydrationResources = () => {};
115
- const restoreStaticChildSlotsCache = () => {
116
- if (!staticChildSlotsCacheSuspended) return;
117
- setStaticChildSlotsCacheEnabled(true);
118
- staticChildSlotsCacheSuspended = false;
147
+ for (const child of Array.from(node.childNodes)) visit(child);
119
148
  };
120
- if (hydrateOptions.skipSelectors?.length) markSkippedElements(rootElement, hydrateOptions.skipSelectors);
121
- let deferredBoundaries = [];
122
- if (hydrateOptions.deferBelowFold) {
123
- setStaticChildSlotsCacheEnabled(false);
124
- staticChildSlotsCacheSuspended = true;
125
- const foldY = hydrateOptions.foldThreshold ?? window.innerHeight;
126
- deferredBoundaries = collectDeferredBelowFoldBoundaries(rootElement, foldY);
127
- interactionReplay.registerDeferredBoundaries(deferredBoundaries);
128
- let selectiveHydrationResourcesReleased = false;
129
- let unregisterRootCleanupCallback = () => {};
130
- function handleScroll() {
131
- const { activated, remaining } = activateVisibleDeferredBoundaries(rootElement, deferredBoundaries, foldY, hooks.activateHydrationBoundary);
132
- if (!activated) return;
133
- if (remaining === 0) releaseSelectiveHydrationResources();
134
- }
135
- releaseSelectiveHydrationResources = () => {
136
- if (selectiveHydrationResourcesReleased) return;
137
- selectiveHydrationResourcesReleased = true;
138
- unregisterRootCleanupCallback();
139
- window.removeEventListener("scroll", handleScroll);
140
- restoreStaticChildSlotsCache();
141
- interactionReplay.clearDeferredBoundaries();
142
- };
143
- interactionReplay.setOnDeferredBoundariesDrained(releaseSelectiveHydrationResources);
144
- unregisterRootCleanupCallback = hooks.registerRootCleanupCallback(rootElement, releaseSelectiveHydrationResources);
145
- window.addEventListener("scroll", handleScroll, { passive: true });
146
- }
147
- if (hydrateOptions.deferUntilIdle && !hasBelowFoldDeferral) {
148
- await queueIdleWork(() => {
149
- hooks.mountOrUpdate(rootElement, (() => resolved.handler(resolved.params)), {
150
- cleanupStrict,
151
- appRuntime: source?.runtime
152
- });
153
- });
154
- await hooks.registerAppNavigation(rootElement, path, source);
155
- return;
156
- }
157
- try {
158
- hooks.mountOrUpdate(rootElement, (() => resolved.handler(resolved.params)), {
159
- cleanupStrict,
160
- appRuntime: source?.runtime
161
- });
162
- await hooks.registerAppNavigation(rootElement, path, source);
149
+ for (const child of Array.from(snapshot.root.childNodes)) visit(child);
150
+ return provisional;
151
+ }
152
+ function restoreRootTree(snapshot) {
153
+ if (!snapshot) return [];
154
+ const errors = [];
155
+ for (const node of collectProvisionalRootNodes(snapshot)) try {
156
+ teardownNodeSubtree(node, { strict: true });
163
157
  } catch (error) {
164
- releaseSelectiveHydrationResources();
165
- throw error;
158
+ errors.push(error);
166
159
  }
167
- if (hydrateOptions.deferUntilIdle && deferredBoundaries.length > 0) await queueIdleWork(() => {
160
+ for (const entry of snapshot.nodes) {
161
+ const { node, attributes } = entry;
168
162
  try {
169
- activateVisibleDeferredBoundaries(rootElement, deferredBoundaries, Number.POSITIVE_INFINITY, hooks.activateHydrationBoundary);
170
- } finally {
171
- releaseSelectiveHydrationResources();
163
+ if (attributes && node instanceof Element) {
164
+ const expected = new Map(attributes);
165
+ for (const attribute of Array.from(node.attributes)) if (!expected.has(attribute.name)) node.removeAttribute(attribute.name);
166
+ for (const [name, value] of attributes) if (node.getAttribute(name) !== value) node.setAttribute(name, value);
167
+ } else if (!(node instanceof Element)) node.nodeValue = entry.nodeValue;
168
+ if (node instanceof Element || node instanceof DocumentFragment) node.replaceChildren(...entry.children);
169
+ } catch (error) {
170
+ errors.push(error);
172
171
  }
172
+ }
173
+ return errors;
174
+ }
175
+ function captureRootHost(root) {
176
+ const snapshot = captureRootTree(root);
177
+ return { restore: () => restoreRootTree(snapshot) };
178
+ }
179
+ //#endregion
180
+ //#region src/boot/root-handler.ts
181
+ function wrapRootRouteHandler(componentFn, cspNonce) {
182
+ const wrappedFn = (props, ctx) => {
183
+ const out = componentFn(props, ctx);
184
+ assertSyncComponentResult(out);
185
+ const portalVNode = {
186
+ $$typeof: ELEMENT_TYPE,
187
+ type: getDefaultPortalHost(),
188
+ props: { __askrAutoDefaultPortal: true },
189
+ key: "__default_portal"
190
+ };
191
+ const root = {
192
+ $$typeof: ELEMENT_TYPE,
193
+ type: Fragment,
194
+ props: { children: out === void 0 || out === null ? [portalVNode] : [out, portalVNode] }
195
+ };
196
+ return cspNonce === void 0 ? root : CspNonceScope({
197
+ value: cspNonce,
198
+ children: root
199
+ });
200
+ };
201
+ Object.defineProperty(wrappedFn, "name", { value: componentFn.name || "Component" });
202
+ return wrappedFn;
203
+ }
204
+ //#endregion
205
+ //#region src/boot/root-update.ts
206
+ function prepare(root, input) {
207
+ const instance = root;
208
+ const generation = captureComponentGeneration(instance);
209
+ const host = captureRootHost(instance.target);
210
+ const current = instance._appRenderRuntime;
211
+ const runtime = createAppRenderRuntime({
212
+ framework: withoutRouteHydrationMetadata(current?.framework),
213
+ dataRuntime: current?.dataRuntime,
214
+ routeRegistry: current?.routeRegistry,
215
+ routeAuth: current?.routeAuth,
216
+ route: input.routeData,
217
+ hasRoute: input.hasRouteData
173
218
  });
174
- if (deferredBoundaries.length === 0) releaseSelectiveHydrationResources();
219
+ let applied = false;
220
+ let settled = false;
221
+ return {
222
+ apply() {
223
+ if (applied) return;
224
+ applied = true;
225
+ stageAppRenderRouteLocation(runtime, input.href);
226
+ instance._appRenderRuntime = runtime;
227
+ if (input.replaceLifetime) {
228
+ clearRegisteredDefaultPortalForInstance(instance);
229
+ generation.prepare(wrapRootRouteHandler(input.handler, instance._cspNonce), {});
230
+ executeComponent(instance);
231
+ } else instance._enqueueRun?.();
232
+ },
233
+ publish() {
234
+ clearStagedAppRenderRouteLocation(runtime);
235
+ },
236
+ rollback() {
237
+ if (settled) return [];
238
+ settled = true;
239
+ clearStagedAppRenderRouteLocation(runtime);
240
+ return applied ? generation.rollback(() => host.restore()) : [];
241
+ },
242
+ retire() {
243
+ if (settled) return [];
244
+ settled = true;
245
+ const errors = [];
246
+ if (applied && input.replaceLifetime) try {
247
+ generation.retire();
248
+ } catch (error) {
249
+ errors.push(error);
250
+ }
251
+ return errors;
252
+ }
253
+ };
254
+ }
255
+ function installRootUpdateHost() {
256
+ configureRootUpdateHost({ prepare });
257
+ }
258
+ //#endregion
259
+ //#region src/boot/runtime-wiring.ts
260
+ let bridged = false;
261
+ /**
262
+ * Browser composition owns the connection between execution and rendering.
263
+ *
264
+ * Installs unconditionally: importing the boot entry means native boot owns the
265
+ * renderer, replacing whatever a consumer had configured before that point.
266
+ */
267
+ function installRendererBridge() {
268
+ bridged = true;
269
+ installRuntimeRenderer(createRendererCapabilities());
270
+ return true;
271
+ }
272
+ /**
273
+ * Connect execution to the DOM renderer only if nothing has yet.
274
+ *
275
+ * Unlike the install above this leaves an already-installed host alone, so a
276
+ * renderer a consumer configured after composing boot survives. Kept in this
277
+ * module rather than the composition root so a caller that only needs
278
+ * rendering — driving the runtime directly, as the jsdom test environment does
279
+ * — does not pull root-lifetime wiring into its module graph.
280
+ */
281
+ function ensureRendererBridge() {
282
+ if (bridged) return;
283
+ installRendererBridge();
284
+ }
285
+ //#endregion
286
+ //#region src/boot/composition.ts
287
+ let rootUpdateHosted = false;
288
+ function ensureRootUpdateHost() {
289
+ if (rootUpdateHosted) return;
290
+ rootUpdateHosted = true;
291
+ installRootUpdateHost();
292
+ }
293
+ /**
294
+ * Browser composition root, owned by the boot entry.
295
+ *
296
+ * Wiring a host is composition, not an import side effect of an implementation
297
+ * module: when these calls lived at the top of root-lifecycle.ts, the import
298
+ * order of unrelated modules decided whether a host existed. An entry barrel is
299
+ * where composition belongs, so importing `@askrjs/askr/boot` installs the
300
+ * native renderer — that is the contract consumers rely on.
301
+ */
302
+ function composeBrowserRuntime() {
303
+ installRendererBridge();
304
+ ensureRootUpdateHost();
305
+ }
306
+ /**
307
+ * Compose whatever is still missing, for a mount reached without the boot entry.
308
+ *
309
+ * Non-clobbering: a renderer the consumer configured after composing boot stays
310
+ * installed across mounts.
311
+ */
312
+ function ensureBrowserRuntime() {
313
+ ensureRendererBridge();
314
+ ensureRootUpdateHost();
175
315
  }
176
316
  //#endregion
177
317
  //#region src/boot/root-element.ts
@@ -182,18 +322,13 @@ function resolveRootElement(root) {
182
322
  }
183
323
  //#endregion
184
324
  //#region src/boot/root-lifecycle.ts
185
- installRendererBridge();
186
325
  let componentIdCounter = 0;
187
326
  const instancesByRoot = /* @__PURE__ */ new WeakMap();
188
327
  const routedRoots = /* @__PURE__ */ new Set();
189
328
  const CLEANUP_SYMBOL = Symbol.for("__askrCleanup__");
190
329
  const ROOT_CLEANUP_CALLBACKS_SYMBOL = Symbol.for("__askrRootCleanupCallbacks__");
191
330
  function clearRootCleanupCallbacks(rootElement) {
192
- try {
193
- delete rootElement[ROOT_CLEANUP_CALLBACKS_SYMBOL];
194
- } catch {
195
- rootElement[ROOT_CLEANUP_CALLBACKS_SYMBOL]?.clear();
196
- }
331
+ if (!Reflect.deleteProperty(rootElement, ROOT_CLEANUP_CALLBACKS_SYMBOL)) rootElement[ROOT_CLEANUP_CALLBACKS_SYMBOL]?.clear();
197
332
  }
198
333
  function registerRootCleanupCallback(rootElement, callback) {
199
334
  const elementWithCleanup = rootElement;
@@ -202,44 +337,31 @@ function registerRootCleanupCallback(rootElement, callback) {
202
337
  elementWithCleanup[ROOT_CLEANUP_CALLBACKS_SYMBOL] = callbacks;
203
338
  return () => {
204
339
  callbacks.delete(callback);
205
- if (callbacks.size === 0) clearRootCleanupCallbacks(rootElement);
340
+ if (callbacks.size === 0 && elementWithCleanup[ROOT_CLEANUP_CALLBACKS_SYMBOL] === callbacks) clearRootCleanupCallbacks(rootElement);
206
341
  };
207
342
  }
208
- function runRootCleanupCallbacks(rootElement, errors) {
209
- const callbacks = rootElement[ROOT_CLEANUP_CALLBACKS_SYMBOL];
210
- if (!callbacks || callbacks.size === 0) {
211
- clearRootCleanupCallbacks(rootElement);
212
- return;
213
- }
214
- clearRootCleanupCallbacks(rootElement);
215
- for (const callback of callbacks) try {
216
- callback();
217
- } catch (e) {
218
- errors.push(e);
219
- }
220
- }
221
343
  function cleanupRootInstance(rootElement, instance, options) {
344
+ if (instancesByRoot.get(rootElement) !== instance) return;
345
+ const element = rootElement;
346
+ const cleanup = element[CLEANUP_SYMBOL];
347
+ const callbacks = element[ROOT_CLEANUP_CALLBACKS_SYMBOL];
348
+ const wasRoutedRoot = routedRoots.delete(rootElement);
349
+ instancesByRoot.delete(rootElement);
350
+ clearRootCleanupCallbacks(rootElement);
351
+ Reflect.deleteProperty(element, CLEANUP_SYMBOL);
352
+ clearDeferredHydrationBoundaries(rootElement);
222
353
  const errors = [];
223
- try {
224
- teardownNodeSubtree(rootElement);
225
- } catch (e) {
226
- errors.push(e);
227
- }
228
- try {
229
- cleanupComponent(instance);
354
+ for (const phase of [[() => teardownNodeSubtree(rootElement), () => cleanupComponent(instance)], callbacks ?? []]) for (const callback of phase) try {
355
+ callback();
230
356
  } catch (e) {
231
357
  errors.push(e);
232
358
  }
233
- runRootCleanupCallbacks(rootElement, errors);
234
- clearDeferredHydrationBoundaries(rootElement);
235
- if (!options?.preserveInstance) {
236
- unregisterAppInstance(instance);
237
- instancesByRoot.delete(rootElement);
238
- clearRootCleanupCallbacks(rootElement);
239
- try {
240
- delete rootElement[CLEANUP_SYMBOL];
241
- } catch {}
242
- }
359
+ if (options?.preserveInstance && !instancesByRoot.has(rootElement)) {
360
+ instancesByRoot.set(rootElement, instance);
361
+ element[CLEANUP_SYMBOL] = cleanup;
362
+ if (wasRoutedRoot) routedRoots.add(rootElement);
363
+ } else unregisterAppInstance(instance);
364
+ if (wasRoutedRoot && routedRoots.size === 0) clearRouteState();
243
365
  if (errors.length > 0) {
244
366
  if (instance.cleanupStrict) throw new AggregateError(errors, `cleanup failed for app root`);
245
367
  else if (isDevelopmentEnvironment()) for (const err of errors) logger.warn("[Askr] cleanup error:", err);
@@ -249,45 +371,36 @@ function attachCleanupForRoot(rootElement, instance) {
249
371
  rootElement[CLEANUP_SYMBOL] = (options) => {
250
372
  cleanupRootInstance(rootElement, instance, options);
251
373
  };
374
+ registerRootCleanupCallback(rootElement, () => {
375
+ disposeRegisteredDefaultPortalScope(instance.portalScope ?? instance);
376
+ });
252
377
  try {
253
378
  const descriptor = Object.getOwnPropertyDescriptor(rootElement, "innerHTML") || Object.getOwnPropertyDescriptor(Object.getPrototypeOf(rootElement), "innerHTML") || Object.getOwnPropertyDescriptor(Element.prototype, "innerHTML");
254
379
  if (descriptor && (descriptor.get || descriptor.set)) Object.defineProperty(rootElement, "innerHTML", {
255
- get: descriptor.get ? function() {
256
- return descriptor.get.call(this);
257
- } : void 0,
380
+ get: function() {
381
+ return descriptor.get?.call(this);
382
+ },
258
383
  set: function(value) {
259
- if (value === "" && instancesByRoot.get(this) === instance) cleanupRootInstance(rootElement, instance);
260
- if (descriptor.set) return descriptor.set.call(this, value);
384
+ if (value === "" && instancesByRoot.get(this) === instance) {
385
+ cleanupRootInstance(rootElement, instance);
386
+ if (instancesByRoot.has(this)) return;
387
+ }
388
+ return descriptor.set?.call(this, value);
261
389
  },
262
390
  configurable: true
263
391
  });
264
392
  } catch {}
265
393
  }
266
394
  function mountOrUpdate(rootElement, componentFn, options) {
395
+ ensureBrowserRuntime();
267
396
  const nonce = validateCspNonce(options?.cspNonce);
268
- const wrappedFn = (props, ctx) => {
269
- const out = componentFn(props, ctx);
270
- if (isPromiseLike(out)) throw new Error("Async components are not supported. Components must return synchronously.");
271
- const portalVNode = {
272
- $$typeof: ELEMENT_TYPE,
273
- type: getDefaultPortalHost(),
274
- props: { __askrAutoDefaultPortal: true },
275
- key: "__default_portal"
276
- };
277
- const root = {
278
- $$typeof: ELEMENT_TYPE,
279
- type: Fragment,
280
- props: { children: out === void 0 || out === null ? [portalVNode] : [out, portalVNode] }
281
- };
282
- return nonce === void 0 ? root : CspNonceScope({
283
- value: nonce,
284
- children: root
285
- });
286
- };
287
- Object.defineProperty(wrappedFn, "name", { value: componentFn.name || "Component" });
397
+ const wrappedFn = wrapRootRouteHandler(componentFn, nonce);
288
398
  const existingCleanup = rootElement[CLEANUP_SYMBOL];
289
399
  const reusedExistingInstance = typeof existingCleanup === "function";
290
- if (reusedExistingInstance) existingCleanup({ preserveInstance: true });
400
+ if (reusedExistingInstance) {
401
+ existingCleanup({ preserveInstance: true });
402
+ if (rootElement[CLEANUP_SYMBOL] !== existingCleanup) return;
403
+ }
291
404
  let instance = instancesByRoot.get(rootElement);
292
405
  if (instance) {
293
406
  const shouldResetHookState = instance._rootComponentFn !== componentFn;
@@ -299,51 +412,19 @@ function mountOrUpdate(rootElement, componentFn, options) {
299
412
  if (isDevelopmentEnvironment()) logger.warn("[Askr] prior cleanup threw:", e);
300
413
  }
301
414
  }
302
- instance.fn = wrappedFn;
303
- instance._rootComponentFn = componentFn;
304
- instance.evaluationGeneration++;
305
- instance.mounted = false;
306
- instance.expectedStateIndices = [];
307
- instance.firstRenderComplete = false;
308
- instance.isRoot = true;
309
- instance.portalScope = instance;
310
- instance._appRenderRuntime = options?.appRuntime;
311
- instance._cspNonce = nonce;
312
- if (shouldResetHookState) {
313
- instance.stateValues = [];
314
- instance.hasPendingUpdate = false;
315
- instance.notifyUpdate = null;
316
- instance.stateIndexCheck = -1;
317
- instance.mountOperations = void 0;
318
- instance.commitOperations = void 0;
319
- instance.lifecycleSlots = void 0;
320
- instance.cleanupFns = void 0;
321
- instance._currentRenderToken = void 0;
322
- instance.lastRenderToken = 0;
323
- instance._pendingReadSources = void 0;
324
- instance._lastReadSources = void 0;
325
- instance._placeholder = void 0;
326
- instance.errorBoundaryState = void 0;
327
- instance._portalErrorParent = void 0;
328
- instance._portalErrorParentGeneration = void 0;
329
- instance.devWarningsEmitted = void 0;
330
- }
331
- if (options && typeof options.cleanupStrict === "boolean") instance.cleanupStrict = options.cleanupStrict;
415
+ restartComponentGeneration(instance, wrappedFn, !shouldResetHookState);
332
416
  } else {
333
417
  const componentId = String(++componentIdCounter);
334
- instance = createComponentInstance(componentId, wrappedFn, {}, rootElement);
418
+ instance = createComponentInstance(componentId, wrappedFn, {}, rootElement, null);
335
419
  instancesByRoot.set(rootElement, instance);
336
- instance.isRoot = true;
337
- instance._rootComponentFn = componentFn;
338
- instance.portalScope = instance;
339
- instance._appRenderRuntime = options?.appRuntime;
340
- instance._cspNonce = nonce;
341
- if (options && typeof options.cleanupStrict === "boolean") instance.cleanupStrict = options.cleanupStrict;
342
420
  }
421
+ instance.isRoot = true;
422
+ instance._rootComponentFn = componentFn;
423
+ instance.portalScope = instance;
424
+ instance._appRenderRuntime = options?.appRuntime;
425
+ instance._cspNonce = nonce;
426
+ if (options && typeof options.cleanupStrict === "boolean") instance.cleanupStrict = options.cleanupStrict;
343
427
  attachCleanupForRoot(rootElement, instance);
344
- registerRootCleanupCallback(rootElement, () => {
345
- disposeRegisteredDefaultPortalScope(instance.portalScope ?? instance);
346
- });
347
428
  executeComponent(instance);
348
429
  flushRuntimeScheduler();
349
430
  }
@@ -355,6 +436,7 @@ function activateHydrationBoundary(rootElement, boundary) {
355
436
  return activated;
356
437
  }
357
438
  async function registerAppNavigation(rootElement, path, source) {
439
+ ensureBrowserRuntime();
358
440
  const instance = instancesByRoot.get(rootElement);
359
441
  if (!instance) throw new Error("Internal error: app instance missing");
360
442
  routedRoots.add(rootElement);
@@ -370,75 +452,365 @@ function cleanupApp(root) {
370
452
  const rootElement = resolveRootElement(root);
371
453
  if (!rootElement) return;
372
454
  const cleanupFn = rootElement[CLEANUP_SYMBOL];
373
- try {
374
- if (typeof cleanupFn === "function") cleanupFn();
375
- } finally {
376
- const wasRoutedRoot = routedRoots.delete(rootElement);
377
- instancesByRoot.delete(rootElement);
378
- clearRootCleanupCallbacks(rootElement);
379
- if (wasRoutedRoot && routedRoots.size === 0) clearRouteState();
455
+ if (typeof cleanupFn === "function") cleanupFn();
456
+ else clearRootCleanupCallbacks(rootElement);
457
+ }
458
+ /** Check whether an app instance is currently mounted at `root`. */
459
+ function hasApp(root) {
460
+ const rootElement = resolveRootElement(root);
461
+ if (!rootElement) return false;
462
+ return instancesByRoot.has(rootElement);
463
+ }
464
+ //#endregion
465
+ //#region src/boot/islands.ts
466
+ /**
467
+ * createIsland: Enhances existing DOM (no router, mounts once)
468
+ */
469
+ function createIsland(config) {
470
+ assertExecutionModel("islands");
471
+ if (!config || typeof config !== "object") throw new Error("createIsland requires a config object");
472
+ if (typeof config.component !== "function") throw new Error("createIsland: component must be a function");
473
+ validateCspNonce(config.cspNonce);
474
+ const rootElement = resolveRootElement(config.root);
475
+ if (!rootElement) throw new Error(`Root element not found: ${config.root}`);
476
+ if ("routes" in config) throw new Error("createIsland does not accept routes; use createSPA for routed apps");
477
+ if (hasRegisteredRoutes()) throw new Error("Routes are not supported with islands. Use createSPA (client) or createSSR (server) instead.");
478
+ mountOrUpdate(rootElement, config.component, {
479
+ cleanupStrict: config.cleanupStrict,
480
+ cspNonce: config.cspNonce,
481
+ appRuntime: createAppRenderRuntime({ dataRuntime: config.dataRuntime ?? getDefaultDataRuntime() })
482
+ });
483
+ }
484
+ /**
485
+ * createIslands: Enhances one or more existing DOM roots (no router).
486
+ * The only public islands constructor.
487
+ */
488
+ function createIslands(config) {
489
+ assertExecutionModel("islands");
490
+ if (!config || typeof config !== "object") throw new Error("createIslands requires a config object");
491
+ validateCspNonce(config.cspNonce);
492
+ if (!Array.isArray(config.islands) || config.islands.length === 0) throw new Error("createIslands requires a non-empty islands array");
493
+ for (const island of config.islands) createIsland({
494
+ ...island,
495
+ cspNonce: island.cspNonce ?? config.cspNonce
496
+ });
497
+ }
498
+ //#endregion
499
+ //#region src/boot/route-startup.ts
500
+ const MAX_INITIAL_ROUTE_REDIRECTS = 20;
501
+ function bindResolvedRouteHandler(resolved) {
502
+ return () => resolved.handler(resolved.params);
503
+ }
504
+ function createDeniedStatusNode(status) {
505
+ return {
506
+ type: "div",
507
+ props: { "data-route-denied": String(status) },
508
+ children: [String(status)]
509
+ };
510
+ }
511
+ function bindDeniedStatus(status) {
512
+ return () => createDeniedStatusNode(status);
513
+ }
514
+ function bindDeniedRouteHandler(status) {
515
+ return () => createDeniedStatusNode(status);
516
+ }
517
+ async function reconcileInitialRouteMetadata(resolved) {
518
+ if (resolved?.kind !== "render" || !resolved.record) return;
519
+ if (!resolved.record.options.title && !resolved.record.options.meta && !resolved.record.metaChain?.length) return;
520
+ const context = getRouteRenderContext(resolved);
521
+ if (!context) return;
522
+ reconcileRouteMeta(await resolveRouteMeta(resolved.record, context));
523
+ }
524
+ async function resolveInitialRoute(auth, source) {
525
+ let path = typeof window !== "undefined" ? window.location.pathname : "/";
526
+ let href = typeof window !== "undefined" ? `${window.location.pathname}${window.location.search}${window.location.hash}` : path;
527
+ const visited = /* @__PURE__ */ new Set();
528
+ for (let redirects = 0; redirects <= MAX_INITIAL_ROUTE_REDIRECTS; redirects++) {
529
+ if (visited.has(href)) throw new Error(`[Askr] Route redirect cycle detected at ${href}.`);
530
+ visited.add(href);
531
+ if (!source?.registry) throw new TypeError("resolveInitialRoute requires a route registry.");
532
+ const resolved = await resolveRouteRequest(href, {
533
+ registry: source.registry,
534
+ auth,
535
+ load: source.load
536
+ });
537
+ if (typeof window === "undefined" || !resolved || resolved.kind !== "redirect") return {
538
+ path,
539
+ href,
540
+ resolved
541
+ };
542
+ const redirectTarget = new URL(resolved.to, window.location.href);
543
+ const redirectHref = `${redirectTarget.pathname}${redirectTarget.search}${redirectTarget.hash}`;
544
+ window.history.replaceState({ path: redirectHref }, "", redirectHref);
545
+ path = redirectTarget.pathname;
546
+ href = redirectHref;
547
+ }
548
+ throw new Error(`[Askr] Route redirect limit exceeded (${MAX_INITIAL_ROUTE_REDIRECTS}).`);
549
+ }
550
+ //#endregion
551
+ //#region src/boot/spa.ts
552
+ /**
553
+ * App bootstrap and mount
554
+ */
555
+ /**
556
+ * createSPA: Initializes the router and mounts the app with an explicit route registry.
557
+ * ```ts
558
+ * import { createRouteRegistry } from '@askrjs/askr/router';
559
+ * const registry = createRouteRegistry(() => { ... });
560
+ * await createSPA({ root: '#app', registry });
561
+ * ```
562
+ *
563
+ */
564
+ async function createSPA(config) {
565
+ assertExecutionModel("spa");
566
+ if (!config || typeof config !== "object") throw new Error("createSPA requires a config object");
567
+ validateCspNonce(config.cspNonce);
568
+ if (!config.registry) throw new Error("createSPA requires a route registry.");
569
+ const { manifest } = config.registry;
570
+ if (manifest.records.length === 0) throw new Error("createSPA requires a route registry with at least one route. If you are enhancing existing HTML, use createIsland instead.");
571
+ const rootElement = resolveRootElement(config.root);
572
+ if (!rootElement) throw new Error(`Root element not found: ${config.root}`);
573
+ configureScrollRestoration(config.scrollRestoration);
574
+ clearRouteState();
575
+ _applyManifest(manifest);
576
+ const routeAuth = config.auth ?? manifest.auth;
577
+ const appRouteSource = {
578
+ registry: config.registry,
579
+ auth: routeAuth
580
+ };
581
+ _setActiveRouteAuthOptions(routeAuth);
582
+ if (isProductionEnvironment()) lockRouteRegistration();
583
+ const { path, href: currentUrl, resolved } = await resolveInitialRoute(routeAuth, { registry: config.registry });
584
+ setServerLocation(currentUrl);
585
+ const appRuntime = createAppRenderRuntime({
586
+ ...resolved?.kind === "render" && hasRouteRenderData(resolved) ? {
587
+ route: getRouteRenderData(resolved),
588
+ hasRoute: true
589
+ } : {},
590
+ dataRuntime: config.dataRuntime ?? getDefaultDataRuntime(),
591
+ routeRegistry: config.registry,
592
+ routeAuth
593
+ });
594
+ if (!resolved) {
595
+ mountOrUpdate(rootElement, () => ({
596
+ type: "div",
597
+ children: []
598
+ }), {
599
+ cleanupStrict: config.cleanupStrict,
600
+ appRuntime,
601
+ cspNonce: config.cspNonce
602
+ });
603
+ await registerAppNavigation(rootElement, path, {
604
+ ...appRouteSource,
605
+ runtime: appRuntime
606
+ });
607
+ return;
608
+ }
609
+ if (resolved.kind === "redirect") {
610
+ mountOrUpdate(rootElement, () => ({
611
+ type: "div",
612
+ children: []
613
+ }), {
614
+ cleanupStrict: config.cleanupStrict,
615
+ appRuntime,
616
+ cspNonce: config.cspNonce
617
+ });
618
+ await registerAppNavigation(rootElement, path, {
619
+ ...appRouteSource,
620
+ runtime: appRuntime
621
+ });
622
+ return;
623
+ }
624
+ await reconcileInitialRouteMetadata(resolved);
625
+ mountOrUpdate(rootElement, resolved.kind === "deny" ? bindDeniedStatus(resolved.status) : bindResolvedRouteHandler({
626
+ handler: resolved.handler,
627
+ params: resolved.params
628
+ }), {
629
+ cleanupStrict: config.cleanupStrict,
630
+ appRuntime,
631
+ cspNonce: config.cspNonce
632
+ });
633
+ await registerAppNavigation(rootElement, path, {
634
+ ...appRouteSource,
635
+ runtime: appRuntime
636
+ });
637
+ }
638
+ //#endregion
639
+ //#region src/boot/hydration.ts
640
+ const DEFERRED_PAYLOAD = "__askr_deferred__";
641
+ const SSR_STYLE_REGISTRY_ATTR = "data-askr-style-registry";
642
+ function applyDeferredStreamPatches(rootElement) {
643
+ const patches = Array.from(rootElement.querySelectorAll("template[data-askr-deferred-patch]"));
644
+ for (const template of patches) {
645
+ const id = template.getAttribute("data-askr-deferred-patch");
646
+ if (!id) continue;
647
+ const boundary = Array.from(rootElement.querySelectorAll("askr-resolve[data-askr-deferred]")).find((candidate) => candidate.getAttribute("data-askr-deferred") === id);
648
+ if (boundary) boundary.replaceWith(template.content.cloneNode(true));
649
+ template.remove();
650
+ for (const script of Array.from(rootElement.querySelectorAll("script[data-askr-deferred-apply]"))) if (script.getAttribute("data-askr-deferred-apply") === id) script.remove();
651
+ }
652
+ }
653
+ function hydrationReviver(_key, value) {
654
+ if (!value || typeof value !== "object") return value;
655
+ const payload = value;
656
+ if (payload[DEFERRED_PAYLOAD] === "fulfilled") return reviveDeferredValue("fulfilled", payload.value, void 0);
657
+ if (payload[DEFERRED_PAYLOAD] === "rejected") return reviveDeferredValue("rejected", void 0, payload.error);
658
+ return value;
659
+ }
660
+ /**
661
+ * Relocate the SSR style registry carrier out of the hydration root.
662
+ *
663
+ * `@askrjs/server` prepends the collected styles to the page body, so they land
664
+ * as a direct child of the mount root. That extra element makes the root's
665
+ * child list disagree with the rendered tree, which costs the app in-place
666
+ * adoption and discards the carried CSS during reconciliation. The carrier is
667
+ * transport rather than application markup, so move it into `<head>`, where it
668
+ * still applies and no longer participates in reconciliation.
669
+ *
670
+ * Deferred style patches resolve the carrier with a document-wide query, so
671
+ * they keep working from its new position.
672
+ */
673
+ function adoptSsrStyleCarriers(rootElement) {
674
+ const head = document.head;
675
+ if (!head) return;
676
+ for (const child of Array.from(rootElement.children)) if (child instanceof HTMLStyleElement && child.hasAttribute(SSR_STYLE_REGISTRY_ATTR)) head.append(child);
677
+ }
678
+ function takeHydrationRenderData(rootElement) {
679
+ for (const child of Array.from(rootElement.children)) if (child instanceof HTMLScriptElement && child.getAttribute("data-askr-render-data") === "true") {
680
+ const raw = child.textContent ?? "";
681
+ child.remove();
682
+ if (!raw) return pageRenderEnvelope(void 0);
380
683
  try {
381
- delete rootElement[CLEANUP_SYMBOL];
684
+ return pageRenderEnvelope(JSON.parse(raw, hydrationReviver));
685
+ } catch (err) {
686
+ const error = /* @__PURE__ */ new Error("[Askr] Failed to parse embedded SSR render data during hydration.");
687
+ error.cause = err;
688
+ throw error;
689
+ }
690
+ }
691
+ return null;
692
+ }
693
+ function markSkippedElements(root, skipSelectors) {
694
+ if (skipSelectors.length === 0) return;
695
+ const selectorList = Array.from(new Set(skipSelectors)).join(", ");
696
+ root.querySelectorAll(selectorList).forEach((el) => el.setAttribute("data-skip-hydrate", "true"));
697
+ }
698
+ function collectDeferredBelowFoldBoundaries(root, foldY) {
699
+ const boundaries = [];
700
+ const stack = [];
701
+ for (let index = root.children.length - 1; index >= 0; index -= 1) stack.push(root.children[index]);
702
+ while (stack.length > 0) {
703
+ const element = stack.pop();
704
+ if (element.hasAttribute("data-skip-hydrate")) continue;
705
+ if (element.getBoundingClientRect().top >= foldY) {
706
+ element.setAttribute("data-skip-hydrate", "true");
707
+ registerDeferredHydrationBoundary(root, element);
708
+ boundaries.push(element);
709
+ continue;
710
+ }
711
+ for (let index = element.children.length - 1; index >= 0; index -= 1) stack.push(element.children[index]);
712
+ }
713
+ return boundaries;
714
+ }
715
+ function activateVisibleDeferredBoundaries(root, boundaries, foldY, activate) {
716
+ let activated = false;
717
+ let remaining = 0;
718
+ for (const element of boundaries) {
719
+ if (!element.hasAttribute("data-skip-hydrate")) continue;
720
+ if (element.getBoundingClientRect().top < foldY) try {
721
+ if (activate(root, element)) activated = true;
382
722
  } catch {}
723
+ if (element.hasAttribute("data-skip-hydrate")) remaining += 1;
383
724
  }
384
- }
385
- /** Check whether an app instance is currently mounted at `root`. */
386
- function hasApp(root) {
387
- const rootElement = resolveRootElement(root);
388
- if (!rootElement) return false;
389
- return instancesByRoot.has(rootElement);
390
- }
391
- //#endregion
392
- //#region src/boot/route-startup.ts
393
- const MAX_INITIAL_ROUTE_REDIRECTS = 20;
394
- function bindResolvedRouteHandler(resolved) {
395
- return () => resolved.handler(resolved.params);
396
- }
397
- function createDeniedStatusNode(status) {
398
725
  return {
399
- type: "div",
400
- props: { "data-route-denied": String(status) },
401
- children: [String(status)]
726
+ activated,
727
+ remaining
402
728
  };
403
729
  }
404
- function bindDeniedStatus(status) {
405
- return () => createDeniedStatusNode(status);
406
- }
407
- function bindDeniedRouteHandler(status) {
408
- return () => createDeniedStatusNode(status);
730
+ function queueIdleWork(work) {
731
+ return new Promise((resolve) => {
732
+ if (typeof requestIdleCallback !== "undefined") {
733
+ requestIdleCallback(() => {
734
+ work();
735
+ resolve();
736
+ }, { timeout: 2e3 });
737
+ return;
738
+ }
739
+ setTimeout(() => {
740
+ work();
741
+ resolve();
742
+ }, 0);
743
+ });
409
744
  }
410
- async function reconcileInitialRouteMetadata(resolved) {
411
- if (resolved?.kind !== "render" || !resolved.record) return;
412
- if (!resolved.record.options.title && !resolved.record.options.meta && !resolved.record.metaChain?.length) return;
413
- const context = getRouteRenderContext(resolved);
414
- if (!context) return;
415
- reconcileRouteMeta(await resolveRouteMeta(resolved.record, context));
745
+ function shouldVerifyHydrationMarkup(config) {
746
+ const explicit = config.hydrate?.verifyMarkup;
747
+ if (typeof explicit === "boolean") return explicit;
748
+ return !isProductionEnvironment();
416
749
  }
417
- async function resolveInitialRoute(auth, source) {
418
- let path = typeof window !== "undefined" ? window.location.pathname : "/";
419
- let href = typeof window !== "undefined" ? `${window.location.pathname}${window.location.search}${window.location.hash}` : path;
420
- const visited = /* @__PURE__ */ new Set();
421
- for (let redirects = 0; redirects <= MAX_INITIAL_ROUTE_REDIRECTS; redirects++) {
422
- if (visited.has(href)) throw new Error(`[Askr] Route redirect cycle detected at ${href}.`);
423
- visited.add(href);
424
- if (!source?.registry) throw new TypeError("resolveInitialRoute requires a route registry.");
425
- const resolved = await resolveRouteRequest(href, {
426
- registry: source.registry,
427
- auth,
428
- load: source.load
429
- });
430
- if (typeof window === "undefined" || !resolved || resolved.kind !== "redirect") return {
431
- path,
432
- href,
433
- resolved
750
+ async function applySelectiveHydration(rootElement, resolved, path, cleanupStrict, hydrateOptions, source, hooks, interactionReplay) {
751
+ const hasBelowFoldDeferral = !!hydrateOptions.deferBelowFold;
752
+ let staticChildSlotsCacheSuspended = false;
753
+ let releaseSelectiveHydrationResources = () => {};
754
+ const restoreStaticChildSlotsCache = () => {
755
+ if (!staticChildSlotsCacheSuspended) return;
756
+ setStaticChildSlotsCacheEnabled(true);
757
+ staticChildSlotsCacheSuspended = false;
758
+ };
759
+ if (hydrateOptions.skipSelectors?.length) markSkippedElements(rootElement, hydrateOptions.skipSelectors);
760
+ let deferredBoundaries = [];
761
+ if (hydrateOptions.deferBelowFold) {
762
+ setStaticChildSlotsCacheEnabled(false);
763
+ staticChildSlotsCacheSuspended = true;
764
+ const foldY = hydrateOptions.foldThreshold ?? window.innerHeight;
765
+ deferredBoundaries = collectDeferredBelowFoldBoundaries(rootElement, foldY);
766
+ interactionReplay.registerDeferredBoundaries(deferredBoundaries);
767
+ let selectiveHydrationResourcesReleased = false;
768
+ let unregisterRootCleanupCallback = () => {};
769
+ function handleScroll() {
770
+ const { activated, remaining } = activateVisibleDeferredBoundaries(rootElement, deferredBoundaries, foldY, hooks.activateHydrationBoundary);
771
+ if (!activated) return;
772
+ if (remaining === 0) releaseSelectiveHydrationResources();
773
+ }
774
+ releaseSelectiveHydrationResources = () => {
775
+ if (selectiveHydrationResourcesReleased) return;
776
+ selectiveHydrationResourcesReleased = true;
777
+ unregisterRootCleanupCallback();
778
+ window.removeEventListener("scroll", handleScroll);
779
+ restoreStaticChildSlotsCache();
780
+ interactionReplay.clearDeferredBoundaries();
434
781
  };
435
- const redirectTarget = new URL(resolved.to, window.location.href);
436
- const redirectHref = `${redirectTarget.pathname}${redirectTarget.search}${redirectTarget.hash}`;
437
- window.history.replaceState({ path: redirectHref }, "", redirectHref);
438
- path = redirectTarget.pathname;
439
- href = redirectHref;
782
+ interactionReplay.setOnDeferredBoundariesDrained(releaseSelectiveHydrationResources);
783
+ unregisterRootCleanupCallback = hooks.registerRootCleanupCallback(rootElement, releaseSelectiveHydrationResources);
784
+ window.addEventListener("scroll", handleScroll, { passive: true });
440
785
  }
441
- throw new Error(`[Askr] Route redirect limit exceeded (${MAX_INITIAL_ROUTE_REDIRECTS}).`);
786
+ if (hydrateOptions.deferUntilIdle && !hasBelowFoldDeferral) {
787
+ await queueIdleWork(() => {
788
+ hooks.mountOrUpdate(rootElement, (() => resolved.handler(resolved.params)), {
789
+ cleanupStrict,
790
+ appRuntime: source?.runtime
791
+ });
792
+ });
793
+ await hooks.registerAppNavigation(rootElement, path, source);
794
+ return;
795
+ }
796
+ try {
797
+ hooks.mountOrUpdate(rootElement, (() => resolved.handler(resolved.params)), {
798
+ cleanupStrict,
799
+ appRuntime: source?.runtime
800
+ });
801
+ await hooks.registerAppNavigation(rootElement, path, source);
802
+ } catch (error) {
803
+ releaseSelectiveHydrationResources();
804
+ throw error;
805
+ }
806
+ if (hydrateOptions.deferUntilIdle && deferredBoundaries.length > 0) await queueIdleWork(() => {
807
+ try {
808
+ activateVisibleDeferredBoundaries(rootElement, deferredBoundaries, Number.POSITIVE_INFINITY, hooks.activateHydrationBoundary);
809
+ } finally {
810
+ releaseSelectiveHydrationResources();
811
+ }
812
+ });
813
+ if (deferredBoundaries.length === 0) releaseSelectiveHydrationResources();
442
814
  }
443
815
  //#endregion
444
816
  //#region src/boot/hydration-interaction-replay.ts
@@ -610,126 +982,11 @@ function beginHydrationInteractionReplay(root, activateBoundary, permanentSkipSe
610
982
  };
611
983
  }
612
984
  //#endregion
613
- //#region src/boot/index.ts
985
+ //#region src/boot/hydrate-spa.ts
614
986
  /**
615
987
  * App bootstrap and mount
616
988
  */
617
989
  /**
618
- * createIsland: Enhances existing DOM (no router, mounts once)
619
- */
620
- function createIsland(config) {
621
- assertExecutionModel("islands");
622
- if (!config || typeof config !== "object") throw new Error("createIsland requires a config object");
623
- if (typeof config.component !== "function") throw new Error("createIsland: component must be a function");
624
- validateCspNonce(config.cspNonce);
625
- const rootElement = resolveRootElement(config.root);
626
- if (!rootElement) throw new Error(`Root element not found: ${config.root}`);
627
- if ("routes" in config) throw new Error("createIsland does not accept routes; use createSPA for routed apps");
628
- if (hasRegisteredRoutes()) throw new Error("Routes are not supported with islands. Use createSPA (client) or createSSR (server) instead.");
629
- mountOrUpdate(rootElement, config.component, {
630
- cleanupStrict: config.cleanupStrict,
631
- cspNonce: config.cspNonce,
632
- appRuntime: createAppRenderRuntime({ dataRuntime: config.dataRuntime ?? getDefaultDataRuntime() })
633
- });
634
- }
635
- /**
636
- * createIslands: Enhances one or more existing DOM roots (no router).
637
- * The only public islands constructor.
638
- */
639
- function createIslands(config) {
640
- assertExecutionModel("islands");
641
- if (!config || typeof config !== "object") throw new Error("createIslands requires a config object");
642
- validateCspNonce(config.cspNonce);
643
- if (!Array.isArray(config.islands) || config.islands.length === 0) throw new Error("createIslands requires a non-empty islands array");
644
- for (const island of config.islands) createIsland({
645
- ...island,
646
- cspNonce: island.cspNonce ?? config.cspNonce
647
- });
648
- }
649
- /**
650
- * createSPA: Initializes the router and mounts the app with an explicit route registry.
651
- * ```ts
652
- * import { createRouteRegistry } from '@askrjs/askr/router';
653
- * const registry = createRouteRegistry(() => { ... });
654
- * await createSPA({ root: '#app', registry });
655
- * ```
656
- *
657
- */
658
- async function createSPA(config) {
659
- assertExecutionModel("spa");
660
- if (!config || typeof config !== "object") throw new Error("createSPA requires a config object");
661
- validateCspNonce(config.cspNonce);
662
- if (!config.registry) throw new Error("createSPA requires a route registry.");
663
- const { manifest } = config.registry;
664
- if (manifest.records.length === 0) throw new Error("createSPA requires a route registry with at least one route. If you are enhancing existing HTML, use createIsland instead.");
665
- const rootElement = resolveRootElement(config.root);
666
- if (!rootElement) throw new Error(`Root element not found: ${config.root}`);
667
- configureScrollRestoration(config.scrollRestoration);
668
- clearRouteState();
669
- _applyManifest(manifest);
670
- const routeAuth = config.auth ?? manifest.auth;
671
- const appRouteSource = {
672
- registry: config.registry,
673
- auth: routeAuth
674
- };
675
- _setActiveRouteAuthOptions(routeAuth);
676
- if (isProductionEnvironment()) lockRouteRegistration();
677
- const { path, href: currentUrl, resolved } = await resolveInitialRoute(routeAuth, { registry: config.registry });
678
- setServerLocation(currentUrl);
679
- const appRuntime = createAppRenderRuntime({
680
- ...resolved?.kind === "render" && hasRouteRenderData(resolved) ? {
681
- route: getRouteRenderData(resolved),
682
- hasRoute: true
683
- } : {},
684
- dataRuntime: config.dataRuntime ?? getDefaultDataRuntime(),
685
- routeRegistry: config.registry,
686
- routeAuth
687
- });
688
- if (!resolved) {
689
- mountOrUpdate(rootElement, () => ({
690
- type: "div",
691
- children: []
692
- }), {
693
- cleanupStrict: config.cleanupStrict,
694
- appRuntime,
695
- cspNonce: config.cspNonce
696
- });
697
- await registerAppNavigation(rootElement, path, {
698
- ...appRouteSource,
699
- runtime: appRuntime
700
- });
701
- return;
702
- }
703
- if (resolved.kind === "redirect") {
704
- mountOrUpdate(rootElement, () => ({
705
- type: "div",
706
- children: []
707
- }), {
708
- cleanupStrict: config.cleanupStrict,
709
- appRuntime,
710
- cspNonce: config.cspNonce
711
- });
712
- await registerAppNavigation(rootElement, path, {
713
- ...appRouteSource,
714
- runtime: appRuntime
715
- });
716
- return;
717
- }
718
- await reconcileInitialRouteMetadata(resolved);
719
- mountOrUpdate(rootElement, resolved.kind === "deny" ? bindDeniedStatus(resolved.status) : bindResolvedRouteHandler({
720
- handler: resolved.handler,
721
- params: resolved.params
722
- }), {
723
- cleanupStrict: config.cleanupStrict,
724
- appRuntime,
725
- cspNonce: config.cspNonce
726
- });
727
- await registerAppNavigation(rootElement, path, {
728
- ...appRouteSource,
729
- runtime: appRuntime
730
- });
731
- }
732
- /**
733
990
  * hydrateSPA: Hydrate server-rendered HTML with an explicit route registry.
734
991
  */
735
992
  async function hydrateSPA(config) {
@@ -744,6 +1001,7 @@ async function hydrateSPA(config) {
744
1001
  const interactionReplay = beginHydrationInteractionReplay(rootElement, (boundary) => activateHydrationBoundary(rootElement, boundary), config.hydrate?.skipSelectors);
745
1002
  try {
746
1003
  applyDeferredStreamPatches(rootElement);
1004
+ adoptSsrStyleCarriers(rootElement);
747
1005
  const hydrationRenderData = takeHydrationRenderData(rootElement);
748
1006
  const hydrationQueryCache = hydrationRenderData?.resources;
749
1007
  if (hydrationQueryCache) hydrateDataRuntime(config.dataRuntime ?? getDefaultDataRuntime(), hydrationQueryCache);
@@ -776,16 +1034,13 @@ async function hydrateSPA(config) {
776
1034
  const hydrationResolved = resolved.kind === "deny" ? {
777
1035
  handler: bindDeniedRouteHandler(resolved.status),
778
1036
  params: {}
779
- } : {
780
- handler: resolved.handler,
781
- params: resolved.params
782
- };
1037
+ } : resolved;
783
1038
  const mountHydratedRoot = (...args) => withIntrinsicHydrationAdoption(() => mountOrUpdate(args[0], args[1], {
784
1039
  ...args[2],
785
1040
  cspNonce: config.cspNonce
786
1041
  }));
787
1042
  if (shouldVerifyHydrationMarkup(config)) {
788
- const { verifyHydrationSyncForUrl } = await import("./verify-hydration-BZPJ_kYa.js");
1043
+ const { verifyHydrationSyncForUrl } = await import("./verify-hydration-ikaxotf7.js");
789
1044
  if (!verifyHydrationSyncForUrl({
790
1045
  root: rootElement,
791
1046
  url: currentUrl,
@@ -842,4 +1097,8 @@ async function hydrateSPA(config) {
842
1097
  }
843
1098
  }
844
1099
  //#endregion
845
- export { cleanupApp as a, hydrateSPA as i, createIslands as n, hasApp as o, createSPA as r, createIsland as t };
1100
+ //#region src/boot/index.ts
1101
+ /** Stable bootstrap exports; each mounting mode owns its orchestration. */
1102
+ composeBrowserRuntime();
1103
+ //#endregion
1104
+ export { cleanupApp as a, createIslands as i, createSPA as n, hasApp as o, createIsland as r, installRendererBridge as s, hydrateSPA as t };