@askrjs/askr 0.2.3 → 0.3.0

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 +11 -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-ZxJXI9hF.js → activity-oy2YFTFF.js} +22 -16
  6. package/dist/benchmark.js +8 -9
  7. package/dist/boot/index.d.ts +21 -9
  8. package/dist/boot/index.js +1 -1
  9. package/dist/{boot-D4GaLA0d.js → boot-BMBMWUGI.js} +680 -440
  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-57x3wa4J.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-B7XCDeC8.js → data-2edW0jhF.js} +133 -138
  22. package/dist/{data-runtime-7t_Y0iv3.js → data-runtime-CxoY98lX.js} +22 -15
  23. package/dist/{deferred-BELyvMsC.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-4LwBwV58.js → effect-C3Zsc1k6.js} +14 -41
  41. package/dist/{types-DkVJhVs9.d.ts → elements.d.ts} +84 -42
  42. package/dist/{for-internal-DeoSg_AV.js → for-state-BDykeOJL.js} +623 -511
  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-Dl02WQKS.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-C46GtScG.js → navigate-prC5V8XV.js} +204 -476
  69. package/dist/{navigate-wel1e6cU.d.ts → navigation.d.ts} +10 -8
  70. package/dist/{readable-CEcteIDE.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-BaC5qHEw.js → render-resolved-BC0W7_5b.js} +165 -558
  74. package/dist/{renderer-BhXofszH.js → renderer-CT0HeY58.js} +6866 -5715
  75. package/dist/{resolution-t8J0yNaU.js → resolution-3DskUkjG.js} +189 -180
  76. package/dist/{resource-operation-B8q-QRUx.js → resource-18-sFRl9.js} +98 -58
  77. package/dist/resources/index.d.ts +64 -46
  78. package/dist/resources/index.js +215 -9
  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-C7rIpOnN.js → selector-BSPVkjKP.js} +41 -41
  86. package/dist/{shared-CGrx2AOL.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-BzhOW2TX.js → state-BXJfudq1.js} +10 -15
  95. package/dist/{structures-BwfeO4Iz.js → structures-CjEzfj4h.js} +37 -57
  96. package/dist/{index-r5THqzPN.d.ts → structures.d.ts} +57 -36
  97. package/dist/testing/index.d.ts +125 -24
  98. package/dist/testing/index.js +26 -62
  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-_Q6avFp0.js → verify-hydration-ikaxotf7.js} +8 -5
  102. package/package.json +13 -15
  103. package/dist/access-Rr7CMa06.js +0 -507
  104. package/dist/activity-CAJ5eho-.d.ts +0 -14
  105. package/dist/benchmark.d.ts +0 -26
  106. package/dist/cleanup-DsrbV-rr.js +0 -535
  107. package/dist/component-cleanup-DjpwO9WX.js +0 -143
  108. package/dist/component-scope-4I3yEMkb.js +0 -195
  109. package/dist/control-DdFw-Caz.d.ts +0 -8
  110. package/dist/csp-nonce-C9OuML0U.js +0 -72
  111. package/dist/css-BuPaTOfe.js +0 -70
  112. package/dist/fastlane-Buz1Rvlr.js +0 -166
  113. package/dist/index-DSqXWc77.d.ts +0 -1402
  114. package/dist/jsx-runtime-BTzDdBJ_.d.ts +0 -2
  115. package/dist/jsx-runtime-Cdd6Gkb0.d.ts +0 -38
  116. package/dist/lifecycle-batch-Be-CLDjV.js +0 -253
  117. package/dist/lifecycle-operations-C5m03NQc.js +0 -195
@@ -1,177 +1,317 @@
1
- import { S as isProductionEnvironment, r as flushRuntimeScheduler, x as isDevelopmentEnvironment, y as logger } from "./access-Rr7CMa06.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-Dl02WQKS.js";
4
- import { t as cleanupComponent } from "./component-cleanup-DjpwO9WX.js";
5
- import { p as isPromiseLike } from "./lifecycle-batch-Be-CLDjV.js";
6
- import { _ as teardownNodeSubtree, d as removeAllListeners } from "./cleanup-DsrbV-rr.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-BhXofszH.js";
10
- import { r as validateCspNonce, t as CspNonceScope } from "./csp-nonce-C9OuML0U.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-t8J0yNaU.js";
15
- import { o as createAppRenderRuntime, r as setServerLocation } from "./activity-ZxJXI9hF.js";
16
- import { f as registerAppInstance, g as unregisterAppInstance, i as resolveRouteMeta, o as configureScrollRestoration, r as reconcileRouteMeta, t as initializeNavigation } from "./navigate-C46GtScG.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,346 @@ 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
+ function applyDeferredStreamPatches(rootElement) {
642
+ const patches = Array.from(rootElement.querySelectorAll("template[data-askr-deferred-patch]"));
643
+ for (const template of patches) {
644
+ const id = template.getAttribute("data-askr-deferred-patch");
645
+ if (!id) continue;
646
+ const boundary = Array.from(rootElement.querySelectorAll("askr-resolve[data-askr-deferred]")).find((candidate) => candidate.getAttribute("data-askr-deferred") === id);
647
+ if (boundary) boundary.replaceWith(template.content.cloneNode(true));
648
+ template.remove();
649
+ for (const script of Array.from(rootElement.querySelectorAll("script[data-askr-deferred-apply]"))) if (script.getAttribute("data-askr-deferred-apply") === id) script.remove();
650
+ }
651
+ }
652
+ function hydrationReviver(_key, value) {
653
+ if (!value || typeof value !== "object") return value;
654
+ const payload = value;
655
+ if (payload[DEFERRED_PAYLOAD] === "fulfilled") return reviveDeferredValue("fulfilled", payload.value, void 0);
656
+ if (payload[DEFERRED_PAYLOAD] === "rejected") return reviveDeferredValue("rejected", void 0, payload.error);
657
+ return value;
658
+ }
659
+ function takeHydrationRenderData(rootElement) {
660
+ for (const child of Array.from(rootElement.children)) if (child instanceof HTMLScriptElement && child.getAttribute("data-askr-render-data") === "true") {
661
+ const raw = child.textContent ?? "";
662
+ child.remove();
663
+ if (!raw) return pageRenderEnvelope(void 0);
380
664
  try {
381
- delete rootElement[CLEANUP_SYMBOL];
665
+ return pageRenderEnvelope(JSON.parse(raw, hydrationReviver));
666
+ } catch (err) {
667
+ const error = /* @__PURE__ */ new Error("[Askr] Failed to parse embedded SSR render data during hydration.");
668
+ error.cause = err;
669
+ throw error;
670
+ }
671
+ }
672
+ return null;
673
+ }
674
+ function markSkippedElements(root, skipSelectors) {
675
+ if (skipSelectors.length === 0) return;
676
+ const selectorList = Array.from(new Set(skipSelectors)).join(", ");
677
+ root.querySelectorAll(selectorList).forEach((el) => el.setAttribute("data-skip-hydrate", "true"));
678
+ }
679
+ function collectDeferredBelowFoldBoundaries(root, foldY) {
680
+ const boundaries = [];
681
+ const stack = [];
682
+ for (let index = root.children.length - 1; index >= 0; index -= 1) stack.push(root.children[index]);
683
+ while (stack.length > 0) {
684
+ const element = stack.pop();
685
+ if (element.hasAttribute("data-skip-hydrate")) continue;
686
+ if (element.getBoundingClientRect().top >= foldY) {
687
+ element.setAttribute("data-skip-hydrate", "true");
688
+ registerDeferredHydrationBoundary(root, element);
689
+ boundaries.push(element);
690
+ continue;
691
+ }
692
+ for (let index = element.children.length - 1; index >= 0; index -= 1) stack.push(element.children[index]);
693
+ }
694
+ return boundaries;
695
+ }
696
+ function activateVisibleDeferredBoundaries(root, boundaries, foldY, activate) {
697
+ let activated = false;
698
+ let remaining = 0;
699
+ for (const element of boundaries) {
700
+ if (!element.hasAttribute("data-skip-hydrate")) continue;
701
+ if (element.getBoundingClientRect().top < foldY) try {
702
+ if (activate(root, element)) activated = true;
382
703
  } catch {}
704
+ if (element.hasAttribute("data-skip-hydrate")) remaining += 1;
383
705
  }
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
706
  return {
399
- type: "div",
400
- props: { "data-route-denied": String(status) },
401
- children: [String(status)]
707
+ activated,
708
+ remaining
402
709
  };
403
710
  }
404
- function bindDeniedStatus(status) {
405
- return () => createDeniedStatusNode(status);
406
- }
407
- function bindDeniedRouteHandler(status) {
408
- return () => createDeniedStatusNode(status);
711
+ function queueIdleWork(work) {
712
+ return new Promise((resolve) => {
713
+ if (typeof requestIdleCallback !== "undefined") {
714
+ requestIdleCallback(() => {
715
+ work();
716
+ resolve();
717
+ }, { timeout: 2e3 });
718
+ return;
719
+ }
720
+ setTimeout(() => {
721
+ work();
722
+ resolve();
723
+ }, 0);
724
+ });
409
725
  }
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));
726
+ function shouldVerifyHydrationMarkup(config) {
727
+ const explicit = config.hydrate?.verifyMarkup;
728
+ if (typeof explicit === "boolean") return explicit;
729
+ return !isProductionEnvironment();
416
730
  }
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
731
+ async function applySelectiveHydration(rootElement, resolved, path, cleanupStrict, hydrateOptions, source, hooks, interactionReplay) {
732
+ const hasBelowFoldDeferral = !!hydrateOptions.deferBelowFold;
733
+ let staticChildSlotsCacheSuspended = false;
734
+ let releaseSelectiveHydrationResources = () => {};
735
+ const restoreStaticChildSlotsCache = () => {
736
+ if (!staticChildSlotsCacheSuspended) return;
737
+ setStaticChildSlotsCacheEnabled(true);
738
+ staticChildSlotsCacheSuspended = false;
739
+ };
740
+ if (hydrateOptions.skipSelectors?.length) markSkippedElements(rootElement, hydrateOptions.skipSelectors);
741
+ let deferredBoundaries = [];
742
+ if (hydrateOptions.deferBelowFold) {
743
+ setStaticChildSlotsCacheEnabled(false);
744
+ staticChildSlotsCacheSuspended = true;
745
+ const foldY = hydrateOptions.foldThreshold ?? window.innerHeight;
746
+ deferredBoundaries = collectDeferredBelowFoldBoundaries(rootElement, foldY);
747
+ interactionReplay.registerDeferredBoundaries(deferredBoundaries);
748
+ let selectiveHydrationResourcesReleased = false;
749
+ let unregisterRootCleanupCallback = () => {};
750
+ function handleScroll() {
751
+ const { activated, remaining } = activateVisibleDeferredBoundaries(rootElement, deferredBoundaries, foldY, hooks.activateHydrationBoundary);
752
+ if (!activated) return;
753
+ if (remaining === 0) releaseSelectiveHydrationResources();
754
+ }
755
+ releaseSelectiveHydrationResources = () => {
756
+ if (selectiveHydrationResourcesReleased) return;
757
+ selectiveHydrationResourcesReleased = true;
758
+ unregisterRootCleanupCallback();
759
+ window.removeEventListener("scroll", handleScroll);
760
+ restoreStaticChildSlotsCache();
761
+ interactionReplay.clearDeferredBoundaries();
434
762
  };
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;
763
+ interactionReplay.setOnDeferredBoundariesDrained(releaseSelectiveHydrationResources);
764
+ unregisterRootCleanupCallback = hooks.registerRootCleanupCallback(rootElement, releaseSelectiveHydrationResources);
765
+ window.addEventListener("scroll", handleScroll, { passive: true });
440
766
  }
441
- throw new Error(`[Askr] Route redirect limit exceeded (${MAX_INITIAL_ROUTE_REDIRECTS}).`);
767
+ if (hydrateOptions.deferUntilIdle && !hasBelowFoldDeferral) {
768
+ await queueIdleWork(() => {
769
+ hooks.mountOrUpdate(rootElement, (() => resolved.handler(resolved.params)), {
770
+ cleanupStrict,
771
+ appRuntime: source?.runtime
772
+ });
773
+ });
774
+ await hooks.registerAppNavigation(rootElement, path, source);
775
+ return;
776
+ }
777
+ try {
778
+ hooks.mountOrUpdate(rootElement, (() => resolved.handler(resolved.params)), {
779
+ cleanupStrict,
780
+ appRuntime: source?.runtime
781
+ });
782
+ await hooks.registerAppNavigation(rootElement, path, source);
783
+ } catch (error) {
784
+ releaseSelectiveHydrationResources();
785
+ throw error;
786
+ }
787
+ if (hydrateOptions.deferUntilIdle && deferredBoundaries.length > 0) await queueIdleWork(() => {
788
+ try {
789
+ activateVisibleDeferredBoundaries(rootElement, deferredBoundaries, Number.POSITIVE_INFINITY, hooks.activateHydrationBoundary);
790
+ } finally {
791
+ releaseSelectiveHydrationResources();
792
+ }
793
+ });
794
+ if (deferredBoundaries.length === 0) releaseSelectiveHydrationResources();
442
795
  }
443
796
  //#endregion
444
797
  //#region src/boot/hydration-interaction-replay.ts
@@ -610,125 +963,11 @@ function beginHydrationInteractionReplay(root, activateBoundary, permanentSkipSe
610
963
  };
611
964
  }
612
965
  //#endregion
613
- //#region src/boot/index.ts
966
+ //#region src/boot/hydrate-spa.ts
614
967
  /**
615
968
  * App bootstrap and mount
616
969
  */
617
970
  /**
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
- });
633
- }
634
- /**
635
- * createIslands: Enhances one or more existing DOM roots (no router).
636
- * The only public islands constructor.
637
- */
638
- function createIslands(config) {
639
- assertExecutionModel("islands");
640
- if (!config || typeof config !== "object") throw new Error("createIslands requires a config object");
641
- validateCspNonce(config.cspNonce);
642
- if (!Array.isArray(config.islands) || config.islands.length === 0) throw new Error("createIslands requires a non-empty islands array");
643
- for (const island of config.islands) createIsland({
644
- ...island,
645
- cspNonce: island.cspNonce ?? config.cspNonce
646
- });
647
- }
648
- /**
649
- * createSPA: Initializes the router and mounts the app with an explicit route registry.
650
- * ```ts
651
- * import { createRouteRegistry } from '@askrjs/askr/router';
652
- * const registry = createRouteRegistry(() => { ... });
653
- * await createSPA({ root: '#app', registry });
654
- * ```
655
- *
656
- */
657
- async function createSPA(config) {
658
- assertExecutionModel("spa");
659
- if (!config || typeof config !== "object") throw new Error("createSPA requires a config object");
660
- validateCspNonce(config.cspNonce);
661
- if (!config.registry) throw new Error("createSPA requires a route registry.");
662
- const { manifest } = config.registry;
663
- 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.");
664
- const rootElement = resolveRootElement(config.root);
665
- if (!rootElement) throw new Error(`Root element not found: ${config.root}`);
666
- configureScrollRestoration(config.scrollRestoration);
667
- clearRouteState();
668
- _applyManifest(manifest);
669
- const routeAuth = config.auth ?? manifest.auth;
670
- const appRouteSource = {
671
- registry: config.registry,
672
- auth: routeAuth
673
- };
674
- _setActiveRouteAuthOptions(routeAuth);
675
- if (isProductionEnvironment()) lockRouteRegistration();
676
- const { path, href: currentUrl, resolved } = await resolveInitialRoute(routeAuth, { registry: config.registry });
677
- setServerLocation(currentUrl);
678
- const appRuntime = createAppRenderRuntime({
679
- ...resolved?.kind === "render" && hasRouteRenderData(resolved) ? {
680
- route: getRouteRenderData(resolved),
681
- hasRoute: true
682
- } : {},
683
- dataRuntime: config.dataRuntime ?? getDefaultDataRuntime(),
684
- routeRegistry: config.registry,
685
- routeAuth
686
- });
687
- if (!resolved) {
688
- mountOrUpdate(rootElement, () => ({
689
- type: "div",
690
- children: []
691
- }), {
692
- cleanupStrict: config.cleanupStrict,
693
- appRuntime,
694
- cspNonce: config.cspNonce
695
- });
696
- await registerAppNavigation(rootElement, path, {
697
- ...appRouteSource,
698
- runtime: appRuntime
699
- });
700
- return;
701
- }
702
- if (resolved.kind === "redirect") {
703
- mountOrUpdate(rootElement, () => ({
704
- type: "div",
705
- children: []
706
- }), {
707
- cleanupStrict: config.cleanupStrict,
708
- appRuntime,
709
- cspNonce: config.cspNonce
710
- });
711
- await registerAppNavigation(rootElement, path, {
712
- ...appRouteSource,
713
- runtime: appRuntime
714
- });
715
- return;
716
- }
717
- await reconcileInitialRouteMetadata(resolved);
718
- mountOrUpdate(rootElement, resolved.kind === "deny" ? bindDeniedStatus(resolved.status) : bindResolvedRouteHandler({
719
- handler: resolved.handler,
720
- params: resolved.params
721
- }), {
722
- cleanupStrict: config.cleanupStrict,
723
- appRuntime,
724
- cspNonce: config.cspNonce
725
- });
726
- await registerAppNavigation(rootElement, path, {
727
- ...appRouteSource,
728
- runtime: appRuntime
729
- });
730
- }
731
- /**
732
971
  * hydrateSPA: Hydrate server-rendered HTML with an explicit route registry.
733
972
  */
734
973
  async function hydrateSPA(config) {
@@ -775,16 +1014,13 @@ async function hydrateSPA(config) {
775
1014
  const hydrationResolved = resolved.kind === "deny" ? {
776
1015
  handler: bindDeniedRouteHandler(resolved.status),
777
1016
  params: {}
778
- } : {
779
- handler: resolved.handler,
780
- params: resolved.params
781
- };
1017
+ } : resolved;
782
1018
  const mountHydratedRoot = (...args) => withIntrinsicHydrationAdoption(() => mountOrUpdate(args[0], args[1], {
783
1019
  ...args[2],
784
1020
  cspNonce: config.cspNonce
785
1021
  }));
786
1022
  if (shouldVerifyHydrationMarkup(config)) {
787
- const { verifyHydrationSyncForUrl } = await import("./verify-hydration-_Q6avFp0.js");
1023
+ const { verifyHydrationSyncForUrl } = await import("./verify-hydration-ikaxotf7.js");
788
1024
  if (!verifyHydrationSyncForUrl({
789
1025
  root: rootElement,
790
1026
  url: currentUrl,
@@ -841,4 +1077,8 @@ async function hydrateSPA(config) {
841
1077
  }
842
1078
  }
843
1079
  //#endregion
844
- export { cleanupApp as a, hydrateSPA as i, createIslands as n, hasApp as o, createSPA as r, createIsland as t };
1080
+ //#region src/boot/index.ts
1081
+ /** Stable bootstrap exports; each mounting mode owns its orchestration. */
1082
+ composeBrowserRuntime();
1083
+ //#endregion
1084
+ export { cleanupApp as a, createIslands as i, createSPA as n, hasApp as o, createIsland as r, installRendererBridge as s, hydrateSPA as t };