@askrjs/askr 0.3.0 → 0.4.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.
- package/CHANGELOG.md +874 -0
- package/README.md +61 -29
- package/capabilities.json +19 -9
- package/dist/actions/index.js +56 -12
- package/dist/benchmark.js +10 -15
- package/dist/boot/index.d.ts +3 -2
- package/dist/boot/index.js +1 -1
- package/dist/boot-DaoTYoCw.js +3989 -0
- package/dist/browser-activity-CB8e-V4f.js +220 -0
- package/dist/components/index.d.ts +8 -1
- package/dist/components/index.js +1 -43
- package/dist/{aria-hKjThHcp.js → compose-ref-DWzdbxsD.js} +41 -4
- package/dist/control/index.d.ts +2 -0
- package/dist/control/index.js +1 -1
- package/dist/control-nfpT2fbK.js +209 -0
- package/dist/core.d.ts +5 -9
- package/dist/csp-nonce-DwGw1A6Q.js +35 -0
- package/dist/data/index.js +3 -3
- package/dist/{data-2edW0jhF.js → data-BCwClJiF.js} +283 -94
- package/dist/data-runtime-C1bFAlba.js +242 -0
- package/dist/{deferred-CbgNhf67.js → deferred-bEEHHvic.js} +42 -20
- package/dist/domains/actions.d.ts +8 -3
- package/dist/domains/component.d.ts +1 -107
- package/dist/domains/context.d.ts +0 -15
- package/dist/domains/control.d.ts +36 -249
- package/dist/domains/data.d.ts +31 -13
- package/dist/domains/diagnostics.d.ts +1 -2
- package/dist/domains/lifecycle.d.ts +2 -5
- package/dist/domains/refs.d.ts +0 -2
- package/dist/domains/routing.d.ts +43 -8
- package/dist/domains/scheduler.d.ts +1 -76
- package/dist/domains/server.d.ts +5 -2
- package/dist/domains/state.d.ts +2 -5
- package/dist/domains/telemetry.d.ts +0 -2
- package/dist/elements.d.ts +214 -51
- package/dist/error-boundary-Bpwofwps.js +293 -0
- package/dist/foundations/icon/index.d.ts +0 -58
- package/dist/foundations/icon/index.js +2 -2
- package/dist/foundations/index.js +2 -1
- package/dist/foundations/interactions/index.js +1 -1
- package/dist/foundations/state/index.d.ts +3 -3
- package/dist/foundations/state/index.js +5 -3
- package/dist/foundations/structures/index.js +2 -1
- package/dist/foundations/utilities/index.d.ts +2 -0
- package/dist/foundations/utilities/index.js +1 -2
- package/dist/fx/index.d.ts +31 -58
- package/dist/fx/index.js +313 -195
- package/dist/hooks-BimKStr4.js +202 -0
- package/dist/index.d.ts +11 -74
- package/dist/index.js +8 -447
- package/dist/instance-BAnfmXOR.js +988 -0
- package/dist/{interactions-CTeACdT_.js → interactions-D2BoREb-.js} +3 -2
- package/dist/jsx-dev-runtime.d.ts +41 -18
- package/dist/jsx-dev-runtime.js +1 -6
- package/dist/{jsx-runtime-CPJXAOBF.js → jsx-runtime-C4idqHY0.js} +18 -7
- package/dist/jsx-runtime.js +1 -1
- package/dist/jsx.d.ts +61 -37
- package/dist/{manifest-D1P3UNWV.js → manifest-CW9gyNBY.js} +3 -3
- package/dist/{navigate-prC5V8XV.js → navigate-CP_0Zy5U.js} +454 -87
- package/dist/navigation.d.ts +12 -3
- package/dist/peer-types.d.ts +8 -0
- package/dist/portal-DuOKfnLa.js +349 -0
- package/dist/promise-Dz8iIHAm.js +16 -0
- package/dist/query-registry-WMCN0BJL.js +320 -0
- package/dist/readable-DFWOsTac.js +21 -0
- package/dist/{render-context-B7ohlh6t.js → render-context-DGHnEveC.js} +27 -11
- package/dist/render-sync-IC5lMFpq.js +1240 -0
- package/dist/resolution-Cx82-xjW.js +1244 -0
- package/dist/resource-CLjB7RBQ.js +377 -0
- package/dist/resources/index.d.ts +21 -11
- package/dist/resources/index.js +45 -244
- package/dist/router/index.d.ts +14 -5
- package/dist/router/index.js +22 -53
- package/dist/scope-CwpVSHVf.js +55 -0
- package/dist/{shared-kU8yvryp.js → shared-O2ZnmYAf.js} +1 -7
- package/dist/{snapshot-source-An4iKajG.js → snapshot-Cjx4N988.js} +6 -1
- package/dist/ssg/index.d.ts +1 -2
- package/dist/ssg/index.js +5 -13
- package/dist/ssr/index.d.ts +26 -28
- package/dist/ssr/index.js +4 -4
- package/dist/{ssr-CiNm6Vzl.js → ssr-BJ8ug7zH.js} +133 -122
- package/dist/ssr-DoV-vJ-5.js +229 -0
- package/dist/state-DWO1KzkX.js +232 -0
- package/dist/structures-D4dDMqHX.js +231 -0
- package/dist/testing/index.d.ts +8 -4
- package/dist/testing/index.js +29 -34
- package/dist/url-CnbfpX0K.js +185 -0
- package/dist/utilities.d.ts +48 -2
- package/dist/verify-hydration-DpVaDnqz.js +152 -0
- package/package.json +19 -9
- package/dist/access-CEA2pOry.js +0 -1046
- package/dist/activity-oy2YFTFF.js +0 -201
- package/dist/boot-BMBMWUGI.js +0 -1084
- package/dist/capabilities-Bbf0mlp9.js +0 -42
- package/dist/compose-ref-v0qGlHoN.js +0 -26
- package/dist/control-BR_mrX__.js +0 -173
- package/dist/control-C-ApnFs1.js +0 -20
- package/dist/csp-nonce-BfFWcd0w.js +0 -173
- package/dist/data-runtime-CxoY98lX.js +0 -149
- package/dist/default-portal-runtime-DvMvWJVp.js +0 -20
- package/dist/dom-renderer.d.ts +0 -77
- package/dist/domains/renderer.d.ts +0 -127
- package/dist/eager-control.d.ts +0 -8
- package/dist/effect-C3Zsc1k6.js +0 -292
- package/dist/for-state-BDykeOJL.js +0 -1753
- package/dist/instance-VAFYVJOa.js +0 -1027
- package/dist/notify-DxxuDPeY.js +0 -266
- package/dist/portal-w3oQ6oQU.js +0 -29
- package/dist/query-registry-LrYpQsDJ.js +0 -83
- package/dist/render-resolved-BC0W7_5b.js +0 -1123
- package/dist/renderer-CT0HeY58.js +0 -8623
- package/dist/resolution-3DskUkjG.js +0 -577
- package/dist/resource-18-sFRl9.js +0 -344
- package/dist/route-matching-CUzZ31gT.js +0 -551
- package/dist/router-internal-7nuNEHfG.js +0 -36
- package/dist/scope-Cq_iYbTR.js +0 -403
- package/dist/selector-BSPVkjKP.js +0 -311
- package/dist/ssr-EPjwJH5R.js +0 -29
- package/dist/state-BXJfudq1.js +0 -91
- package/dist/structures-CjEzfj4h.js +0 -654
- package/dist/timer-C7uqnJVt.js +0 -85
- package/dist/verify-hydration-ikaxotf7.js +0 -27
- package/dist/vnode-D24p6IW2.js +0 -7
|
@@ -0,0 +1,3989 @@
|
|
|
1
|
+
import { C as schedule, F as logger, I as isDevelopmentEnvironment, L as isProductionEnvironment, M as getOwner, N as runWithOwner, S as queueTask, _ as batch, c as setRenderHost, d as recordUndo, f as rewindJournal, g as outsideRendering, i as getContextEpoch, j as Owner, l as createRenderJournal, m as withRenderJournal, n as HookOrderChangeError, p as settleJournal, t as ComponentInstance, u as journalMark, v as effectScheduler, w as Computation, y as flushSync } from "./instance-BAnfmXOR.js";
|
|
2
|
+
import { n as readValue } from "./readable-DFWOsTac.js";
|
|
3
|
+
import { n as Fragment, t as ELEMENT_TYPE } from "./jsx-DRS-qjiN.js";
|
|
4
|
+
import { _ as RenderDepthError, b as reportUncaughtErrorLater, g as routeError, h as noteErrorOrigin, i as currentComponent, l as provideAppRuntime, m as deliverToBoundary, p as clarifyRenderError, v as clarifyRenderOverflow, y as reportUncaughtError } from "./hooks-BimKStr4.js";
|
|
5
|
+
import { r as validateCspNonce, t as CspNonceScope } from "./csp-nonce-DwGw1A6Q.js";
|
|
6
|
+
import { d as startHydrationRenderPhase, p as stopHydrationRenderPhase, x as pageRenderEnvelope } from "./render-context-DGHnEveC.js";
|
|
7
|
+
import { a as descriptorType, o as functionChildOutput, s as normalizeChildren } from "./error-boundary-Bpwofwps.js";
|
|
8
|
+
import { D as clearStagedAppRenderRouteLocation, E as configureRootUpdateHost, O as createAppRenderRuntime, S as unregisterAppInstance, T as setServerLocation, _ as registerAppInstance, c as reconcileRouteMeta, d as configureScrollRestoration, k as stageAppRenderRouteLocation, l as resolveRouteMeta, o as readHistoryIndex, s as loadDocument, t as initializeNavigation } from "./navigate-CP_0Zy5U.js";
|
|
9
|
+
import { t as assertSyncComponentResult } from "./promise-Dz8iIHAm.js";
|
|
10
|
+
import { a as hydrateDataRuntime, m as reviveDeferredValue } from "./query-registry-WMCN0BJL.js";
|
|
11
|
+
import { A as clearRouteState, D as _setActiveRouteAuthOptions, Y as lockRouteRegistration, a as resolveRouteRequest, gt as withoutRouteHydrationMetadata, i as hasRouteRenderData, m as withoutHydratedAuth, n as getRouteRenderContext, p as readHydratedAuth, q as hasRegisteredRoutes, r as getRouteRenderData } from "./resolution-Cx82-xjW.js";
|
|
12
|
+
import { c as isSSRPortalHydrationAnchor, i as provideDefaultPortal, l as isSSRPortalWriterAnchor, t as DefaultPortal } from "./portal-DuOKfnLa.js";
|
|
13
|
+
import { r as assertExecutionModel, t as _applyManifest } from "./manifest-CW9gyNBY.js";
|
|
14
|
+
import { a as getDefaultDataRuntime } from "./data-runtime-C1bFAlba.js";
|
|
15
|
+
import { c as resolveNavigationUrl, d as isCustomElementName, l as attributeNamespace, n as UNSAFE_URL_SCHEME_ATTRIBUTES, s as rejectUnsafeUrlAttribute, t as SCRIPT_URL_RESOURCE_ATTRIBUTES, u as getPublicAttributeName } from "./url-CnbfpX0K.js";
|
|
16
|
+
import { a as PROPERTY_PROP_PREFIX, c as isKnownBooleanProperty, d as booleanAttributeValue, f as isSkippedProp, h as styleValueText, i as ATTRIBUTE_PROP_PREFIX, m as normalizeStylePropertyName, o as domPropertyValue, p as keepsFalseValue, r as sanitizeCssValue, s as getDomPropertyName, u as propertyReflectsAttribute } from "./ssr-DoV-vJ-5.js";
|
|
17
|
+
//#region src/boot/root-handler.ts
|
|
18
|
+
const AUTO_PORTAL = {
|
|
19
|
+
$$typeof: ELEMENT_TYPE,
|
|
20
|
+
type: DefaultPortal,
|
|
21
|
+
props: { __askrAutoDefaultPortal: true },
|
|
22
|
+
key: "__default_portal"
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Wrap a root's component: its output is followed by the automatic default
|
|
26
|
+
* portal host, and the CSP nonce (if any) is provided to everything below.
|
|
27
|
+
*/
|
|
28
|
+
function wrapRootRouteHandler(componentFn, cspNonce) {
|
|
29
|
+
const wrappedFn = (props, ctx) => {
|
|
30
|
+
const out = componentFn(props, ctx);
|
|
31
|
+
assertSyncComponentResult(out);
|
|
32
|
+
const root = {
|
|
33
|
+
$$typeof: ELEMENT_TYPE,
|
|
34
|
+
type: Fragment,
|
|
35
|
+
props: { children: out === void 0 || out === null ? [AUTO_PORTAL] : [out, AUTO_PORTAL] }
|
|
36
|
+
};
|
|
37
|
+
return cspNonce === void 0 ? root : CspNonceScope({
|
|
38
|
+
value: cspNonce,
|
|
39
|
+
children: root
|
|
40
|
+
});
|
|
41
|
+
};
|
|
42
|
+
Object.defineProperty(wrappedFn, "name", { value: componentFn.name || "Component" });
|
|
43
|
+
return wrappedFn;
|
|
44
|
+
}
|
|
45
|
+
//#endregion
|
|
46
|
+
//#region src/boot/root-update.ts
|
|
47
|
+
/**
|
|
48
|
+
* Route updates for application roots.
|
|
49
|
+
*
|
|
50
|
+
* `apply()` renders the destination (render phase only); `publish()` commits
|
|
51
|
+
* it; `rollback()` discards it and restores the root's previous route state.
|
|
52
|
+
* A navigation applies every root first and publishes only if all succeeded.
|
|
53
|
+
*/
|
|
54
|
+
function prepare(root, input) {
|
|
55
|
+
const app = root;
|
|
56
|
+
const current = app.appRuntime;
|
|
57
|
+
const runtime = createAppRenderRuntime({
|
|
58
|
+
framework: withoutRouteHydrationMetadata(current?.framework),
|
|
59
|
+
dataRuntime: current?.dataRuntime,
|
|
60
|
+
routeRegistry: current?.routeRegistry,
|
|
61
|
+
routeAuth: current?.routeAuth,
|
|
62
|
+
route: input.routeData,
|
|
63
|
+
hasRoute: input.hasRouteData,
|
|
64
|
+
lifetime: current?.lifetime
|
|
65
|
+
});
|
|
66
|
+
const previous = {
|
|
67
|
+
appRuntime: app.appRuntime,
|
|
68
|
+
component: app.component,
|
|
69
|
+
handler: app.handler,
|
|
70
|
+
generation: app.generation
|
|
71
|
+
};
|
|
72
|
+
let prepared = null;
|
|
73
|
+
let settled = false;
|
|
74
|
+
const restore = () => {
|
|
75
|
+
app.appRuntime = previous.appRuntime;
|
|
76
|
+
app.component = previous.component;
|
|
77
|
+
app.handler = previous.handler;
|
|
78
|
+
app.generation = previous.generation;
|
|
79
|
+
};
|
|
80
|
+
return {
|
|
81
|
+
apply() {
|
|
82
|
+
if (prepared || settled) return;
|
|
83
|
+
stageAppRenderRouteLocation(runtime, input.href);
|
|
84
|
+
app.appRuntime = runtime;
|
|
85
|
+
if (input.replaceLifetime) {
|
|
86
|
+
app.component = input.handler;
|
|
87
|
+
app.handler = wrapRootRouteHandler(input.handler, app.cspNonce);
|
|
88
|
+
app.generation++;
|
|
89
|
+
runtime.lifetime = app.generation;
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
prepared = app.root.prepare(app.view());
|
|
93
|
+
prepared.apply();
|
|
94
|
+
} catch (error) {
|
|
95
|
+
restore();
|
|
96
|
+
clearStagedAppRenderRouteLocation(runtime);
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
publish() {
|
|
101
|
+
if (settled) return {
|
|
102
|
+
aborted: false,
|
|
103
|
+
errors: []
|
|
104
|
+
};
|
|
105
|
+
settled = true;
|
|
106
|
+
try {
|
|
107
|
+
prepared?.publish();
|
|
108
|
+
return {
|
|
109
|
+
aborted: false,
|
|
110
|
+
errors: []
|
|
111
|
+
};
|
|
112
|
+
} catch (error) {
|
|
113
|
+
const aborted = prepared?.aborted === true;
|
|
114
|
+
if (aborted) restore();
|
|
115
|
+
return {
|
|
116
|
+
aborted,
|
|
117
|
+
errors: [error]
|
|
118
|
+
};
|
|
119
|
+
} finally {
|
|
120
|
+
clearStagedAppRenderRouteLocation(runtime);
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
rollback() {
|
|
124
|
+
if (settled) return [];
|
|
125
|
+
settled = true;
|
|
126
|
+
clearStagedAppRenderRouteLocation(runtime);
|
|
127
|
+
const errors = prepared?.rollback() ?? [];
|
|
128
|
+
restore();
|
|
129
|
+
return errors;
|
|
130
|
+
},
|
|
131
|
+
retire() {
|
|
132
|
+
return [];
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
function installRootUpdateHost() {
|
|
137
|
+
configureRootUpdateHost({ prepare });
|
|
138
|
+
}
|
|
139
|
+
//#endregion
|
|
140
|
+
//#region src/boot/composition.ts
|
|
141
|
+
let composed = false;
|
|
142
|
+
/**
|
|
143
|
+
* Browser composition root, owned by the boot entry: connects route updates
|
|
144
|
+
* from the router to application roots.
|
|
145
|
+
*/
|
|
146
|
+
function composeBrowserRuntime() {
|
|
147
|
+
if (composed) return;
|
|
148
|
+
composed = true;
|
|
149
|
+
installRootUpdateHost();
|
|
150
|
+
}
|
|
151
|
+
/** Compose whatever is still missing, for a mount reached without the boot entry. */
|
|
152
|
+
function ensureBrowserRuntime() {
|
|
153
|
+
composeBrowserRuntime();
|
|
154
|
+
}
|
|
155
|
+
//#endregion
|
|
156
|
+
//#region src/core/dom/pass.ts
|
|
157
|
+
/**
|
|
158
|
+
* A render pass.
|
|
159
|
+
*
|
|
160
|
+
* The render phase runs components, diffs their output, and builds new DOM
|
|
161
|
+
* off-document. Anything that would change live DOM or committed renderer
|
|
162
|
+
* state is recorded as an operation instead. `commit()` applies the
|
|
163
|
+
* operations in order; `discard()` drops them, disposes every owner the pass
|
|
164
|
+
* created, and rewinds the render journal (props and scope values set during
|
|
165
|
+
* render). A failed structural DOM write restores child lists that this pass
|
|
166
|
+
* already applied, then discards its provisional owners and subscriptions.
|
|
167
|
+
*
|
|
168
|
+
* Operations are recorded parent-first: a reconcile reserves its slot before
|
|
169
|
+
* its children record theirs, so a parent places its children before the
|
|
170
|
+
* children update their own contents.
|
|
171
|
+
*/
|
|
172
|
+
/** A structural DOM write failed before its render pass could publish. */
|
|
173
|
+
var CommitMutationError = class extends Error {
|
|
174
|
+
constructor(failure) {
|
|
175
|
+
super("DOM commit failed");
|
|
176
|
+
this.failure = failure;
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
var Pass = class {
|
|
180
|
+
constructor() {
|
|
181
|
+
this.hasDormantPortalWriter = false;
|
|
182
|
+
this.commitAborted = false;
|
|
183
|
+
this.ops = [];
|
|
184
|
+
this.created = [];
|
|
185
|
+
this.renderedInstances = [];
|
|
186
|
+
this.afterCommit = [];
|
|
187
|
+
this.beforeSettle = [];
|
|
188
|
+
this.commitUndo = [];
|
|
189
|
+
this.commitSettle = [];
|
|
190
|
+
this.journal = createRenderJournal();
|
|
191
|
+
this.phase = "prepared";
|
|
192
|
+
this.failures = [];
|
|
193
|
+
}
|
|
194
|
+
/** Run render work with this pass's provisional undo journal active. */
|
|
195
|
+
run(render) {
|
|
196
|
+
return withRenderJournal(this.journal, render);
|
|
197
|
+
}
|
|
198
|
+
/** Record an operation on committed state. */
|
|
199
|
+
op(fn) {
|
|
200
|
+
this.ops.push(fn);
|
|
201
|
+
}
|
|
202
|
+
/** Reserve an operation slot to fill after nested work records its own. */
|
|
203
|
+
reserve() {
|
|
204
|
+
this.ops.push(null);
|
|
205
|
+
return this.ops.length - 1;
|
|
206
|
+
}
|
|
207
|
+
fill(slot, fn) {
|
|
208
|
+
this.ops[slot] = fn;
|
|
209
|
+
}
|
|
210
|
+
/** An owner this pass created; disposed if the pass is discarded. */
|
|
211
|
+
own(owner) {
|
|
212
|
+
this.created.push(owner);
|
|
213
|
+
}
|
|
214
|
+
/** An instance rendered by this pass; it commits (and mounts) with it. */
|
|
215
|
+
markRendered(instance) {
|
|
216
|
+
this.renderedInstances.push(instance);
|
|
217
|
+
}
|
|
218
|
+
/** Undo a provisional render-time change if this work is discarded. */
|
|
219
|
+
onDiscard(fn) {
|
|
220
|
+
recordUndo(fn, this.journal);
|
|
221
|
+
}
|
|
222
|
+
/** Run after all operations are applied (refs). */
|
|
223
|
+
after(fn) {
|
|
224
|
+
this.afterCommit.push(fn);
|
|
225
|
+
}
|
|
226
|
+
/** Run after DOM operations while the pass can still be rolled back. */
|
|
227
|
+
beforeJournalSettle(fn) {
|
|
228
|
+
this.beforeSettle.push(fn);
|
|
229
|
+
}
|
|
230
|
+
/** Restore a reversible write on abort; settle its old lifetime on success. */
|
|
231
|
+
onReversibleCommit(undo, settle = () => {}) {
|
|
232
|
+
this.commitUndo.push(undo);
|
|
233
|
+
this.commitSettle.push(settle);
|
|
234
|
+
}
|
|
235
|
+
mark() {
|
|
236
|
+
return {
|
|
237
|
+
ops: this.ops.length,
|
|
238
|
+
created: this.created.length,
|
|
239
|
+
rendered: this.renderedInstances.length,
|
|
240
|
+
journal: journalMark(this.journal),
|
|
241
|
+
after: this.afterCommit.length,
|
|
242
|
+
beforeSettle: this.beforeSettle.length,
|
|
243
|
+
commitUndo: this.commitUndo.length,
|
|
244
|
+
commitSettle: this.commitSettle.length,
|
|
245
|
+
dormantPortalWriter: this.hasDormantPortalWriter
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
/** Discard everything recorded since `mark` (an error boundary caught). */
|
|
249
|
+
rewind(mark) {
|
|
250
|
+
const errors = [];
|
|
251
|
+
this.ops.length = mark.ops;
|
|
252
|
+
this.afterCommit.length = mark.after;
|
|
253
|
+
this.renderedInstances.length = mark.rendered;
|
|
254
|
+
this.hasDormantPortalWriter = mark.dormantPortalWriter;
|
|
255
|
+
this.beforeSettle.length = mark.beforeSettle;
|
|
256
|
+
this.commitUndo.length = mark.commitUndo;
|
|
257
|
+
this.commitSettle.length = mark.commitSettle;
|
|
258
|
+
rewindJournal(this.journal, mark.journal, errors);
|
|
259
|
+
for (const owner of this.created.splice(mark.created).reverse()) owner.dispose(errors);
|
|
260
|
+
return errors;
|
|
261
|
+
}
|
|
262
|
+
discard() {
|
|
263
|
+
if (this.phase === "applied") return this.rollback();
|
|
264
|
+
if (this.phase === "published" || this.phase === "discarded") return [];
|
|
265
|
+
this.phase = "discarded";
|
|
266
|
+
return this.rewind({
|
|
267
|
+
ops: 0,
|
|
268
|
+
created: 0,
|
|
269
|
+
rendered: 0,
|
|
270
|
+
journal: 0,
|
|
271
|
+
after: 0,
|
|
272
|
+
beforeSettle: 0,
|
|
273
|
+
commitUndo: 0,
|
|
274
|
+
commitSettle: 0,
|
|
275
|
+
dormantPortalWriter: false
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
/** Apply reversible operations while retaining undo work for rollback. */
|
|
279
|
+
apply() {
|
|
280
|
+
if (this.phase === "applied") return;
|
|
281
|
+
if (this.phase !== "prepared") throw new Error("[Askr] Cannot apply a settled render pass.");
|
|
282
|
+
const abort = (failure) => {
|
|
283
|
+
this.commitAborted = true;
|
|
284
|
+
for (const error of this.rollback()) reportUncaughtErrorLater(error);
|
|
285
|
+
throw failure;
|
|
286
|
+
};
|
|
287
|
+
this.phase = "applied";
|
|
288
|
+
for (const op of this.ops) {
|
|
289
|
+
if (!op) continue;
|
|
290
|
+
try {
|
|
291
|
+
op();
|
|
292
|
+
} catch (error) {
|
|
293
|
+
if (error instanceof CommitMutationError) abort(error.failure);
|
|
294
|
+
this.failures.push(error);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
for (const fn of this.beforeSettle) try {
|
|
298
|
+
fn();
|
|
299
|
+
} catch (error) {
|
|
300
|
+
if (error instanceof CommitMutationError) abort(error.failure);
|
|
301
|
+
this.failures.push(error);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
/** Publish applied work and run irreversible cleanup, refs, and lifecycles. */
|
|
305
|
+
publish() {
|
|
306
|
+
if (this.phase === "prepared") this.apply();
|
|
307
|
+
if (this.phase !== "applied") throw new Error("[Askr] Cannot publish a render pass that was not applied.");
|
|
308
|
+
this.phase = "published";
|
|
309
|
+
settleJournal(this.journal, 0);
|
|
310
|
+
for (const settle of this.commitSettle) try {
|
|
311
|
+
settle();
|
|
312
|
+
} catch (error) {
|
|
313
|
+
this.failures.push(error);
|
|
314
|
+
}
|
|
315
|
+
for (const fn of this.afterCommit) try {
|
|
316
|
+
fn();
|
|
317
|
+
} catch (error) {
|
|
318
|
+
this.failures.push(error);
|
|
319
|
+
}
|
|
320
|
+
for (const instance of this.renderedInstances) if (!instance.disposed) try {
|
|
321
|
+
mount(instance);
|
|
322
|
+
} catch (error) {
|
|
323
|
+
this.failures.push(error);
|
|
324
|
+
}
|
|
325
|
+
if (this.failures.length === 1) throw this.failures[0];
|
|
326
|
+
if (this.failures.length > 1) throw new AggregateError(this.failures, "Commit failed");
|
|
327
|
+
}
|
|
328
|
+
/** Undo applied operations and discard every provisional render mutation. */
|
|
329
|
+
rollback() {
|
|
330
|
+
if (this.phase === "published" || this.phase === "discarded") return [];
|
|
331
|
+
const errors = [];
|
|
332
|
+
if (this.phase === "applied") for (let index = this.commitUndo.length - 1; index >= 0; index--) try {
|
|
333
|
+
this.commitUndo[index]();
|
|
334
|
+
} catch (error) {
|
|
335
|
+
errors.push(error);
|
|
336
|
+
}
|
|
337
|
+
this.phase = "prepared";
|
|
338
|
+
errors.push(...this.discard());
|
|
339
|
+
return errors;
|
|
340
|
+
}
|
|
341
|
+
/** Apply and publish immediately for a standalone root render. */
|
|
342
|
+
commit() {
|
|
343
|
+
this.apply();
|
|
344
|
+
this.publish();
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
/** Mark an instance committed and schedule its post-commit work. */
|
|
348
|
+
function mount(instance) {
|
|
349
|
+
instance.mounted = true;
|
|
350
|
+
const syncQueue = instance.commitSyncQueue;
|
|
351
|
+
instance.commitSyncQueue = null;
|
|
352
|
+
for (const fn of syncQueue ?? []) fn();
|
|
353
|
+
const queue = instance.commitQueue;
|
|
354
|
+
if (!queue) return;
|
|
355
|
+
instance.commitQueue = null;
|
|
356
|
+
queueTask(() => {
|
|
357
|
+
if (instance.disposed) return;
|
|
358
|
+
for (const fn of queue) try {
|
|
359
|
+
const cleanup = fn();
|
|
360
|
+
if (typeof cleanup === "function") instance.onCleanup(cleanup);
|
|
361
|
+
} catch (error) {
|
|
362
|
+
reportUncaughtErrorLater(error);
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
//#endregion
|
|
367
|
+
//#region src/core/dom/element-attributes.ts
|
|
368
|
+
/**
|
|
369
|
+
* Low-level attribute access for rendered elements: the attribute name a prop
|
|
370
|
+
* renders as, namespaced writes, and class names on HTML and SVG elements.
|
|
371
|
+
*/
|
|
372
|
+
const SVG_NAMESPACE = "http://www.w3.org/2000/svg";
|
|
373
|
+
function isSVGDomElement(el) {
|
|
374
|
+
return typeof SVGElement !== "undefined" && el instanceof SVGElement;
|
|
375
|
+
}
|
|
376
|
+
function getRenderedAttributeName(el, propName) {
|
|
377
|
+
const attributeName = getPublicAttributeName(propName, isCustomElementName(el.localName));
|
|
378
|
+
return el.namespaceURI === SVG_NAMESPACE ? attributeName : attributeName.toLowerCase();
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Write an attribute, placing `xlink:`/`xml:`/`xmlns:` names on SVG and MathML
|
|
382
|
+
* elements in their namespace.
|
|
383
|
+
*/
|
|
384
|
+
function writeAttribute(el, attributeName, value, namespace = attributeNamespace(el.namespaceURI, attributeName)) {
|
|
385
|
+
if (namespace === null) el.setAttribute(attributeName, value);
|
|
386
|
+
else el.setAttributeNS(namespace, attributeName, value);
|
|
387
|
+
}
|
|
388
|
+
function setRenderedAttribute(el, propName, value) {
|
|
389
|
+
writeAttribute(el, getRenderedAttributeName(el, propName), value);
|
|
390
|
+
}
|
|
391
|
+
function removeRenderedAttribute(el, propName) {
|
|
392
|
+
el.removeAttribute(getRenderedAttributeName(el, propName));
|
|
393
|
+
}
|
|
394
|
+
function readElementClassName(el) {
|
|
395
|
+
if (isSVGDomElement(el)) return el.getAttribute("class") ?? "";
|
|
396
|
+
return el.className;
|
|
397
|
+
}
|
|
398
|
+
function writeElementClassName(el, value) {
|
|
399
|
+
if (isSVGDomElement(el)) {
|
|
400
|
+
if (value.length > 0) el.setAttribute("class", value);
|
|
401
|
+
else el.removeAttribute("class");
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
el.className = value;
|
|
405
|
+
}
|
|
406
|
+
function tagNamesEqualIgnoreCase(a, b) {
|
|
407
|
+
if (a === b) return true;
|
|
408
|
+
if (a.length !== b.length) return false;
|
|
409
|
+
for (let index = 0; index < a.length; index += 1) {
|
|
410
|
+
const aCode = a.charCodeAt(index);
|
|
411
|
+
const bCode = b.charCodeAt(index);
|
|
412
|
+
if (aCode === bCode) continue;
|
|
413
|
+
if ((aCode >= 65 && aCode <= 90 ? aCode + 32 : aCode) !== (bCode >= 65 && bCode <= 90 ? bCode + 32 : bCode)) return false;
|
|
414
|
+
}
|
|
415
|
+
return true;
|
|
416
|
+
}
|
|
417
|
+
//#endregion
|
|
418
|
+
//#region src/core/dom/events.ts
|
|
419
|
+
/**
|
|
420
|
+
* Event handler props on rendered elements.
|
|
421
|
+
*
|
|
422
|
+
* Bubbling UI events (`click`, `input`, keyboard, mouse, touch) are
|
|
423
|
+
* delegated: each app root, plus `document.body` for content rendered outside
|
|
424
|
+
* any root (portals), listens once per event type. When an event reaches a
|
|
425
|
+
* container, its handlers along the event path are snapshotted and run
|
|
426
|
+
* target-first in one batch, so state writes flush once, synchronously, after
|
|
427
|
+
* the last handler, and a handler that removes an ancestor does not stop that
|
|
428
|
+
* ancestor's handler for the same event. Other events and capture handlers
|
|
429
|
+
* get one direct listener per element, run in their own batch.
|
|
430
|
+
*
|
|
431
|
+
* A handler's handler function can be swapped without re-registering. It runs
|
|
432
|
+
* with its component as the current owner, and a throwing handler is reported
|
|
433
|
+
* like an uncaught listener exception.
|
|
434
|
+
*/
|
|
435
|
+
/** Root container visible to listeners through their component owner. */
|
|
436
|
+
const EVENT_ROOT_CONTAINER = Symbol("askr.event-root-container");
|
|
437
|
+
const DELEGATED_EVENTS = /* @__PURE__ */ new Set([
|
|
438
|
+
"click",
|
|
439
|
+
"dblclick",
|
|
440
|
+
"mousedown",
|
|
441
|
+
"mouseup",
|
|
442
|
+
"mouseover",
|
|
443
|
+
"mouseout",
|
|
444
|
+
"mousemove",
|
|
445
|
+
"touchend",
|
|
446
|
+
"touchcancel",
|
|
447
|
+
"keydown",
|
|
448
|
+
"keyup",
|
|
449
|
+
"keypress",
|
|
450
|
+
"input"
|
|
451
|
+
]);
|
|
452
|
+
/** `onClick` -> click, `onKeyDownCapture` -> keydown (capture); else null. */
|
|
453
|
+
function parseEventProp(propName) {
|
|
454
|
+
if (propName.length <= 2 || !propName.startsWith("on")) return null;
|
|
455
|
+
const capture = propName.endsWith("Capture") && !propName.endsWith("PointerCapture") && propName.length > 9;
|
|
456
|
+
const name = capture ? propName.slice(0, -7) : propName;
|
|
457
|
+
if (name.length <= 2) return null;
|
|
458
|
+
return {
|
|
459
|
+
eventName: name.slice(2).toLowerCase(),
|
|
460
|
+
capture
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* `wheel`, `touchstart` and `touchmove` opt out of passive defaults so a
|
|
465
|
+
* handler can call `preventDefault()`; `scroll` is always passive.
|
|
466
|
+
*/
|
|
467
|
+
function listenerOptions(eventName, capture) {
|
|
468
|
+
let options;
|
|
469
|
+
if (eventName === "scroll") options = { passive: true };
|
|
470
|
+
else if (eventName === "wheel" || eventName === "touchstart" || eventName === "touchmove") options = { passive: false };
|
|
471
|
+
if (!capture) return options;
|
|
472
|
+
return {
|
|
473
|
+
...options,
|
|
474
|
+
capture: true
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
function invoke(listener, el, event) {
|
|
478
|
+
runWithOwner(listener.owner, () => {
|
|
479
|
+
try {
|
|
480
|
+
listener.handler.call(el, event);
|
|
481
|
+
} catch (error) {
|
|
482
|
+
reportUncaughtError(error);
|
|
483
|
+
}
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
/** Delegated handlers per element, by event type. */
|
|
487
|
+
const delegatedHandlers = /* @__PURE__ */ new WeakMap();
|
|
488
|
+
/** Elements holding a delegated handler, per event type. */
|
|
489
|
+
const delegatedUsage = /* @__PURE__ */ new Map();
|
|
490
|
+
/** Registered app roots, each counted once per live root using it. */
|
|
491
|
+
const roots = /* @__PURE__ */ new Map();
|
|
492
|
+
/** Container listeners, per container, by event type. */
|
|
493
|
+
const containerListeners = /* @__PURE__ */ new Map();
|
|
494
|
+
function fallbackContainer() {
|
|
495
|
+
return typeof document === "undefined" ? null : document.body;
|
|
496
|
+
}
|
|
497
|
+
function listen(container, eventName) {
|
|
498
|
+
let listeners = containerListeners.get(container);
|
|
499
|
+
if (listeners?.has(eventName)) return;
|
|
500
|
+
const listener = (event) => dispatch(event, eventName);
|
|
501
|
+
container.addEventListener(eventName, listener);
|
|
502
|
+
if (!listeners) containerListeners.set(container, listeners = /* @__PURE__ */ new Map());
|
|
503
|
+
listeners.set(eventName, listener);
|
|
504
|
+
}
|
|
505
|
+
function unlisten(container, eventName) {
|
|
506
|
+
const listeners = containerListeners.get(container);
|
|
507
|
+
const listener = listeners?.get(eventName);
|
|
508
|
+
if (!listener) return;
|
|
509
|
+
container.removeEventListener(eventName, listener);
|
|
510
|
+
listeners.delete(eventName);
|
|
511
|
+
if (listeners.size === 0) containerListeners.delete(container);
|
|
512
|
+
}
|
|
513
|
+
function containers() {
|
|
514
|
+
const all = [...roots.keys()];
|
|
515
|
+
const fallback = fallbackContainer();
|
|
516
|
+
if (fallback && !roots.has(fallback)) all.push(fallback);
|
|
517
|
+
return all;
|
|
518
|
+
}
|
|
519
|
+
/** Delegate events rendered under `root` at `root`, until released. */
|
|
520
|
+
function registerEventRoot(root) {
|
|
521
|
+
roots.set(root, (roots.get(root) ?? 0) + 1);
|
|
522
|
+
for (const eventName of delegatedUsage.keys()) listen(root, eventName);
|
|
523
|
+
let released = false;
|
|
524
|
+
return () => {
|
|
525
|
+
if (released) return;
|
|
526
|
+
released = true;
|
|
527
|
+
const count = roots.get(root) - 1;
|
|
528
|
+
if (count > 0) {
|
|
529
|
+
roots.set(root, count);
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
roots.delete(root);
|
|
533
|
+
if (root === fallbackContainer()) return;
|
|
534
|
+
for (const eventName of delegatedUsage.keys()) unlisten(root, eventName);
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
function isShadowRoot(node) {
|
|
538
|
+
return node.nodeType === 11 && node.host != null;
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* Index of `container` in `composed`, or -1 when the composed path cannot be
|
|
542
|
+
* used: it lacks the container, or it skips an ancestor of the (retargeted)
|
|
543
|
+
* target below the container, as some browser hosts do.
|
|
544
|
+
*/
|
|
545
|
+
function composedPathEnd(composed, target, container) {
|
|
546
|
+
const end = composed.indexOf(container);
|
|
547
|
+
if (end === -1) return -1;
|
|
548
|
+
let expected = target;
|
|
549
|
+
for (let i = 0; i < end && expected; i++) if (composed[i] === expected) expected = expected.parentNode;
|
|
550
|
+
return expected === container ? end : -1;
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* The target's parent chain up to `container`, supplemented with any
|
|
554
|
+
* composed path nodes it misses, for hosts whose composed path is unusable.
|
|
555
|
+
*/
|
|
556
|
+
function ancestryPath(event, container) {
|
|
557
|
+
const path = [];
|
|
558
|
+
const seen = /* @__PURE__ */ new Set();
|
|
559
|
+
for (let node = event.target; node; node = node.parentNode) {
|
|
560
|
+
seen.add(node);
|
|
561
|
+
path.push(node);
|
|
562
|
+
if (node === container) return path;
|
|
563
|
+
}
|
|
564
|
+
for (const node of event.composedPath?.() ?? []) if (!seen.has(node)) {
|
|
565
|
+
seen.add(node);
|
|
566
|
+
path.push(node);
|
|
567
|
+
}
|
|
568
|
+
return path;
|
|
569
|
+
}
|
|
570
|
+
/** The event as seen by one element's handler. */
|
|
571
|
+
function eventFor(event, currentTarget, target) {
|
|
572
|
+
return new Proxy(event, { get(native, property) {
|
|
573
|
+
if (property === "currentTarget") return currentTarget;
|
|
574
|
+
if (property === "target") return target;
|
|
575
|
+
const value = Reflect.get(native, property, native);
|
|
576
|
+
return typeof value === "function" ? value.bind(native) : value;
|
|
577
|
+
} });
|
|
578
|
+
}
|
|
579
|
+
function dispatch(event, eventName) {
|
|
580
|
+
const container = event.currentTarget;
|
|
581
|
+
const composed = event.composedPath?.() ?? [];
|
|
582
|
+
const end = composedPathEnd(composed, event.target, container);
|
|
583
|
+
const path = end === -1 ? ancestryPath(event, container) : composed;
|
|
584
|
+
const last = end === -1 ? path.length : end;
|
|
585
|
+
const retarget = end > 0 && composed[0] !== event.target;
|
|
586
|
+
let target = retarget ? composed[0] : event.target;
|
|
587
|
+
let depth = 0;
|
|
588
|
+
let targetDepth = 0;
|
|
589
|
+
const handlers = [];
|
|
590
|
+
for (let i = 0; i < last; i++) {
|
|
591
|
+
const node = path[i];
|
|
592
|
+
if (node === container) break;
|
|
593
|
+
if (retarget) {
|
|
594
|
+
if (isShadowRoot(node)) {
|
|
595
|
+
if (--depth < targetDepth) {
|
|
596
|
+
target = node.host;
|
|
597
|
+
targetDepth = depth;
|
|
598
|
+
}
|
|
599
|
+
} else if (i > 0 && path[i - 1].assignedSlot === node) depth++;
|
|
600
|
+
}
|
|
601
|
+
if (node.nodeType !== 1) continue;
|
|
602
|
+
if (containerListeners.get(node)?.has(eventName)) handlers.length = 0;
|
|
603
|
+
const listener = delegatedHandlers.get(node)?.get(eventName);
|
|
604
|
+
if (listener) handlers.push({
|
|
605
|
+
el: node,
|
|
606
|
+
listener,
|
|
607
|
+
target
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
if (handlers.length === 0) return;
|
|
611
|
+
outsideRendering(() => batch(() => {
|
|
612
|
+
for (const { el, listener, target } of handlers) {
|
|
613
|
+
invoke(listener, el, eventFor(event, el, target));
|
|
614
|
+
if (event.cancelBubble) break;
|
|
615
|
+
}
|
|
616
|
+
}));
|
|
617
|
+
}
|
|
618
|
+
function delegate(el, listener) {
|
|
619
|
+
let byEvent = delegatedHandlers.get(el);
|
|
620
|
+
if (!byEvent) delegatedHandlers.set(el, byEvent = /* @__PURE__ */ new Map());
|
|
621
|
+
byEvent.set(listener.eventName, listener);
|
|
622
|
+
const usage = delegatedUsage.get(listener.eventName) ?? 0;
|
|
623
|
+
delegatedUsage.set(listener.eventName, usage + 1);
|
|
624
|
+
if (usage === 0) for (const container of containers()) listen(container, listener.eventName);
|
|
625
|
+
}
|
|
626
|
+
function undelegate(el, eventName) {
|
|
627
|
+
const byEvent = delegatedHandlers.get(el);
|
|
628
|
+
if (!byEvent?.delete(eventName)) return;
|
|
629
|
+
if (byEvent.size === 0) delegatedHandlers.delete(el);
|
|
630
|
+
const usage = delegatedUsage.get(eventName) - 1;
|
|
631
|
+
if (usage > 0) {
|
|
632
|
+
delegatedUsage.set(eventName, usage);
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
delegatedUsage.delete(eventName);
|
|
636
|
+
for (const container of containerListeners.keys()) unlisten(container, eventName);
|
|
637
|
+
}
|
|
638
|
+
function setListener(listeners, el, propName, event, handler, owner) {
|
|
639
|
+
const existing = listeners.get(propName);
|
|
640
|
+
if (existing) {
|
|
641
|
+
existing.handler = handler;
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
if (!event.capture && DELEGATED_EVENTS.has(event.eventName) && (roots.size > 0 || fallbackContainer())) {
|
|
645
|
+
const listener = {
|
|
646
|
+
handler,
|
|
647
|
+
owner,
|
|
648
|
+
eventName: event.eventName,
|
|
649
|
+
wrapped: null,
|
|
650
|
+
options: void 0
|
|
651
|
+
};
|
|
652
|
+
listeners.set(propName, listener);
|
|
653
|
+
delegate(el, listener);
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
const options = listenerOptions(event.eventName, event.capture);
|
|
657
|
+
const listener = {
|
|
658
|
+
handler,
|
|
659
|
+
owner,
|
|
660
|
+
eventName: event.eventName,
|
|
661
|
+
options,
|
|
662
|
+
wrapped: (nativeEvent) => {
|
|
663
|
+
outsideRendering(() => batch(() => invoke(listener, el, nativeEvent)));
|
|
664
|
+
}
|
|
665
|
+
};
|
|
666
|
+
listeners.set(propName, listener);
|
|
667
|
+
el.addEventListener(event.eventName, listener.wrapped, options);
|
|
668
|
+
}
|
|
669
|
+
function removeListener(listeners, el, propName) {
|
|
670
|
+
const listener = listeners.get(propName);
|
|
671
|
+
if (!listener) return;
|
|
672
|
+
listeners.delete(propName);
|
|
673
|
+
if (!listener.wrapped) {
|
|
674
|
+
undelegate(el, listener.eventName);
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
el.removeEventListener(listener.eventName, listener.wrapped, listener.options);
|
|
678
|
+
}
|
|
679
|
+
function removeAllListeners(listeners, el, errors) {
|
|
680
|
+
for (const propName of Array.from(listeners.keys())) try {
|
|
681
|
+
removeListener(listeners, el, propName);
|
|
682
|
+
} catch (error) {
|
|
683
|
+
errors.push(error);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
//#endregion
|
|
687
|
+
//#region src/core/dom/hydration.ts
|
|
688
|
+
/**
|
|
689
|
+
* Hydration: rendering over server markup with a cursor.
|
|
690
|
+
*
|
|
691
|
+
* While a root's first render hydrates, each container element has a cursor
|
|
692
|
+
* at its next unclaimed child. Creating a text or element node first tries to
|
|
693
|
+
* claim the node under the cursor: an element with the same tag, or a text
|
|
694
|
+
* node (split when the server merged adjacent texts). A node that does not
|
|
695
|
+
* match is created fresh instead. When a container's children are rendered,
|
|
696
|
+
* one commit operation puts the claimed and created nodes in order and
|
|
697
|
+
* removes whatever the server rendered that the client did not.
|
|
698
|
+
*/
|
|
699
|
+
var HydrationCursor = class HydrationCursor {
|
|
700
|
+
constructor(stopAt = null) {
|
|
701
|
+
this.stopAt = stopAt;
|
|
702
|
+
this.next = /* @__PURE__ */ new Map();
|
|
703
|
+
this.deferred = [];
|
|
704
|
+
}
|
|
705
|
+
/** Consume private SSR metadata emitted immediately before a component output. */
|
|
706
|
+
claimResourceSlots(container) {
|
|
707
|
+
let node = this.next.has(container) ? this.next.get(container) : container.firstChild;
|
|
708
|
+
for (; node && node !== this.stopAt; node = node.nextSibling) {
|
|
709
|
+
if (node.nodeType === 3 && isInsignificantWhitespace(node)) continue;
|
|
710
|
+
if (node.nodeType !== 8) return null;
|
|
711
|
+
const match = /^askr-resource:(r:\d+(?:,r:\d+)*)$/.exec(node.data);
|
|
712
|
+
if (match) {
|
|
713
|
+
this.advance(container, node);
|
|
714
|
+
return match[1].split(",");
|
|
715
|
+
}
|
|
716
|
+
if (isRangeMarker(node, "askr-range-start") || isRangeMarker(node, "askr-range-end")) return null;
|
|
717
|
+
}
|
|
718
|
+
return null;
|
|
719
|
+
}
|
|
720
|
+
/** Server nodes reserved for a portal host whose writer has not hydrated. */
|
|
721
|
+
heldNodes(container) {
|
|
722
|
+
if (!this.stopAt) return [];
|
|
723
|
+
const nodes = [];
|
|
724
|
+
let node = this.next.has(container) ? this.next.get(container) : container.firstChild;
|
|
725
|
+
while (node && node !== this.stopAt) {
|
|
726
|
+
if (node.nodeType !== 8) nodes.push(node);
|
|
727
|
+
node = node.nextSibling;
|
|
728
|
+
}
|
|
729
|
+
return nodes;
|
|
730
|
+
}
|
|
731
|
+
/**
|
|
732
|
+
* Reserve the server nodes at `container`'s cursor for a render that runs
|
|
733
|
+
* later. A server range (`askr-range-start` ... `askr-range-end`) is skipped
|
|
734
|
+
* as a whole; the returned cursor claims inside it, or from the reserved
|
|
735
|
+
* position onward when there is no range.
|
|
736
|
+
*/
|
|
737
|
+
reserve(container) {
|
|
738
|
+
let node = this.next.has(container) ? this.next.get(container) : container.firstChild;
|
|
739
|
+
while (node && node !== this.stopAt && node.nodeType === 3 && isInsignificantWhitespace(node)) node = node.nextSibling;
|
|
740
|
+
if (node && isRangeMarker(node, "askr-range-start")) {
|
|
741
|
+
let depth = 0;
|
|
742
|
+
let end = node;
|
|
743
|
+
for (; end && end !== this.stopAt; end = end.nextSibling) if (isRangeMarker(end, "askr-range-start")) depth++;
|
|
744
|
+
else if (isRangeMarker(end, "askr-range-end") && --depth === 0) break;
|
|
745
|
+
if (end && end !== this.stopAt) {
|
|
746
|
+
this.next.set(container, end.nextSibling);
|
|
747
|
+
const inner = new HydrationCursor(end);
|
|
748
|
+
inner.next.set(container, node.nextSibling);
|
|
749
|
+
return inner;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
const later = new HydrationCursor(this.stopAt);
|
|
753
|
+
later.next.set(container, node);
|
|
754
|
+
return later;
|
|
755
|
+
}
|
|
756
|
+
peek(container) {
|
|
757
|
+
let node = this.next.has(container) ? this.next.get(container) : container.firstChild;
|
|
758
|
+
while (node && node !== this.stopAt && (node.nodeType === 8 || node.nodeType === 3 && isInsignificantWhitespace(node))) node = node.nextSibling;
|
|
759
|
+
return node === this.stopAt ? null : node;
|
|
760
|
+
}
|
|
761
|
+
advance(container, node) {
|
|
762
|
+
this.next.set(container, node.nextSibling);
|
|
763
|
+
}
|
|
764
|
+
claimElement(container, tag, namespace) {
|
|
765
|
+
const node = this.peek(container);
|
|
766
|
+
if (!node || node.nodeType !== 1) return null;
|
|
767
|
+
const el = node;
|
|
768
|
+
const expectedNs = namespace ?? "http://www.w3.org/1999/xhtml";
|
|
769
|
+
if (!(expectedNs === "http://www.w3.org/1999/xhtml" ? el.localName === tag.toLowerCase() : el.localName === tag)) return null;
|
|
770
|
+
if ((el.namespaceURI ?? expectedNs) !== expectedNs) return null;
|
|
771
|
+
this.advance(container, el);
|
|
772
|
+
return el;
|
|
773
|
+
}
|
|
774
|
+
/**
|
|
775
|
+
* Claim a text node for `text`. A server text that begins with `text` is
|
|
776
|
+
* split so the remainder stays claimable by the next text; any other
|
|
777
|
+
* server text is claimed and corrected at commit.
|
|
778
|
+
*/
|
|
779
|
+
claimText(container, text) {
|
|
780
|
+
const node = this.peek(container);
|
|
781
|
+
if (!node || node.nodeType !== 3) return null;
|
|
782
|
+
const textNode = node;
|
|
783
|
+
if (text.length > 0 && textNode.data.length > text.length && textNode.data.startsWith(text)) {
|
|
784
|
+
const remainder = document.createTextNode(textNode.data.slice(text.length));
|
|
785
|
+
this.next.set(container, remainder);
|
|
786
|
+
return textNode;
|
|
787
|
+
}
|
|
788
|
+
this.advance(container, textNode);
|
|
789
|
+
return textNode;
|
|
790
|
+
}
|
|
791
|
+
};
|
|
792
|
+
function isRangeMarker(node, data) {
|
|
793
|
+
return node.nodeType === 8 && node.data === data;
|
|
794
|
+
}
|
|
795
|
+
function isInsignificantWhitespace(node) {
|
|
796
|
+
return node.data.trim() === "" && node.data.includes("\n");
|
|
797
|
+
}
|
|
798
|
+
/**
|
|
799
|
+
* Put `container`'s content in the order `expected`, removing any server
|
|
800
|
+
* node that was not claimed. Nodes after `stopAt` are left alone.
|
|
801
|
+
*/
|
|
802
|
+
function syncChildren(pass, container, expected, stopAt = null) {
|
|
803
|
+
const previous = Array.from(container.childNodes);
|
|
804
|
+
pass.onReversibleCommit(() => restoreChildren(container, previous));
|
|
805
|
+
try {
|
|
806
|
+
const skipAnchors = (node) => {
|
|
807
|
+
while (node && node !== stopAt && isSSRPortalHydrationAnchor(node)) node = node.nextSibling;
|
|
808
|
+
return node;
|
|
809
|
+
};
|
|
810
|
+
let cursor = skipAnchors(container.firstChild);
|
|
811
|
+
for (const node of expected) if (node === cursor) cursor = skipAnchors(cursor.nextSibling);
|
|
812
|
+
else container.insertBefore(node, cursor);
|
|
813
|
+
while (cursor && cursor !== stopAt) {
|
|
814
|
+
const next = cursor.nextSibling;
|
|
815
|
+
if (!isSSRPortalHydrationAnchor(cursor)) container.removeChild(cursor);
|
|
816
|
+
cursor = next;
|
|
817
|
+
}
|
|
818
|
+
} catch (error) {
|
|
819
|
+
throw new CommitMutationError(error);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
function restoreChildren(container, previous) {
|
|
823
|
+
for (let index = 0; index < previous.length; index++) {
|
|
824
|
+
const current = container.childNodes[index] ?? null;
|
|
825
|
+
if (current !== previous[index]) container.insertBefore(previous[index], current);
|
|
826
|
+
}
|
|
827
|
+
while (container.childNodes.length > previous.length) container.removeChild(container.lastChild);
|
|
828
|
+
}
|
|
829
|
+
/** Attribute names `props` renders on `el`. */
|
|
830
|
+
function renderedAttributes(el, props) {
|
|
831
|
+
const names = /* @__PURE__ */ new Set();
|
|
832
|
+
if (el.localName === "option") {
|
|
833
|
+
const parent = el.parentElement;
|
|
834
|
+
const select = parent?.localName === "optgroup" ? parent.parentElement : parent;
|
|
835
|
+
if (select?.localName === "select" && select.hasAttribute("value")) names.add("selected");
|
|
836
|
+
}
|
|
837
|
+
for (const key in props) {
|
|
838
|
+
if (isSkippedProp(key) || parseEventProp(key)) continue;
|
|
839
|
+
const value = props[key];
|
|
840
|
+
if (typeof value !== "function") {
|
|
841
|
+
if (value === null || value === void 0) continue;
|
|
842
|
+
if (value === false && !keepsFalseValue(key)) continue;
|
|
843
|
+
}
|
|
844
|
+
if (key === "className") {
|
|
845
|
+
names.add("class");
|
|
846
|
+
continue;
|
|
847
|
+
}
|
|
848
|
+
const name = key.startsWith("attr:") ? key.slice(ATTRIBUTE_PROP_PREFIX.length) : key;
|
|
849
|
+
names.add(getRenderedAttributeName(el, name));
|
|
850
|
+
}
|
|
851
|
+
return names;
|
|
852
|
+
}
|
|
853
|
+
/**
|
|
854
|
+
* Server attributes the client does not render are removed from an adopted
|
|
855
|
+
* element, so a mismatched attribute does not survive hydration.
|
|
856
|
+
*/
|
|
857
|
+
function removeUnrenderedAttributes(el, props, beforeRemove) {
|
|
858
|
+
const keep = renderedAttributes(el, props);
|
|
859
|
+
const removed = Array.from(el.attributes).filter((attribute) => !keep.has(attribute.name));
|
|
860
|
+
beforeRemove?.(removed);
|
|
861
|
+
for (const attribute of removed) el.removeAttribute(attribute.name);
|
|
862
|
+
}
|
|
863
|
+
//#endregion
|
|
864
|
+
//#region src/core/dom/dom-properties.ts
|
|
865
|
+
/**
|
|
866
|
+
* Per element, the props Askr wrote as DOM properties, mapped to the property
|
|
867
|
+
* name. A property cannot be "removed" like an attribute, so this is what lets
|
|
868
|
+
* an absent prop reset its property.
|
|
869
|
+
*/
|
|
870
|
+
const writtenDomProperties = /* @__PURE__ */ new WeakMap();
|
|
871
|
+
/**
|
|
872
|
+
* Property names `prop:` never assigns: raw HTML sinks (use
|
|
873
|
+
* `dangerouslySetInnerHTML`) and names that would change the prototype chain.
|
|
874
|
+
*/
|
|
875
|
+
const BLOCKED_PROPERTY_NAMES = /* @__PURE__ */ new Set([
|
|
876
|
+
"innerHTML",
|
|
877
|
+
"outerHTML",
|
|
878
|
+
"srcdoc",
|
|
879
|
+
"__proto__",
|
|
880
|
+
"constructor",
|
|
881
|
+
"prototype"
|
|
882
|
+
]);
|
|
883
|
+
function isBlockedPropertyName(name) {
|
|
884
|
+
if (!BLOCKED_PROPERTY_NAMES.has(name)) return false;
|
|
885
|
+
if (isDevelopmentEnvironment()) logger.warn(`[Askr] ${PROPERTY_PROP_PREFIX}${name} is ignored.`);
|
|
886
|
+
return true;
|
|
887
|
+
}
|
|
888
|
+
function isUrlPropertyName(name) {
|
|
889
|
+
const lower = name.toLowerCase();
|
|
890
|
+
return UNSAFE_URL_SCHEME_ATTRIBUTES.has(lower) || SCRIPT_URL_RESOURCE_ATTRIBUTES.has(lower);
|
|
891
|
+
}
|
|
892
|
+
function writeProperty(el, name, value) {
|
|
893
|
+
const host = el;
|
|
894
|
+
if (Object.is(host[name], value)) return;
|
|
895
|
+
host[name] = value;
|
|
896
|
+
}
|
|
897
|
+
/** Capture property and ownership state before a pass writes a DOM property. */
|
|
898
|
+
function captureDomPropertyUndo(el, key, value, tagName) {
|
|
899
|
+
const name = getDomPropertyName(tagName, key, value);
|
|
900
|
+
const written = writtenDomProperties.get(el);
|
|
901
|
+
const previousName = written?.get(key);
|
|
902
|
+
if (name === null && previousName === void 0) return null;
|
|
903
|
+
const names = /* @__PURE__ */ new Set();
|
|
904
|
+
if (name !== null && !BLOCKED_PROPERTY_NAMES.has(name)) names.add(name);
|
|
905
|
+
if (previousName !== void 0) names.add(previousName);
|
|
906
|
+
const host = el;
|
|
907
|
+
const properties = Array.from(names, (property) => ({
|
|
908
|
+
property,
|
|
909
|
+
descriptor: Object.getOwnPropertyDescriptor(host, property),
|
|
910
|
+
value: host[property]
|
|
911
|
+
}));
|
|
912
|
+
const attributeNames = /* @__PURE__ */ new Set();
|
|
913
|
+
for (const property of names) attributeNames.add(getRenderedAttributeName(el, property));
|
|
914
|
+
if (!key.startsWith("prop:")) attributeNames.add(getRenderedAttributeName(el, key));
|
|
915
|
+
const attributes = Array.from(attributeNames, (attribute) => ({
|
|
916
|
+
attribute,
|
|
917
|
+
value: el.getAttribute(attribute)
|
|
918
|
+
}));
|
|
919
|
+
const hadWrittenMap = written !== void 0;
|
|
920
|
+
const writtenBefore = written ? new Map(written) : null;
|
|
921
|
+
return () => {
|
|
922
|
+
const failures = [];
|
|
923
|
+
for (const { property, descriptor, value: before } of properties) try {
|
|
924
|
+
if (descriptor && "value" in descriptor) {
|
|
925
|
+
Object.defineProperty(host, property, descriptor);
|
|
926
|
+
continue;
|
|
927
|
+
}
|
|
928
|
+
if (!Reflect.set(host, property, before)) throw new TypeError(`Could not restore DOM property ${property}`);
|
|
929
|
+
if (descriptor) Object.defineProperty(host, property, descriptor);
|
|
930
|
+
else delete host[property];
|
|
931
|
+
} catch (error) {
|
|
932
|
+
failures.push(error);
|
|
933
|
+
}
|
|
934
|
+
for (const { attribute, value: before } of attributes) try {
|
|
935
|
+
if (before === null) el.removeAttribute(attribute);
|
|
936
|
+
else el.setAttribute(attribute, before);
|
|
937
|
+
} catch (error) {
|
|
938
|
+
failures.push(error);
|
|
939
|
+
}
|
|
940
|
+
try {
|
|
941
|
+
if (hadWrittenMap) writtenDomProperties.set(el, new Map(writtenBefore));
|
|
942
|
+
else writtenDomProperties.delete(el);
|
|
943
|
+
} catch (error) {
|
|
944
|
+
failures.push(error);
|
|
945
|
+
}
|
|
946
|
+
if (failures.length === 1) throw failures[0];
|
|
947
|
+
if (failures.length > 1) throw new AggregateError(failures, "DOM property rollback failed");
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
function hasDomPropertyWrite(el, key, value, tagName) {
|
|
951
|
+
return getDomPropertyName(tagName, key, value) !== null || writtenDomProperties.get(el)?.has(key) === true;
|
|
952
|
+
}
|
|
953
|
+
/**
|
|
954
|
+
* Put a property back to its default once Askr no longer sets it.
|
|
955
|
+
*
|
|
956
|
+
* - `muted`/`indeterminate` become `false`.
|
|
957
|
+
* - A property with a same-named attribute (`href`, `title`, `hidden`,
|
|
958
|
+
* `className` -> `class`, ...) has that attribute removed, which restores
|
|
959
|
+
* the element's own default.
|
|
960
|
+
* - An own property (an expando, or a value set before a custom element
|
|
961
|
+
* upgraded) is deleted.
|
|
962
|
+
* - Otherwise strings become `''` and booleans `false`. Numbers keep their
|
|
963
|
+
* value (there is no safe generic default), and anything else becomes
|
|
964
|
+
* `undefined`.
|
|
965
|
+
*/
|
|
966
|
+
function resetProperty(el, key, name) {
|
|
967
|
+
const host = el;
|
|
968
|
+
if (isKnownBooleanProperty(key)) {
|
|
969
|
+
writeProperty(el, name, false);
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
972
|
+
const attributeName = getRenderedAttributeName(el, name);
|
|
973
|
+
if (el.hasAttribute(attributeName)) {
|
|
974
|
+
el.removeAttribute(attributeName);
|
|
975
|
+
return;
|
|
976
|
+
}
|
|
977
|
+
if (Object.prototype.hasOwnProperty.call(host, name)) {
|
|
978
|
+
delete host[name];
|
|
979
|
+
return;
|
|
980
|
+
}
|
|
981
|
+
const current = host[name];
|
|
982
|
+
if (typeof current === "number") return;
|
|
983
|
+
writeProperty(el, name, typeof current === "string" ? "" : typeof current === "boolean" ? false : void 0);
|
|
984
|
+
}
|
|
985
|
+
function forgetProperty(el, key) {
|
|
986
|
+
const written = writtenDomProperties.get(el);
|
|
987
|
+
const name = written?.get(key);
|
|
988
|
+
if (name === void 0) return;
|
|
989
|
+
written.delete(key);
|
|
990
|
+
resetProperty(el, key, name);
|
|
991
|
+
}
|
|
992
|
+
/**
|
|
993
|
+
* Write a prop that targets a DOM property.
|
|
994
|
+
*
|
|
995
|
+
* Returns `true` when the prop is fully handled, and `false` when the caller
|
|
996
|
+
* must still run the attribute path: for attribute props, and for `muted`,
|
|
997
|
+
* which keeps its attribute alongside the property. A prop that was a
|
|
998
|
+
* property and is now an attribute (a custom element prop that went from an
|
|
999
|
+
* object to a string or `null`) has its property reset first.
|
|
1000
|
+
*/
|
|
1001
|
+
function applyDomPropertyProp(el, key, value, tagName) {
|
|
1002
|
+
const name = getDomPropertyName(tagName, key, value);
|
|
1003
|
+
if (name === null) {
|
|
1004
|
+
if (isCustomElementName(tagName)) forgetProperty(el, key);
|
|
1005
|
+
return false;
|
|
1006
|
+
}
|
|
1007
|
+
if (isBlockedPropertyName(name)) return true;
|
|
1008
|
+
if (value === null || value === void 0) {
|
|
1009
|
+
forgetProperty(el, key);
|
|
1010
|
+
return true;
|
|
1011
|
+
}
|
|
1012
|
+
let next = domPropertyValue(key, value);
|
|
1013
|
+
if (next !== null && next !== void 0 && isUrlPropertyName(name)) {
|
|
1014
|
+
const text = String(next);
|
|
1015
|
+
if (rejectUnsafeUrlAttribute(name, text)) {
|
|
1016
|
+
forgetProperty(el, key);
|
|
1017
|
+
return true;
|
|
1018
|
+
}
|
|
1019
|
+
if (!isCustomElementName(tagName)) next = text;
|
|
1020
|
+
}
|
|
1021
|
+
let written = writtenDomProperties.get(el);
|
|
1022
|
+
if (!written) {
|
|
1023
|
+
written = /* @__PURE__ */ new Map();
|
|
1024
|
+
writtenDomProperties.set(el, written);
|
|
1025
|
+
}
|
|
1026
|
+
if (!written.has(key)) {
|
|
1027
|
+
written.set(key, name);
|
|
1028
|
+
if (!key.startsWith("prop:") && !isKnownBooleanProperty(key)) {
|
|
1029
|
+
const attributeName = getRenderedAttributeName(el, key);
|
|
1030
|
+
if (el.hasAttribute(attributeName)) el.removeAttribute(attributeName);
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
writeProperty(el, name, next);
|
|
1034
|
+
return !propertyReflectsAttribute(key);
|
|
1035
|
+
}
|
|
1036
|
+
//#endregion
|
|
1037
|
+
//#region src/core/dom/prop-values.ts
|
|
1038
|
+
/**
|
|
1039
|
+
* How a scalar prop value is written to an element: class tokens, owned style
|
|
1040
|
+
* properties, form state, DOM properties, URL-guarded attributes, and the
|
|
1041
|
+
* `attr:`/`prop:` escape hatches. Shared semantics with SSR attribute
|
|
1042
|
+
* serialization; the renderer decides when these run.
|
|
1043
|
+
*/
|
|
1044
|
+
/** Props whose live DOM property must be synced alongside the attribute. */
|
|
1045
|
+
function isFormControlProp(key) {
|
|
1046
|
+
return key === "value" || key === "checked" || key === "selected";
|
|
1047
|
+
}
|
|
1048
|
+
/** Attribute text for a scalar prop, rendering HTML booleans bare. */
|
|
1049
|
+
function renderedScalarValue(el, key, value) {
|
|
1050
|
+
return value === true ? booleanAttributeValue(getRenderedAttributeName(el, key)) : String(value);
|
|
1051
|
+
}
|
|
1052
|
+
/**
|
|
1053
|
+
* The text to write for a scalar attribute prop, or `null` when the renderer
|
|
1054
|
+
* never writes it: unsafe URLs, and inline event handler or object values
|
|
1055
|
+
* requested through the `attr:` escape hatch (SSR skips those too). The value
|
|
1056
|
+
* is converted once, and the checked text is the text written, so a
|
|
1057
|
+
* `toString()` cannot pass the URL check and then return something else.
|
|
1058
|
+
*/
|
|
1059
|
+
function renderableAttributeText(el, key, value) {
|
|
1060
|
+
let name = key;
|
|
1061
|
+
if (key.startsWith("attr:")) {
|
|
1062
|
+
name = key.slice(ATTRIBUTE_PROP_PREFIX.length);
|
|
1063
|
+
if (value !== null && typeof value === "object" || name.slice(0, 2).toLowerCase() === "on") return null;
|
|
1064
|
+
}
|
|
1065
|
+
const text = renderedScalarValue(el, key, value);
|
|
1066
|
+
return rejectUnsafeUrlAttribute(name, text) ? null : text;
|
|
1067
|
+
}
|
|
1068
|
+
function isDangerousInnerHTMLPayload(value) {
|
|
1069
|
+
return value !== null && typeof value === "object" && "__html" in value;
|
|
1070
|
+
}
|
|
1071
|
+
function applyDangerousInnerHTMLValue(el, value) {
|
|
1072
|
+
if (!isDangerousInnerHTMLPayload(value)) return;
|
|
1073
|
+
if (isDevelopmentEnvironment()) logger.warn("[Askr] dangerouslySetInnerHTML is being used, which bypasses the framework's normal rendering and can introduce XSS vulnerabilities if the HTML is derived from untrusted input. Make sure the value is sanitized.");
|
|
1074
|
+
const html = value.__html;
|
|
1075
|
+
const nextHtml = html === null || html === void 0 ? "" : String(html);
|
|
1076
|
+
if (hasMatchingInnerHTML(el, nextHtml)) return;
|
|
1077
|
+
el.innerHTML = nextHtml;
|
|
1078
|
+
}
|
|
1079
|
+
/** Compare parsed HTML without replacing the live descendants. */
|
|
1080
|
+
function hasMatchingInnerHTML(el, html) {
|
|
1081
|
+
try {
|
|
1082
|
+
if (el.innerHTML === html) return true;
|
|
1083
|
+
if (el.childNodes.length === 0 || !el.namespaceURI) return false;
|
|
1084
|
+
const probe = el.ownerDocument.createElement("template").content.ownerDocument.createElementNS(el.namespaceURI, el.localName);
|
|
1085
|
+
probe.innerHTML = html;
|
|
1086
|
+
return probe.innerHTML === el.innerHTML;
|
|
1087
|
+
} catch {
|
|
1088
|
+
return false;
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
function applyFormControlProp(el, key, value, tagName) {
|
|
1092
|
+
if (key === "value") {
|
|
1093
|
+
const stringValue = String(value);
|
|
1094
|
+
if (tagNamesEqualIgnoreCase(tagName, "select")) {
|
|
1095
|
+
const select = el;
|
|
1096
|
+
const selectedValues = new Set(select.multiple && Array.isArray(value) ? value.map(String) : [stringValue]);
|
|
1097
|
+
let firstMatch = -1;
|
|
1098
|
+
for (let index = 0; index < select.options.length; index++) {
|
|
1099
|
+
const option = select.options[index];
|
|
1100
|
+
const selected = selectedValues.has(option.value) && (select.multiple || firstMatch < 0);
|
|
1101
|
+
if (selected && firstMatch < 0) firstMatch = index;
|
|
1102
|
+
if (option.hasAttribute("selected") !== selected) {
|
|
1103
|
+
if (selected) option.setAttribute("selected", "");
|
|
1104
|
+
else option.removeAttribute("selected");
|
|
1105
|
+
}
|
|
1106
|
+
if (select.multiple && option.selected !== selected) option.selected = selected;
|
|
1107
|
+
}
|
|
1108
|
+
if (!select.multiple && select.selectedIndex !== firstMatch) select.selectedIndex = firstMatch;
|
|
1109
|
+
} else if (tagNamesEqualIgnoreCase(tagName, "input") || tagNamesEqualIgnoreCase(tagName, "textarea")) {
|
|
1110
|
+
const control = el;
|
|
1111
|
+
if (control.value !== stringValue) control.value = stringValue;
|
|
1112
|
+
}
|
|
1113
|
+
if (el.getAttribute("value") !== stringValue) el.setAttribute("value", stringValue);
|
|
1114
|
+
return;
|
|
1115
|
+
}
|
|
1116
|
+
if (key === "selected") {
|
|
1117
|
+
const selected = Boolean(value);
|
|
1118
|
+
if (tagNamesEqualIgnoreCase(tagName, "option")) {
|
|
1119
|
+
const option = el;
|
|
1120
|
+
if (option.selected !== selected) option.selected = selected;
|
|
1121
|
+
}
|
|
1122
|
+
if (selected) {
|
|
1123
|
+
if (!el.hasAttribute("selected")) el.setAttribute("selected", "");
|
|
1124
|
+
} else if (el.hasAttribute("selected")) el.removeAttribute("selected");
|
|
1125
|
+
return;
|
|
1126
|
+
}
|
|
1127
|
+
if (key === "checked") {
|
|
1128
|
+
if (tagNamesEqualIgnoreCase(tagName, "input")) {
|
|
1129
|
+
const checked = Boolean(value);
|
|
1130
|
+
const input = el;
|
|
1131
|
+
if (input.checked !== checked) input.checked = checked;
|
|
1132
|
+
if (checked) {
|
|
1133
|
+
if (!el.hasAttribute("checked")) el.setAttribute("checked", "");
|
|
1134
|
+
} else if (el.hasAttribute("checked")) el.removeAttribute("checked");
|
|
1135
|
+
} else if (value) {
|
|
1136
|
+
if (!el.hasAttribute("checked")) el.setAttribute("checked", "");
|
|
1137
|
+
} else if (el.hasAttribute("checked")) el.removeAttribute("checked");
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
const IMPORTANT_SUFFIX = " !important";
|
|
1141
|
+
function readStyleEntry(style, propertyName) {
|
|
1142
|
+
return style.getPropertyValue(propertyName) + (style.getPropertyPriority(propertyName) ? IMPORTANT_SUFFIX : "");
|
|
1143
|
+
}
|
|
1144
|
+
function collectStyleEntries(style) {
|
|
1145
|
+
const entries = /* @__PURE__ */ new Map();
|
|
1146
|
+
for (let index = 0; index < style.length; index += 1) {
|
|
1147
|
+
const propertyName = style.item(index);
|
|
1148
|
+
entries.set(propertyName, readStyleEntry(style, propertyName));
|
|
1149
|
+
}
|
|
1150
|
+
return entries;
|
|
1151
|
+
}
|
|
1152
|
+
function normalizeStyleEntries(value) {
|
|
1153
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
1154
|
+
const entries = /* @__PURE__ */ new Map();
|
|
1155
|
+
for (const [key, entryValue] of Object.entries(value)) {
|
|
1156
|
+
if (entryValue === void 0 || entryValue === null || entryValue === false) continue;
|
|
1157
|
+
const propertyName = normalizeStylePropertyName(key);
|
|
1158
|
+
const safeValue = sanitizeCssValue(styleValueText(propertyName, entryValue));
|
|
1159
|
+
if (safeValue) entries.set(propertyName, safeValue);
|
|
1160
|
+
}
|
|
1161
|
+
return entries;
|
|
1162
|
+
}
|
|
1163
|
+
let scratchStyle = null;
|
|
1164
|
+
/** Style entries a prop value renders; `null` when it is not expressible. */
|
|
1165
|
+
function readStyleEntries(el, value) {
|
|
1166
|
+
if (value === null || value === void 0 || value === false) return /* @__PURE__ */ new Map();
|
|
1167
|
+
if (typeof value !== "string") return normalizeStyleEntries(value);
|
|
1168
|
+
scratchStyle ??= el.ownerDocument.createElement("div").style;
|
|
1169
|
+
scratchStyle.cssText = value;
|
|
1170
|
+
const entries = collectStyleEntries(scratchStyle);
|
|
1171
|
+
scratchStyle.cssText = "";
|
|
1172
|
+
return entries;
|
|
1173
|
+
}
|
|
1174
|
+
/**
|
|
1175
|
+
* Apply a `style` prop. `previousValue` is the value Askr last applied
|
|
1176
|
+
* (`null` when none): only the properties it owns are removed or rewritten, so
|
|
1177
|
+
* properties set by other code survive. When `previousValue` is `undefined`
|
|
1178
|
+
* the element's whole style is treated as Askr-owned.
|
|
1179
|
+
*/
|
|
1180
|
+
function applyStylePropValue(el, value, previousValue) {
|
|
1181
|
+
const style = el.style;
|
|
1182
|
+
const isEmpty = value === null || value === void 0 || value === false;
|
|
1183
|
+
if (!style) {
|
|
1184
|
+
if (isEmpty) {
|
|
1185
|
+
el.removeAttribute("style");
|
|
1186
|
+
return;
|
|
1187
|
+
}
|
|
1188
|
+
el.setAttribute("style", String(value));
|
|
1189
|
+
return;
|
|
1190
|
+
}
|
|
1191
|
+
if ((el.getAttribute("style") ?? "") === (isEmpty ? "" : value) || isEmpty && previousValue === null) return;
|
|
1192
|
+
if (previousValue === void 0 && (isEmpty || typeof value === "string")) {
|
|
1193
|
+
style.cssText = isEmpty ? "" : value;
|
|
1194
|
+
if (isEmpty) el.removeAttribute("style");
|
|
1195
|
+
return;
|
|
1196
|
+
}
|
|
1197
|
+
const nextEntries = readStyleEntries(el, value);
|
|
1198
|
+
if (!nextEntries) {
|
|
1199
|
+
style.cssText = String(value);
|
|
1200
|
+
return;
|
|
1201
|
+
}
|
|
1202
|
+
const previousEntries = (previousValue === void 0 ? null : previousValue === value ? nextEntries : readStyleEntries(el, previousValue)) ?? collectStyleEntries(style);
|
|
1203
|
+
let didWrite = false;
|
|
1204
|
+
for (const [propertyName] of previousEntries) {
|
|
1205
|
+
if (nextEntries.has(propertyName) || !style.getPropertyValue(propertyName)) continue;
|
|
1206
|
+
style.removeProperty(propertyName);
|
|
1207
|
+
didWrite = true;
|
|
1208
|
+
}
|
|
1209
|
+
for (const [propertyName, propertyValue] of nextEntries) {
|
|
1210
|
+
if (readStyleEntry(style, propertyName) === propertyValue) continue;
|
|
1211
|
+
const important = propertyValue.endsWith(IMPORTANT_SUFFIX);
|
|
1212
|
+
style.setProperty(propertyName, important ? propertyValue.slice(0, -11) : propertyValue, important ? "important" : "");
|
|
1213
|
+
didWrite = true;
|
|
1214
|
+
}
|
|
1215
|
+
if (isEmpty && style.length === 0) {
|
|
1216
|
+
el.removeAttribute("style");
|
|
1217
|
+
didWrite = true;
|
|
1218
|
+
}
|
|
1219
|
+
if (!didWrite) {}
|
|
1220
|
+
}
|
|
1221
|
+
function applyStaticScalarPropsToElement(el, props, tagName) {
|
|
1222
|
+
for (const key in props) {
|
|
1223
|
+
if (isSkippedProp(key)) continue;
|
|
1224
|
+
const value = props[key];
|
|
1225
|
+
if (applyDomPropertyProp(el, key, value, tagName)) continue;
|
|
1226
|
+
if (value === void 0 || value === null || value === false && !keepsFalseValue(key)) continue;
|
|
1227
|
+
if (key === "class" || key === "className") writeElementClassName(el, String(value));
|
|
1228
|
+
else if (key === "style") applyStylePropValue(el, value);
|
|
1229
|
+
else if (isFormControlProp(key)) applyFormControlProp(el, key, value, tagName);
|
|
1230
|
+
else if (key === "dangerouslySetInnerHTML") applyDangerousInnerHTMLValue(el, value);
|
|
1231
|
+
else {
|
|
1232
|
+
const text = renderableAttributeText(el, key, value);
|
|
1233
|
+
if (text === null) removeRenderedAttribute(el, key);
|
|
1234
|
+
else setRenderedAttribute(el, key, text);
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
const EMPTY_CLASS_TOKENS = [];
|
|
1239
|
+
/** Class tokens Askr last applied; `null` means unknown. */
|
|
1240
|
+
function previousClassTokens(previousValue) {
|
|
1241
|
+
return previousValue === null || previousValue === false ? EMPTY_CLASS_TOKENS : tokenizeClassValue(previousValue);
|
|
1242
|
+
}
|
|
1243
|
+
function tokenizeClassValue(value) {
|
|
1244
|
+
if (typeof value !== "string") return null;
|
|
1245
|
+
const trimmed = value.trim();
|
|
1246
|
+
if (trimmed.length === 0) return [];
|
|
1247
|
+
return trimmed.split(/\s+/);
|
|
1248
|
+
}
|
|
1249
|
+
function patchClassList(el, previousTokens, nextTokens) {
|
|
1250
|
+
if (previousTokens.length === nextTokens.length) {
|
|
1251
|
+
let identical = true;
|
|
1252
|
+
for (let index = 0; index < previousTokens.length; index += 1) if (previousTokens[index] !== nextTokens[index]) {
|
|
1253
|
+
identical = false;
|
|
1254
|
+
break;
|
|
1255
|
+
}
|
|
1256
|
+
if (identical) {
|
|
1257
|
+
for (const token of nextTokens) if (!el.classList.contains(token)) el.classList.add(token);
|
|
1258
|
+
return;
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
if (previousTokens.length === 0) {
|
|
1262
|
+
if (nextTokens.length === 0) return;
|
|
1263
|
+
el.classList.add(...nextTokens);
|
|
1264
|
+
return;
|
|
1265
|
+
}
|
|
1266
|
+
if (nextTokens.length === 0) {
|
|
1267
|
+
el.classList.remove(...previousTokens);
|
|
1268
|
+
return;
|
|
1269
|
+
}
|
|
1270
|
+
if (previousTokens.length === 1 && nextTokens.length === 1) {
|
|
1271
|
+
el.classList.remove(previousTokens[0]);
|
|
1272
|
+
el.classList.add(nextTokens[0]);
|
|
1273
|
+
return;
|
|
1274
|
+
}
|
|
1275
|
+
const nextSet = new Set(nextTokens);
|
|
1276
|
+
const previousSet = new Set(previousTokens);
|
|
1277
|
+
for (const token of previousTokens) if (!nextSet.has(token)) el.classList.remove(token);
|
|
1278
|
+
for (const token of nextTokens) if (!previousSet.has(token)) el.classList.add(token);
|
|
1279
|
+
}
|
|
1280
|
+
function applyClassPropValue(el, value, previousValue, descriptor) {
|
|
1281
|
+
const nextString = String(value);
|
|
1282
|
+
if (!descriptor && nextString.length > 0 && readElementClassName(el) === nextString) return;
|
|
1283
|
+
const nextTokens = tokenizeClassValue(nextString);
|
|
1284
|
+
const previousTokens = descriptor?.lastClassTokens ?? previousClassTokens(previousValue);
|
|
1285
|
+
if (nextTokens && previousTokens) {
|
|
1286
|
+
if (Object.is(value, previousValue)) {
|
|
1287
|
+
for (const token of nextTokens) if (!el.classList.contains(token)) el.classList.add(token);
|
|
1288
|
+
return;
|
|
1289
|
+
}
|
|
1290
|
+
patchClassList(el, previousTokens, nextTokens);
|
|
1291
|
+
dropEmptySvgClass(el);
|
|
1292
|
+
if (descriptor) descriptor.lastClassTokens = nextTokens;
|
|
1293
|
+
return;
|
|
1294
|
+
}
|
|
1295
|
+
writeElementClassName(el, nextString);
|
|
1296
|
+
if (descriptor) descriptor.lastClassTokens = nextTokens;
|
|
1297
|
+
}
|
|
1298
|
+
/** An SVG element renders no `class` attribute for an empty class list. */
|
|
1299
|
+
function dropEmptySvgClass(el) {
|
|
1300
|
+
if (isSVGDomElement(el) && el.getAttribute("class") === "") el.removeAttribute("class");
|
|
1301
|
+
}
|
|
1302
|
+
function applyScalarPropValue(el, key, value, tagName, previousValue, descriptor) {
|
|
1303
|
+
if (key === "dangerouslySetInnerHTML" && !isDangerousInnerHTMLPayload(value)) return;
|
|
1304
|
+
if (applyDomPropertyProp(el, key, value, tagName)) return;
|
|
1305
|
+
if (value === void 0 || value === null || value === false && !keepsFalseValue(key)) {
|
|
1306
|
+
if (key === "class" || key === "className") {
|
|
1307
|
+
const previousTokens = descriptor ? descriptor.lastClassTokens : previousClassTokens(previousValue);
|
|
1308
|
+
if (previousTokens === null) writeElementClassName(el, "");
|
|
1309
|
+
else if (previousTokens.length > 0) {
|
|
1310
|
+
el.classList.remove(...previousTokens);
|
|
1311
|
+
dropEmptySvgClass(el);
|
|
1312
|
+
}
|
|
1313
|
+
if (descriptor) descriptor.lastClassTokens = [];
|
|
1314
|
+
} else if (key === "value") applyFormControlProp(el, key, "", tagName);
|
|
1315
|
+
else if (key === "checked" || key === "selected") applyFormControlProp(el, key, false, tagName);
|
|
1316
|
+
else if (key === "style") applyStylePropValue(el, null, previousValue);
|
|
1317
|
+
else removeRenderedAttribute(el, key);
|
|
1318
|
+
return;
|
|
1319
|
+
}
|
|
1320
|
+
if (key === "class" || key === "className") applyClassPropValue(el, value, previousValue, descriptor);
|
|
1321
|
+
else if (key === "style") applyStylePropValue(el, value, previousValue);
|
|
1322
|
+
else if (isFormControlProp(key)) applyFormControlProp(el, key, value, tagName);
|
|
1323
|
+
else if (key === "dangerouslySetInnerHTML") applyDangerousInnerHTMLValue(el, value);
|
|
1324
|
+
else {
|
|
1325
|
+
const nextValue = renderableAttributeText(el, key, value);
|
|
1326
|
+
if (nextValue === null) {
|
|
1327
|
+
removeRenderedAttribute(el, key);
|
|
1328
|
+
return;
|
|
1329
|
+
}
|
|
1330
|
+
const attributeName = getRenderedAttributeName(el, key);
|
|
1331
|
+
if (el.getAttribute(attributeName) === nextValue) return;
|
|
1332
|
+
writeAttribute(el, attributeName, nextValue);
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
//#endregion
|
|
1336
|
+
//#region src/core/dom/refs.ts
|
|
1337
|
+
/**
|
|
1338
|
+
* Refs: a callback receives the element (then null on removal); an object
|
|
1339
|
+
* ref's `current` is assigned. A read-only object ref is ignored.
|
|
1340
|
+
*/
|
|
1341
|
+
function setRef(ref, value) {
|
|
1342
|
+
if (!ref) return;
|
|
1343
|
+
if (typeof ref === "function") {
|
|
1344
|
+
ref(value);
|
|
1345
|
+
return;
|
|
1346
|
+
}
|
|
1347
|
+
try {
|
|
1348
|
+
ref.current = value;
|
|
1349
|
+
} catch {}
|
|
1350
|
+
}
|
|
1351
|
+
//#endregion
|
|
1352
|
+
//#region src/core/dom/tree.ts
|
|
1353
|
+
const HOST = 0;
|
|
1354
|
+
const DYNAMIC = 4;
|
|
1355
|
+
/** Parents whose children are DOM children of one element. */
|
|
1356
|
+
function isContainer(parent) {
|
|
1357
|
+
return parent.kind === HOST || parent.kind === 5;
|
|
1358
|
+
}
|
|
1359
|
+
function containerElement(parent) {
|
|
1360
|
+
return parent.el;
|
|
1361
|
+
}
|
|
1362
|
+
/** The element that directly contains `parent`'s DOM. */
|
|
1363
|
+
function containerOf(parent) {
|
|
1364
|
+
for (let p = parent; p; p = p.parent) if (isContainer(p)) return containerElement(p);
|
|
1365
|
+
throw new Error("[Askr] Rendered node is not attached to a container.");
|
|
1366
|
+
}
|
|
1367
|
+
function firstDom(node) {
|
|
1368
|
+
const pending = [node];
|
|
1369
|
+
while (pending.length) {
|
|
1370
|
+
const current = pending.pop();
|
|
1371
|
+
switch (current.kind) {
|
|
1372
|
+
case HOST: return current.el;
|
|
1373
|
+
case 1:
|
|
1374
|
+
case 7: return current.node;
|
|
1375
|
+
default: for (let i = current.children.length - 1; i >= 0; i--) pending.push(current.children[i]);
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
return null;
|
|
1379
|
+
}
|
|
1380
|
+
function collectDom(node, out = []) {
|
|
1381
|
+
const pending = [node];
|
|
1382
|
+
while (pending.length) {
|
|
1383
|
+
const current = pending.pop();
|
|
1384
|
+
switch (current.kind) {
|
|
1385
|
+
case HOST:
|
|
1386
|
+
out.push(current.el);
|
|
1387
|
+
break;
|
|
1388
|
+
case 1:
|
|
1389
|
+
case 7:
|
|
1390
|
+
out.push(current.node);
|
|
1391
|
+
break;
|
|
1392
|
+
default: for (let i = current.children.length - 1; i >= 0; i--) pending.push(current.children[i]);
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
return out;
|
|
1396
|
+
}
|
|
1397
|
+
/**
|
|
1398
|
+
* The DOM node that content at the end of `parent` must be inserted before:
|
|
1399
|
+
* null for an element's children, otherwise the first DOM after `parent`.
|
|
1400
|
+
*/
|
|
1401
|
+
function endOf(parent) {
|
|
1402
|
+
if (parent.kind === 5) return parent.tail?.parentNode === parent.el ? parent.tail : null;
|
|
1403
|
+
if (isContainer(parent)) return null;
|
|
1404
|
+
return nextDomAfter(parent);
|
|
1405
|
+
}
|
|
1406
|
+
/** The first DOM node after `node`'s own DOM within its container. */
|
|
1407
|
+
function nextDomAfter(node) {
|
|
1408
|
+
let current = node;
|
|
1409
|
+
for (let parent = current.parent; parent; parent = current.parent) {
|
|
1410
|
+
const siblings = parent.children;
|
|
1411
|
+
for (let i = siblings.indexOf(current) + 1; i < siblings.length; i++) {
|
|
1412
|
+
const dom = firstDom(siblings[i]);
|
|
1413
|
+
if (dom) return dom;
|
|
1414
|
+
}
|
|
1415
|
+
if (parent.kind === 5) return parent.tail?.parentNode === parent.el ? parent.tail : null;
|
|
1416
|
+
if (isContainer(parent)) return null;
|
|
1417
|
+
current = parent;
|
|
1418
|
+
}
|
|
1419
|
+
return null;
|
|
1420
|
+
}
|
|
1421
|
+
//#endregion
|
|
1422
|
+
//#region src/core/dom/teardown.ts
|
|
1423
|
+
/**
|
|
1424
|
+
* Failures while tearing down removed content are reported after the update,
|
|
1425
|
+
* never thrown into it: the DOM change has already happened and is not
|
|
1426
|
+
* rolled back.
|
|
1427
|
+
*/
|
|
1428
|
+
function reportTeardown(errors) {
|
|
1429
|
+
if (errors.length === 1) reportUncaughtErrorLater(errors[0]);
|
|
1430
|
+
else if (errors.length > 1) reportUncaughtErrorLater(new AggregateError(errors, "Cleanup failed while removing content"));
|
|
1431
|
+
}
|
|
1432
|
+
//#endregion
|
|
1433
|
+
//#region src/core/dom/props.ts
|
|
1434
|
+
function isBinding(key, value) {
|
|
1435
|
+
return typeof value === "function" && key !== "ref" && key !== "dangerouslySetInnerHTML";
|
|
1436
|
+
}
|
|
1437
|
+
/** A `<select>` value can only select options that already exist. */
|
|
1438
|
+
function followsChildren(tag, key) {
|
|
1439
|
+
return key === "value" && tag === "select";
|
|
1440
|
+
}
|
|
1441
|
+
function isSimpleAttribute(tag, key, value) {
|
|
1442
|
+
return !isCustomElementName(tag) && !key.includes(":") && getDomPropertyName(tag, key, value) === null && key !== "class" && key !== "className" && key !== "style" && key !== "value" && key !== "checked" && key !== "selected" && key !== "dangerouslySetInnerHTML";
|
|
1443
|
+
}
|
|
1444
|
+
function isInputValue(tag, key) {
|
|
1445
|
+
return key === "value" && (tag === "input" || tag === "textarea");
|
|
1446
|
+
}
|
|
1447
|
+
function isBooleanControl(tag, key) {
|
|
1448
|
+
return tag === "input" && key === "checked" || tag === "option" && key === "selected";
|
|
1449
|
+
}
|
|
1450
|
+
/** Record the live attribute before applying a reversible scalar write. */
|
|
1451
|
+
function writeSimpleAttribute(pass, node, key, value, previous, beforeApply) {
|
|
1452
|
+
const { el, tag } = node;
|
|
1453
|
+
const name = getRenderedAttributeName(el, key);
|
|
1454
|
+
pass.op(() => {
|
|
1455
|
+
recordAttributeUndo(pass, el, name);
|
|
1456
|
+
try {
|
|
1457
|
+
beforeApply?.();
|
|
1458
|
+
applyScalarPropValue(el, key, value, tag, previous);
|
|
1459
|
+
} catch (error) {
|
|
1460
|
+
throw new CommitMutationError(error);
|
|
1461
|
+
}
|
|
1462
|
+
});
|
|
1463
|
+
}
|
|
1464
|
+
/** Record a reflected class or style write for an abortable commit. */
|
|
1465
|
+
function writeReflectedProp(pass, node, key, apply, beforeApply) {
|
|
1466
|
+
const { el } = node;
|
|
1467
|
+
const name = getRenderedAttributeName(el, key);
|
|
1468
|
+
pass.op(() => {
|
|
1469
|
+
recordAttributeUndo(pass, el, name);
|
|
1470
|
+
try {
|
|
1471
|
+
beforeApply?.();
|
|
1472
|
+
apply();
|
|
1473
|
+
} catch (error) {
|
|
1474
|
+
throw new CommitMutationError(error);
|
|
1475
|
+
}
|
|
1476
|
+
});
|
|
1477
|
+
}
|
|
1478
|
+
function recordAttributeUndo(pass, el, name) {
|
|
1479
|
+
const namespace = attributeNamespace(el.namespaceURI, name);
|
|
1480
|
+
const colon = name.indexOf(":");
|
|
1481
|
+
const localName = colon === -1 ? name : name.slice(colon + 1);
|
|
1482
|
+
const before = namespace ? el.getAttributeNS(namespace, localName) : el.getAttribute(name);
|
|
1483
|
+
pass.onReversibleCommit(() => {
|
|
1484
|
+
if (before === null) {
|
|
1485
|
+
if (namespace) el.removeAttributeNS(namespace, localName);
|
|
1486
|
+
else el.removeAttribute(name);
|
|
1487
|
+
} else if (namespace) el.setAttributeNS(namespace, name, before);
|
|
1488
|
+
else el.setAttribute(name, before);
|
|
1489
|
+
});
|
|
1490
|
+
}
|
|
1491
|
+
/** Restore exact child identities without needlessly disconnecting unchanged nodes. */
|
|
1492
|
+
function restoreElementChildren(el, children) {
|
|
1493
|
+
if (el.childNodes.length === children.length) {
|
|
1494
|
+
let unchanged = true;
|
|
1495
|
+
for (let index = 0; index < children.length; index += 1) if (el.childNodes[index] !== children[index]) {
|
|
1496
|
+
unchanged = false;
|
|
1497
|
+
break;
|
|
1498
|
+
}
|
|
1499
|
+
if (unchanged) return;
|
|
1500
|
+
}
|
|
1501
|
+
el.replaceChildren(...children);
|
|
1502
|
+
}
|
|
1503
|
+
/** Make property-backed scalar writes part of the pass rollback journal. */
|
|
1504
|
+
function writePropertyScalar(pass, node, key, value, apply) {
|
|
1505
|
+
pass.op(() => {
|
|
1506
|
+
try {
|
|
1507
|
+
const undo = captureDomPropertyUndo(node.el, key, value, node.tag);
|
|
1508
|
+
if (undo) pass.onReversibleCommit(undo);
|
|
1509
|
+
apply();
|
|
1510
|
+
} catch (error) {
|
|
1511
|
+
throw new CommitMutationError(error);
|
|
1512
|
+
}
|
|
1513
|
+
});
|
|
1514
|
+
}
|
|
1515
|
+
function queueScalarProp(pass, node, key, value, apply) {
|
|
1516
|
+
if (key === "dangerouslySetInnerHTML") {
|
|
1517
|
+
if (!isDangerousInnerHTMLPayload(value)) {
|
|
1518
|
+
pass.op(apply);
|
|
1519
|
+
return;
|
|
1520
|
+
}
|
|
1521
|
+
pass.op(() => {
|
|
1522
|
+
const children = Array.from(node.el.childNodes);
|
|
1523
|
+
pass.onReversibleCommit(() => restoreElementChildren(node.el, children));
|
|
1524
|
+
try {
|
|
1525
|
+
apply();
|
|
1526
|
+
} catch (error) {
|
|
1527
|
+
if (error instanceof CommitMutationError) throw error;
|
|
1528
|
+
throw new CommitMutationError(error);
|
|
1529
|
+
}
|
|
1530
|
+
});
|
|
1531
|
+
return;
|
|
1532
|
+
}
|
|
1533
|
+
if (hasDomPropertyWrite(node.el, key, value, node.tag)) writePropertyScalar(pass, node, key, value, apply);
|
|
1534
|
+
else pass.op(() => {
|
|
1535
|
+
recordAttributeUndo(pass, node.el, getRenderedAttributeName(node.el, key));
|
|
1536
|
+
try {
|
|
1537
|
+
apply();
|
|
1538
|
+
} catch (error) {
|
|
1539
|
+
if (error instanceof CommitMutationError) throw error;
|
|
1540
|
+
throw new CommitMutationError(error);
|
|
1541
|
+
}
|
|
1542
|
+
});
|
|
1543
|
+
}
|
|
1544
|
+
/** Remove a committed binding provisionally and restore it if the pass aborts. */
|
|
1545
|
+
function retireBindingForCommit(pass, node, key) {
|
|
1546
|
+
const binding = node.bindings?.get(key);
|
|
1547
|
+
if (!binding) return void 0;
|
|
1548
|
+
node.bindings.delete(key);
|
|
1549
|
+
pass.onReversibleCommit(() => {
|
|
1550
|
+
(node.bindings ??= /* @__PURE__ */ new Map()).set(key, binding);
|
|
1551
|
+
}, () => reportTeardown(binding.dispose()));
|
|
1552
|
+
return binding;
|
|
1553
|
+
}
|
|
1554
|
+
/** Restore or remove a binding-map entry when a provisional install aborts. */
|
|
1555
|
+
function recordBindingInstallUndo(pass, node, key, binding, previous) {
|
|
1556
|
+
pass.onReversibleCommit(() => {
|
|
1557
|
+
if (node.bindings?.get(key) !== binding) return;
|
|
1558
|
+
if (previous) node.bindings.set(key, previous);
|
|
1559
|
+
else {
|
|
1560
|
+
node.bindings.delete(key);
|
|
1561
|
+
if (node.bindings.size === 0) node.bindings = null;
|
|
1562
|
+
}
|
|
1563
|
+
});
|
|
1564
|
+
}
|
|
1565
|
+
/** Restore both the reflected attribute and the live form value on abort. */
|
|
1566
|
+
function writeInputValue(pass, node, value, previous) {
|
|
1567
|
+
const { el, tag } = node;
|
|
1568
|
+
pass.op(() => {
|
|
1569
|
+
recordInputValueUndo(pass, node);
|
|
1570
|
+
try {
|
|
1571
|
+
applyScalarPropValue(el, "value", value, tag, previous);
|
|
1572
|
+
} catch (error) {
|
|
1573
|
+
throw new CommitMutationError(error);
|
|
1574
|
+
}
|
|
1575
|
+
});
|
|
1576
|
+
}
|
|
1577
|
+
/** Restore a checkbox or option's live state and reflected attribute. */
|
|
1578
|
+
function writeBooleanControl(pass, node, key, value, previous) {
|
|
1579
|
+
const { el, tag } = node;
|
|
1580
|
+
pass.op(() => {
|
|
1581
|
+
recordBooleanControlUndo(pass, node, key);
|
|
1582
|
+
try {
|
|
1583
|
+
applyScalarPropValue(el, key, value, tag, previous);
|
|
1584
|
+
} catch (error) {
|
|
1585
|
+
throw new CommitMutationError(error);
|
|
1586
|
+
}
|
|
1587
|
+
});
|
|
1588
|
+
}
|
|
1589
|
+
function recordInputValueUndo(pass, node) {
|
|
1590
|
+
const control = node.el;
|
|
1591
|
+
const beforeValue = control.value;
|
|
1592
|
+
const beforeAttribute = node.el.getAttribute("value");
|
|
1593
|
+
pass.onReversibleCommit(() => {
|
|
1594
|
+
if (beforeAttribute === null) node.el.removeAttribute("value");
|
|
1595
|
+
else node.el.setAttribute("value", beforeAttribute);
|
|
1596
|
+
control.value = beforeValue;
|
|
1597
|
+
});
|
|
1598
|
+
}
|
|
1599
|
+
function recordBooleanControlUndo(pass, node, key) {
|
|
1600
|
+
const control = node.el;
|
|
1601
|
+
const beforeValue = control[key];
|
|
1602
|
+
const beforeAttribute = node.el.getAttribute(key);
|
|
1603
|
+
pass.onReversibleCommit(() => {
|
|
1604
|
+
if (beforeAttribute === null) node.el.removeAttribute(key);
|
|
1605
|
+
else node.el.setAttribute(key, beforeAttribute);
|
|
1606
|
+
control[key] = beforeValue;
|
|
1607
|
+
});
|
|
1608
|
+
}
|
|
1609
|
+
/** The last value each binding applied, for diffing its next write. */
|
|
1610
|
+
const appliedByBinding = /* @__PURE__ */ new WeakMap();
|
|
1611
|
+
function lastApplied(binding) {
|
|
1612
|
+
return binding ? appliedByBinding.get(binding) : void 0;
|
|
1613
|
+
}
|
|
1614
|
+
function setHandler(node, key, value) {
|
|
1615
|
+
const event = parseEventProp(key);
|
|
1616
|
+
if (typeof value === "function") setListener(node.listeners ??= /* @__PURE__ */ new Map(), node.el, key, event, value, node.owner);
|
|
1617
|
+
else if (node.listeners) removeListener(node.listeners, node.el, key);
|
|
1618
|
+
}
|
|
1619
|
+
function setHandlerForCommit(pass, node, key, value, previous) {
|
|
1620
|
+
pass.op(() => {
|
|
1621
|
+
pass.onReversibleCommit(() => setHandler(node, key, previous));
|
|
1622
|
+
try {
|
|
1623
|
+
setHandler(node, key, value);
|
|
1624
|
+
} catch (error) {
|
|
1625
|
+
throw new CommitMutationError(error);
|
|
1626
|
+
}
|
|
1627
|
+
});
|
|
1628
|
+
}
|
|
1629
|
+
/**
|
|
1630
|
+
* Write props to an element that has no committed props yet: a new, detached
|
|
1631
|
+
* element (written now) or an adopted server element (`adopted`: recorded as
|
|
1632
|
+
* commit operations).
|
|
1633
|
+
*/
|
|
1634
|
+
function applyInitialProps(pass, node, adopted = false) {
|
|
1635
|
+
const props = node.props;
|
|
1636
|
+
let scalars = null;
|
|
1637
|
+
const flushScalars = () => {
|
|
1638
|
+
if (!scalars) return;
|
|
1639
|
+
const batch = scalars;
|
|
1640
|
+
scalars = null;
|
|
1641
|
+
if (adopted) for (const key in batch) {
|
|
1642
|
+
const value = batch[key];
|
|
1643
|
+
if (isInputValue(node.tag, key)) writeInputValue(pass, node, value, void 0);
|
|
1644
|
+
else if (isBooleanControl(node.tag, key)) writeBooleanControl(pass, node, key, value, void 0);
|
|
1645
|
+
else if (isSimpleAttribute(node.tag, key, value)) writeSimpleAttribute(pass, node, key, value, void 0);
|
|
1646
|
+
else if (key === "class" || key === "className" || key === "style") writeReflectedProp(pass, node, key, () => applyStaticScalarPropsToElement(node.el, { [key]: value }, node.tag));
|
|
1647
|
+
else queueScalarProp(pass, node, key, value, () => applyStaticScalarPropsToElement(node.el, { [key]: value }, node.tag));
|
|
1648
|
+
}
|
|
1649
|
+
else applyStaticScalarPropsToElement(node.el, batch, node.tag);
|
|
1650
|
+
};
|
|
1651
|
+
for (const key in props) {
|
|
1652
|
+
if (isSkippedProp(key)) continue;
|
|
1653
|
+
const value = props[key];
|
|
1654
|
+
if (parseEventProp(key)) {
|
|
1655
|
+
flushScalars();
|
|
1656
|
+
if (adopted) setHandlerForCommit(pass, node, key, value, void 0);
|
|
1657
|
+
else {
|
|
1658
|
+
setHandler(node, key, value);
|
|
1659
|
+
recordUndo(() => setHandler(node, key, void 0));
|
|
1660
|
+
}
|
|
1661
|
+
} else if (isBinding(key, value)) {
|
|
1662
|
+
flushScalars();
|
|
1663
|
+
if (!followsChildren(node.tag, key)) bind(pass, node, key, value, void 0, !adopted);
|
|
1664
|
+
} else if (!followsChildren(node.tag, key)) (scalars ??= {})[key] = value;
|
|
1665
|
+
}
|
|
1666
|
+
flushScalars();
|
|
1667
|
+
}
|
|
1668
|
+
/** Write props that must follow the element's children. */
|
|
1669
|
+
function captureSelectUndo(select) {
|
|
1670
|
+
const beforeValue = select.getAttribute("value");
|
|
1671
|
+
const beforeIndex = select.selectedIndex;
|
|
1672
|
+
const beforeOptions = Array.from(select.options, (option) => ({
|
|
1673
|
+
option,
|
|
1674
|
+
selected: option.selected,
|
|
1675
|
+
attribute: option.hasAttribute("selected")
|
|
1676
|
+
}));
|
|
1677
|
+
return () => {
|
|
1678
|
+
if (beforeValue === null) select.removeAttribute("value");
|
|
1679
|
+
else select.setAttribute("value", beforeValue);
|
|
1680
|
+
for (const { option, attribute } of beforeOptions) if (attribute) option.setAttribute("selected", "");
|
|
1681
|
+
else option.removeAttribute("selected");
|
|
1682
|
+
if (select.multiple) for (const { option, selected } of beforeOptions) option.selected = selected;
|
|
1683
|
+
else select.selectedIndex = beforeIndex;
|
|
1684
|
+
};
|
|
1685
|
+
}
|
|
1686
|
+
function recordSelectUndo(pass, node) {
|
|
1687
|
+
pass.onReversibleCommit(captureSelectUndo(node.el));
|
|
1688
|
+
}
|
|
1689
|
+
function applyTrailingProps(pass, node, props, initial = false, adopted = false) {
|
|
1690
|
+
if (node.tag !== "select" || !("value" in props)) return;
|
|
1691
|
+
if (initial && adopted) pass.op(() => recordSelectUndo(pass, node));
|
|
1692
|
+
if (typeof props.value === "function") {
|
|
1693
|
+
if (initial) bind(pass, node, "value", props.value, void 0, !adopted);
|
|
1694
|
+
else node.bindings?.get("value")?.run();
|
|
1695
|
+
return;
|
|
1696
|
+
}
|
|
1697
|
+
const writeValue = () => {
|
|
1698
|
+
try {
|
|
1699
|
+
applyScalarPropValue(node.el, "value", props.value, node.tag, void 0);
|
|
1700
|
+
} catch (error) {
|
|
1701
|
+
if (initial && !adopted) throw error;
|
|
1702
|
+
throw new CommitMutationError(error);
|
|
1703
|
+
}
|
|
1704
|
+
};
|
|
1705
|
+
if (initial && adopted) pass.op(writeValue);
|
|
1706
|
+
else writeValue();
|
|
1707
|
+
}
|
|
1708
|
+
/** The select whose options include content under `parent`, if any. */
|
|
1709
|
+
function enclosingSelect(parent) {
|
|
1710
|
+
for (let p = parent; p; p = p.parent) {
|
|
1711
|
+
if (p.kind !== HOST) {
|
|
1712
|
+
if (p.kind === 5) return null;
|
|
1713
|
+
continue;
|
|
1714
|
+
}
|
|
1715
|
+
if (p.tag === "select") return p;
|
|
1716
|
+
if (p.tag !== "optgroup" && p.tag !== "option") return null;
|
|
1717
|
+
}
|
|
1718
|
+
return null;
|
|
1719
|
+
}
|
|
1720
|
+
/**
|
|
1721
|
+
* Re-apply a committed select's controlled value against its current
|
|
1722
|
+
* options. Its options changed without the select itself being patched.
|
|
1723
|
+
*/
|
|
1724
|
+
function resyncSelect(select) {
|
|
1725
|
+
if (select.dormant || !("value" in select.props)) return;
|
|
1726
|
+
const value = select.props.value;
|
|
1727
|
+
const resolved = typeof value === "function" ? readValue(value) : value;
|
|
1728
|
+
const undo = captureSelectUndo(select.el);
|
|
1729
|
+
try {
|
|
1730
|
+
applyScalarPropValue(select.el, "value", resolved, select.tag, void 0);
|
|
1731
|
+
} catch (error) {
|
|
1732
|
+
try {
|
|
1733
|
+
undo();
|
|
1734
|
+
} catch (rollbackError) {
|
|
1735
|
+
throw new AggregateError([error, rollbackError], "Controlled select resynchronization and rollback failed");
|
|
1736
|
+
}
|
|
1737
|
+
throw error;
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
/** Record the writes that turn `previous` into `next` on a committed element. */
|
|
1741
|
+
function patchProps(pass, node, previous, next) {
|
|
1742
|
+
const { el, tag } = node;
|
|
1743
|
+
for (const key in previous) {
|
|
1744
|
+
if (key in next || isSkippedProp(key)) continue;
|
|
1745
|
+
if (followsChildren(tag, key)) pass.op(() => recordSelectUndo(pass, node));
|
|
1746
|
+
const old = previous[key];
|
|
1747
|
+
if (parseEventProp(key)) setHandlerForCommit(pass, node, key, void 0, old);
|
|
1748
|
+
else if (!isBinding(key, old) && isInputValue(tag, key)) writeInputValue(pass, node, void 0, old);
|
|
1749
|
+
else if (!isBinding(key, old) && isBooleanControl(tag, key)) writeBooleanControl(pass, node, key, void 0, old);
|
|
1750
|
+
else if (!isBinding(key, old) && isSimpleAttribute(tag, key, void 0)) writeSimpleAttribute(pass, node, key, void 0, old);
|
|
1751
|
+
else if (!isBinding(key, old) && (key === "class" || key === "className" || key === "style")) writeReflectedProp(pass, node, key, () => applyScalarPropValue(el, key, void 0, tag, old));
|
|
1752
|
+
else if (isBinding(key, old) && isSimpleAttribute(tag, key, void 0)) {
|
|
1753
|
+
const binding = node.bindings?.get(key);
|
|
1754
|
+
writeSimpleAttribute(pass, node, key, void 0, lastApplied(binding), () => retireBindingForCommit(pass, node, key));
|
|
1755
|
+
} else if (isBinding(key, old) && (key === "class" || key === "className" || key === "style")) {
|
|
1756
|
+
const binding = node.bindings?.get(key);
|
|
1757
|
+
writeReflectedProp(pass, node, key, () => applyScalarPropValue(el, key, void 0, tag, lastApplied(binding)), () => retireBindingForCommit(pass, node, key));
|
|
1758
|
+
} else queueScalarProp(pass, node, key, void 0, () => {
|
|
1759
|
+
const binding = retireBindingForCommit(pass, node, key);
|
|
1760
|
+
const from = isBinding(key, old) ? lastApplied(binding) : old;
|
|
1761
|
+
try {
|
|
1762
|
+
applyScalarPropValue(el, key, void 0, tag, from);
|
|
1763
|
+
} catch (error) {
|
|
1764
|
+
if (!followsChildren(tag, key)) throw error;
|
|
1765
|
+
throw new CommitMutationError(error);
|
|
1766
|
+
}
|
|
1767
|
+
});
|
|
1768
|
+
}
|
|
1769
|
+
for (const key in next) {
|
|
1770
|
+
if (isSkippedProp(key)) continue;
|
|
1771
|
+
if (followsChildren(tag, key)) pass.op(() => recordSelectUndo(pass, node));
|
|
1772
|
+
const value = next[key];
|
|
1773
|
+
const old = previous[key];
|
|
1774
|
+
if (parseEventProp(key)) {
|
|
1775
|
+
if (value !== old) setHandlerForCommit(pass, node, key, value, old);
|
|
1776
|
+
continue;
|
|
1777
|
+
}
|
|
1778
|
+
if (isBinding(key, value)) {
|
|
1779
|
+
if (value === old && node.bindings?.has(key)) continue;
|
|
1780
|
+
const replaced = node.bindings?.get(key);
|
|
1781
|
+
const from = replaced ? lastApplied(replaced) : old;
|
|
1782
|
+
pass.op(() => retireBindingForCommit(pass, node, key));
|
|
1783
|
+
bind(pass, node, key, value, from, false);
|
|
1784
|
+
continue;
|
|
1785
|
+
}
|
|
1786
|
+
if (isBinding(key, old)) {
|
|
1787
|
+
const binding = node.bindings?.get(key);
|
|
1788
|
+
const from = lastApplied(binding);
|
|
1789
|
+
if (isSimpleAttribute(tag, key, value)) writeSimpleAttribute(pass, node, key, value, from, () => retireBindingForCommit(pass, node, key));
|
|
1790
|
+
else if (key === "class" || key === "className" || key === "style") writeReflectedProp(pass, node, key, () => applyScalarPropValue(el, key, value, tag, from), () => retireBindingForCommit(pass, node, key));
|
|
1791
|
+
else queueScalarProp(pass, node, key, value, () => {
|
|
1792
|
+
const current = retireBindingForCommit(pass, node, key);
|
|
1793
|
+
applyScalarPropValue(el, key, value, tag, lastApplied(current));
|
|
1794
|
+
});
|
|
1795
|
+
continue;
|
|
1796
|
+
}
|
|
1797
|
+
if (Object.is(value, old) && key === "dangerouslySetInnerHTML") continue;
|
|
1798
|
+
if (isInputValue(tag, key)) writeInputValue(pass, node, value, old);
|
|
1799
|
+
else if (isBooleanControl(tag, key)) writeBooleanControl(pass, node, key, value, old);
|
|
1800
|
+
else if (isSimpleAttribute(tag, key, value)) writeSimpleAttribute(pass, node, key, value, old);
|
|
1801
|
+
else if (key === "class" || key === "className" || key === "style") writeReflectedProp(pass, node, key, () => applyScalarPropValue(el, key, value, tag, old));
|
|
1802
|
+
else queueScalarProp(pass, node, key, value, () => {
|
|
1803
|
+
try {
|
|
1804
|
+
applyScalarPropValue(el, key, value, tag, old);
|
|
1805
|
+
} catch (error) {
|
|
1806
|
+
if (!followsChildren(tag, key)) throw error;
|
|
1807
|
+
throw new CommitMutationError(error);
|
|
1808
|
+
}
|
|
1809
|
+
});
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
function bind(pass, node, key, read, previousValue, fresh) {
|
|
1813
|
+
const { el, tag } = node;
|
|
1814
|
+
let last = previousValue;
|
|
1815
|
+
let installing = true;
|
|
1816
|
+
let propertyBacked = false;
|
|
1817
|
+
const binding = new Computation(node.owner, () => {
|
|
1818
|
+
try {
|
|
1819
|
+
const value = key.startsWith("prop:") ? read() : readValue(read);
|
|
1820
|
+
propertyBacked = hasDomPropertyWrite(el, key, value, tag);
|
|
1821
|
+
const propertyUndo = propertyBacked ? captureDomPropertyUndo(el, key, value, tag) : null;
|
|
1822
|
+
if (propertyUndo && installing && !fresh) pass.onReversibleCommit(propertyUndo);
|
|
1823
|
+
try {
|
|
1824
|
+
applyScalarPropValue(el, key, value, tag, last);
|
|
1825
|
+
} catch (error) {
|
|
1826
|
+
if (propertyUndo && (fresh || !installing)) try {
|
|
1827
|
+
propertyUndo();
|
|
1828
|
+
} catch (rollbackError) {
|
|
1829
|
+
throw new AggregateError([error, rollbackError], "Bound DOM property write and rollback failed");
|
|
1830
|
+
}
|
|
1831
|
+
throw error;
|
|
1832
|
+
}
|
|
1833
|
+
const changed = !Object.is(last, value);
|
|
1834
|
+
last = value;
|
|
1835
|
+
if (changed && tag === "option" && key === "value") {
|
|
1836
|
+
const select = enclosingSelect(node.parent);
|
|
1837
|
+
if (select) resyncSelect(select);
|
|
1838
|
+
}
|
|
1839
|
+
appliedByBinding.set(binding, value);
|
|
1840
|
+
} catch (error) {
|
|
1841
|
+
if (!deliverToBoundary(node.owner, error)) throw error;
|
|
1842
|
+
}
|
|
1843
|
+
}, effectScheduler("effect", (node.owner?.depth ?? 0) + 1), null, "previous");
|
|
1844
|
+
pass.own(binding);
|
|
1845
|
+
const install = () => {
|
|
1846
|
+
const previous = node.bindings?.get(key);
|
|
1847
|
+
(node.bindings ??= /* @__PURE__ */ new Map()).set(key, binding);
|
|
1848
|
+
const reversibleAttribute = isSimpleAttribute(tag, key, void 0) || key === "class" || key === "className" || key === "style";
|
|
1849
|
+
const reversibleControl = isInputValue(tag, key) || isBooleanControl(tag, key);
|
|
1850
|
+
if (!fresh) {
|
|
1851
|
+
recordBindingInstallUndo(pass, node, key, binding, previous);
|
|
1852
|
+
if (reversibleAttribute) recordAttributeUndo(pass, el, getRenderedAttributeName(el, key));
|
|
1853
|
+
else if (isInputValue(tag, key)) recordInputValueUndo(pass, node);
|
|
1854
|
+
else if (isBooleanControl(tag, key)) recordBooleanControlUndo(pass, node, key);
|
|
1855
|
+
}
|
|
1856
|
+
try {
|
|
1857
|
+
binding.run();
|
|
1858
|
+
if (binding._hasError) throw binding._error;
|
|
1859
|
+
} catch (error) {
|
|
1860
|
+
if (!fresh && (reversibleAttribute || reversibleControl || propertyBacked)) throw new CommitMutationError(error);
|
|
1861
|
+
throw error;
|
|
1862
|
+
} finally {
|
|
1863
|
+
installing = false;
|
|
1864
|
+
}
|
|
1865
|
+
};
|
|
1866
|
+
if (fresh) install();
|
|
1867
|
+
else pass.op(install);
|
|
1868
|
+
}
|
|
1869
|
+
/** Point the element's ref at it once the pass commits. */
|
|
1870
|
+
function attachRef(pass, node, previous) {
|
|
1871
|
+
const ref = node.props.ref;
|
|
1872
|
+
if (ref === previous) return;
|
|
1873
|
+
const el = node.el;
|
|
1874
|
+
pass.after(() => {
|
|
1875
|
+
if (previous) setRef(previous, null);
|
|
1876
|
+
if (ref) setRef(ref, el);
|
|
1877
|
+
});
|
|
1878
|
+
}
|
|
1879
|
+
/** End a removed element's bindings, listeners, and ref. */
|
|
1880
|
+
function releaseProps(node, errors) {
|
|
1881
|
+
if (node.bindings) {
|
|
1882
|
+
for (const binding of node.bindings.values()) binding.dispose(errors);
|
|
1883
|
+
node.bindings = null;
|
|
1884
|
+
}
|
|
1885
|
+
if (node.listeners) removeAllListeners(node.listeners, node.el, errors);
|
|
1886
|
+
const ref = node.props.ref;
|
|
1887
|
+
if (ref) try {
|
|
1888
|
+
setRef(ref, null);
|
|
1889
|
+
} catch (error) {
|
|
1890
|
+
errors.push(error);
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
//#endregion
|
|
1894
|
+
//#region src/core/dom/reconcile.ts
|
|
1895
|
+
function withOwner(ctx, owner) {
|
|
1896
|
+
return owner === ctx.owner ? ctx : {
|
|
1897
|
+
...ctx,
|
|
1898
|
+
owner
|
|
1899
|
+
};
|
|
1900
|
+
}
|
|
1901
|
+
function typeOf(node) {
|
|
1902
|
+
switch (node.kind) {
|
|
1903
|
+
case HOST: return node.tag;
|
|
1904
|
+
case 2: return node.instance.fn;
|
|
1905
|
+
case 7: return node.node;
|
|
1906
|
+
default: return node.kind;
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
function matches(node, child) {
|
|
1910
|
+
return node.kind === child.kind && typeOf(node) === descriptorType(child);
|
|
1911
|
+
}
|
|
1912
|
+
/**
|
|
1913
|
+
* Reconcile `parent`'s children against `value`.
|
|
1914
|
+
*
|
|
1915
|
+
* With `fresh`, `parent` is new: its children are built and, for an element,
|
|
1916
|
+
* appended to it directly. Otherwise the committed list is replaced by an
|
|
1917
|
+
* operation. Returns the new child list.
|
|
1918
|
+
*/
|
|
1919
|
+
function reconcileChildren(ctx, parent, value, fresh, checkKeys = true) {
|
|
1920
|
+
const next = normalizeChildren(value);
|
|
1921
|
+
if (fresh) {
|
|
1922
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1923
|
+
for (const child of next) {
|
|
1924
|
+
if (child.key === void 0) continue;
|
|
1925
|
+
if (seen.has(child.key)) throw new Error(`[Askr] Duplicate key ${String(child.key)} among siblings.`);
|
|
1926
|
+
seen.add(child.key);
|
|
1927
|
+
}
|
|
1928
|
+
const result = next.map((child) => ctx.nodes.create(ctx, parent, child));
|
|
1929
|
+
if (parent.kind === HOST) {
|
|
1930
|
+
if (ctx.hydrate?.container === parent.el) {
|
|
1931
|
+
const el = parent.el;
|
|
1932
|
+
ctx.pass.op(() => syncChildren(ctx.pass, el, result.flatMap((node) => collectDom(node))));
|
|
1933
|
+
} else for (const node of result) for (const dom of collectDom(node)) parent.el.appendChild(dom);
|
|
1934
|
+
}
|
|
1935
|
+
return result;
|
|
1936
|
+
}
|
|
1937
|
+
const previous = parent.children;
|
|
1938
|
+
if (next.length === previous.length) {
|
|
1939
|
+
let samePositions = true;
|
|
1940
|
+
for (let i = 0; i < next.length; i++) if (next[i].key !== previous[i].key || !matches(previous[i], next[i])) {
|
|
1941
|
+
samePositions = false;
|
|
1942
|
+
break;
|
|
1943
|
+
}
|
|
1944
|
+
if (samePositions) {
|
|
1945
|
+
const result = previous.slice();
|
|
1946
|
+
for (let i = 0; i < next.length; i++) ctx.nodes.patch(ctx, previous[i], next[i]);
|
|
1947
|
+
return result;
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
const slot = ctx.pass.reserve();
|
|
1951
|
+
const index = indexChildren(previous);
|
|
1952
|
+
const used = new Uint8Array(previous.length);
|
|
1953
|
+
const sources = new Int32Array(next.length).fill(-1);
|
|
1954
|
+
const result = [];
|
|
1955
|
+
const cursors = /* @__PURE__ */ new Map();
|
|
1956
|
+
let seen = null;
|
|
1957
|
+
for (let i = 0; i < next.length; i++) {
|
|
1958
|
+
const child = next[i];
|
|
1959
|
+
let match = -1;
|
|
1960
|
+
if (child.key !== void 0) {
|
|
1961
|
+
seen ??= /* @__PURE__ */ new Set();
|
|
1962
|
+
if (seen.has(child.key)) throw new Error(`[Askr] Duplicate key ${String(child.key)} among siblings.`);
|
|
1963
|
+
seen.add(child.key);
|
|
1964
|
+
const at = index.keyed?.get(child.key);
|
|
1965
|
+
if (at !== void 0 && !used[at] && matches(previous[at], child)) match = at;
|
|
1966
|
+
} else {
|
|
1967
|
+
const type = descriptorType(child);
|
|
1968
|
+
const bucket = index.unkeyed?.get(type);
|
|
1969
|
+
if (bucket) {
|
|
1970
|
+
let cursor = cursors.get(type) ?? 0;
|
|
1971
|
+
while (cursor < bucket.length && used[bucket[cursor]]) cursor++;
|
|
1972
|
+
if (cursor < bucket.length && matches(previous[bucket[cursor]], child)) {
|
|
1973
|
+
match = bucket[cursor];
|
|
1974
|
+
cursor++;
|
|
1975
|
+
}
|
|
1976
|
+
cursors.set(type, cursor);
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
if (match >= 0) {
|
|
1980
|
+
used[match] = 1;
|
|
1981
|
+
sources[i] = match;
|
|
1982
|
+
ctx.nodes.patch(ctx, previous[match], child);
|
|
1983
|
+
result.push(previous[match]);
|
|
1984
|
+
} else result.push(ctx.nodes.create(ctx, parent, child));
|
|
1985
|
+
}
|
|
1986
|
+
const removed = previous.filter((_, i) => !used[i]);
|
|
1987
|
+
if (removed.length === 0 && result.length === previous.length && result.every((node, index) => node === previous[index])) return result;
|
|
1988
|
+
const stable = longestIncreasingSubsequence(sources);
|
|
1989
|
+
const nodes = ctx.nodes;
|
|
1990
|
+
nodes.listChanged?.(ctx, parent);
|
|
1991
|
+
ctx.pass.fill(slot, () => {
|
|
1992
|
+
const positions = [];
|
|
1993
|
+
const record = (node) => {
|
|
1994
|
+
positions.push({
|
|
1995
|
+
node,
|
|
1996
|
+
parent: node.parentNode,
|
|
1997
|
+
next: node.nextSibling
|
|
1998
|
+
});
|
|
1999
|
+
};
|
|
2000
|
+
ctx.pass.onReversibleCommit(() => {
|
|
2001
|
+
for (let index = positions.length - 1; index >= 0; index--) {
|
|
2002
|
+
const { node, parent, next } = positions[index];
|
|
2003
|
+
if (!parent) node.parentNode?.removeChild(node);
|
|
2004
|
+
else if (node.parentNode !== parent || node.nextSibling !== next) parent.insertBefore(node, next);
|
|
2005
|
+
}
|
|
2006
|
+
parent.children = previous;
|
|
2007
|
+
}, () => {
|
|
2008
|
+
const errors = [];
|
|
2009
|
+
for (const node of removed) nodes.release(node, errors);
|
|
2010
|
+
reportTeardown(errors);
|
|
2011
|
+
});
|
|
2012
|
+
try {
|
|
2013
|
+
placeChildren(parent, result, sources, stable, record);
|
|
2014
|
+
for (const node of removed) for (const dom of collectDom(node)) {
|
|
2015
|
+
record(dom);
|
|
2016
|
+
dom.parentNode?.removeChild(dom);
|
|
2017
|
+
}
|
|
2018
|
+
} catch (error) {
|
|
2019
|
+
throw new CommitMutationError(error);
|
|
2020
|
+
}
|
|
2021
|
+
parent.children = result;
|
|
2022
|
+
for (const node of result) if (node.parent !== parent) node.parent = parent;
|
|
2023
|
+
});
|
|
2024
|
+
return result;
|
|
2025
|
+
}
|
|
2026
|
+
function indexChildren(children) {
|
|
2027
|
+
const index = {
|
|
2028
|
+
keyed: null,
|
|
2029
|
+
unkeyed: null
|
|
2030
|
+
};
|
|
2031
|
+
for (let i = 0; i < children.length; i++) {
|
|
2032
|
+
const node = children[i];
|
|
2033
|
+
if (node.key !== void 0) {
|
|
2034
|
+
(index.keyed ??= /* @__PURE__ */ new Map()).set(node.key, i);
|
|
2035
|
+
continue;
|
|
2036
|
+
}
|
|
2037
|
+
const type = typeOf(node);
|
|
2038
|
+
const bucket = (index.unkeyed ??= /* @__PURE__ */ new Map()).get(type);
|
|
2039
|
+
if (bucket) bucket.push(i);
|
|
2040
|
+
else index.unkeyed.set(type, [i]);
|
|
2041
|
+
}
|
|
2042
|
+
return index;
|
|
2043
|
+
}
|
|
2044
|
+
/**
|
|
2045
|
+
* Put `children`'s DOM in order, walking backwards from the end so every
|
|
2046
|
+
* insertion has a settled reference node. Reused children on the stable
|
|
2047
|
+
* subsequence stay where they are.
|
|
2048
|
+
*/
|
|
2049
|
+
function placeChildren(parent, children, sources, stable, record) {
|
|
2050
|
+
const container = containerOf(parent);
|
|
2051
|
+
let next = endOf(parent);
|
|
2052
|
+
for (let i = children.length - 1; i >= 0; i--) {
|
|
2053
|
+
const node = children[i];
|
|
2054
|
+
if (sources[i] >= 0 && stable.has(i)) {
|
|
2055
|
+
next = firstDom(node) ?? next;
|
|
2056
|
+
continue;
|
|
2057
|
+
}
|
|
2058
|
+
const nodes = collectDom(node);
|
|
2059
|
+
for (let j = nodes.length - 1; j >= 0; j--) {
|
|
2060
|
+
const dom = nodes[j];
|
|
2061
|
+
if (dom.parentNode !== container || dom.nextSibling !== next) {
|
|
2062
|
+
record(dom);
|
|
2063
|
+
container.insertBefore(dom, next);
|
|
2064
|
+
}
|
|
2065
|
+
next = dom;
|
|
2066
|
+
}
|
|
2067
|
+
}
|
|
2068
|
+
}
|
|
2069
|
+
/** Indices of a longest increasing run of `sources` values (ignoring -1). */
|
|
2070
|
+
function longestIncreasingSubsequence(sources) {
|
|
2071
|
+
const tails = [];
|
|
2072
|
+
const previous = new Int32Array(sources.length).fill(-1);
|
|
2073
|
+
for (let i = 0; i < sources.length; i++) {
|
|
2074
|
+
const value = sources[i];
|
|
2075
|
+
if (value < 0) continue;
|
|
2076
|
+
let low = 0;
|
|
2077
|
+
let high = tails.length;
|
|
2078
|
+
while (low < high) {
|
|
2079
|
+
const mid = low + high >> 1;
|
|
2080
|
+
if (sources[tails[mid]] < value) low = mid + 1;
|
|
2081
|
+
else high = mid;
|
|
2082
|
+
}
|
|
2083
|
+
if (low > 0) previous[i] = tails[low - 1];
|
|
2084
|
+
tails[low] = i;
|
|
2085
|
+
}
|
|
2086
|
+
const result = /* @__PURE__ */ new Set();
|
|
2087
|
+
for (let i = tails.length ? tails[tails.length - 1] : -1; i >= 0; i = previous[i]) result.add(i);
|
|
2088
|
+
return result;
|
|
2089
|
+
}
|
|
2090
|
+
//#endregion
|
|
2091
|
+
//#region src/core/dom/nodes.ts
|
|
2092
|
+
const SVG_NS = "http://www.w3.org/2000/svg";
|
|
2093
|
+
const MATHML_NS = "http://www.w3.org/1998/Math/MathML";
|
|
2094
|
+
/** Namespace for children of an element in namespace `ns` with tag `tag`. */
|
|
2095
|
+
function childNamespace(tag, ns) {
|
|
2096
|
+
if (tag === "svg") return SVG_NS;
|
|
2097
|
+
if (tag === "math") return MATHML_NS;
|
|
2098
|
+
if (ns === SVG_NS && tag === "foreignObject") return null;
|
|
2099
|
+
return ns;
|
|
2100
|
+
}
|
|
2101
|
+
function elementNamespace(tag, parentNs) {
|
|
2102
|
+
if (tag === "svg") return SVG_NS;
|
|
2103
|
+
if (tag === "math") return MATHML_NS;
|
|
2104
|
+
return parentNs;
|
|
2105
|
+
}
|
|
2106
|
+
/** Namespace new children of `parent` are created in. */
|
|
2107
|
+
function namespaceAt(parent) {
|
|
2108
|
+
for (let p = parent; p; p = p.parent) {
|
|
2109
|
+
if (p.kind === HOST) return childNamespace(p.tag, namespaceOf(p.el));
|
|
2110
|
+
if (p.kind === 5) return childNamespace(p.el.localName, namespaceOf(p.el));
|
|
2111
|
+
}
|
|
2112
|
+
return null;
|
|
2113
|
+
}
|
|
2114
|
+
function createRenderContext(pass, owner, ns, hydrate = null) {
|
|
2115
|
+
return {
|
|
2116
|
+
pass,
|
|
2117
|
+
owner,
|
|
2118
|
+
ns,
|
|
2119
|
+
nodes: domNodes,
|
|
2120
|
+
hydrate
|
|
2121
|
+
};
|
|
2122
|
+
}
|
|
2123
|
+
/** Schedules a standalone update of a function child (see `updates`). */
|
|
2124
|
+
let scheduleDynamicUpdate = () => {};
|
|
2125
|
+
function setDynamicUpdateScheduler(schedule) {
|
|
2126
|
+
scheduleDynamicUpdate = schedule;
|
|
2127
|
+
}
|
|
2128
|
+
function nearestInstance(owner) {
|
|
2129
|
+
for (let o = owner; o; o = o.parent) if (o instanceof ComponentInstance) return o;
|
|
2130
|
+
return null;
|
|
2131
|
+
}
|
|
2132
|
+
function createHost(ctx, parent, key, tag, props) {
|
|
2133
|
+
const ns = elementNamespace(tag, ctx.ns);
|
|
2134
|
+
const hydrate = ctx.hydrate;
|
|
2135
|
+
const adopted = hydrate ? hydrate.cursor.claimElement(hydrate.container, tag, ns) : null;
|
|
2136
|
+
const el = adopted ?? (ns ? document.createElementNS(ns, tag) : document.createElement(tag));
|
|
2137
|
+
const node = {
|
|
2138
|
+
kind: HOST,
|
|
2139
|
+
parent,
|
|
2140
|
+
key,
|
|
2141
|
+
el,
|
|
2142
|
+
tag,
|
|
2143
|
+
props,
|
|
2144
|
+
children: [],
|
|
2145
|
+
bindings: null,
|
|
2146
|
+
listeners: null,
|
|
2147
|
+
owner: nearestInstance(ctx.owner)
|
|
2148
|
+
};
|
|
2149
|
+
if (adopted?.hasAttribute(SKIP_HYDRATE)) {
|
|
2150
|
+
ctx.pass.hasDormantPortalWriter ||= containsPortalWriterAnchor(adopted);
|
|
2151
|
+
node.dormant = {
|
|
2152
|
+
owner: ctx.owner,
|
|
2153
|
+
ns
|
|
2154
|
+
};
|
|
2155
|
+
ctx.pass.op(() => {
|
|
2156
|
+
const previous = dormantHosts.get(adopted);
|
|
2157
|
+
ctx.pass.onReversibleCommit(() => {
|
|
2158
|
+
if (previous) dormantHosts.set(adopted, previous);
|
|
2159
|
+
else dormantHosts.delete(adopted);
|
|
2160
|
+
});
|
|
2161
|
+
dormantHosts.set(adopted, node);
|
|
2162
|
+
});
|
|
2163
|
+
return node;
|
|
2164
|
+
}
|
|
2165
|
+
applyInitialProps(ctx.pass, node, adopted !== null);
|
|
2166
|
+
if (adopted) ctx.pass.op(() => {
|
|
2167
|
+
try {
|
|
2168
|
+
removeUnrenderedAttributes(adopted, props, (attributes) => {
|
|
2169
|
+
ctx.pass.onReversibleCommit(() => {
|
|
2170
|
+
for (const attribute of attributes) adopted.setAttributeNode(attribute);
|
|
2171
|
+
});
|
|
2172
|
+
});
|
|
2173
|
+
} catch (error) {
|
|
2174
|
+
throw new CommitMutationError(error);
|
|
2175
|
+
}
|
|
2176
|
+
});
|
|
2177
|
+
if (!isDangerousInnerHTMLPayload(props.dangerouslySetInnerHTML)) node.children = reconcileChildren({
|
|
2178
|
+
...ctx,
|
|
2179
|
+
ns: childNamespace(tag, ns),
|
|
2180
|
+
hydrate: adopted && hydrate ? {
|
|
2181
|
+
cursor: hydrate.cursor,
|
|
2182
|
+
container: adopted
|
|
2183
|
+
} : null
|
|
2184
|
+
}, node, props.children, true);
|
|
2185
|
+
applyTrailingProps(ctx.pass, node, props, true, adopted !== null);
|
|
2186
|
+
attachRef(ctx.pass, node, void 0);
|
|
2187
|
+
return node;
|
|
2188
|
+
}
|
|
2189
|
+
const SKIP_HYDRATE = "data-skip-hydrate";
|
|
2190
|
+
const dormantHosts = /* @__PURE__ */ new WeakMap();
|
|
2191
|
+
function containsPortalWriterAnchor(root) {
|
|
2192
|
+
const pending = Array.from(root.childNodes);
|
|
2193
|
+
while (pending.length) {
|
|
2194
|
+
const node = pending.pop();
|
|
2195
|
+
if (isSSRPortalWriterAnchor(node)) return true;
|
|
2196
|
+
for (const child of node.childNodes) pending.push(child);
|
|
2197
|
+
}
|
|
2198
|
+
return false;
|
|
2199
|
+
}
|
|
2200
|
+
/** The dormant host adopted for `el`, if it has not been activated. */
|
|
2201
|
+
function dormantHostFor(el) {
|
|
2202
|
+
const node = dormantHosts.get(el);
|
|
2203
|
+
return node?.dormant ? node : null;
|
|
2204
|
+
}
|
|
2205
|
+
/**
|
|
2206
|
+
* Hydrate a dormant host in place: claim its server children, then apply
|
|
2207
|
+
* its props, listeners, and ref. The marker is removed when the pass
|
|
2208
|
+
* commits, so a failed activation leaves it for a retry.
|
|
2209
|
+
*/
|
|
2210
|
+
function hydrateDormantHost(pass, node) {
|
|
2211
|
+
const dormant = node.dormant;
|
|
2212
|
+
const el = node.el;
|
|
2213
|
+
const ctx = createRenderContext(pass, dormant.owner, dormant.ns, {
|
|
2214
|
+
cursor: new HydrationCursor(),
|
|
2215
|
+
container: el
|
|
2216
|
+
});
|
|
2217
|
+
const props = node.props;
|
|
2218
|
+
applyInitialProps(pass, node, true);
|
|
2219
|
+
if (!isDangerousInnerHTMLPayload(props.dangerouslySetInnerHTML)) {
|
|
2220
|
+
node.children = reconcileChildren({
|
|
2221
|
+
...ctx,
|
|
2222
|
+
ns: childNamespace(node.tag, dormant.ns)
|
|
2223
|
+
}, node, props.children, true);
|
|
2224
|
+
pass.onDiscard(() => {
|
|
2225
|
+
node.children = [];
|
|
2226
|
+
});
|
|
2227
|
+
}
|
|
2228
|
+
applyTrailingProps(pass, node, props, true, true);
|
|
2229
|
+
attachRef(pass, node, void 0);
|
|
2230
|
+
pass.op(() => {
|
|
2231
|
+
pass.onReversibleCommit(() => {
|
|
2232
|
+
node.dormant = dormant;
|
|
2233
|
+
dormantHosts.set(el, node);
|
|
2234
|
+
});
|
|
2235
|
+
try {
|
|
2236
|
+
removeUnrenderedAttributes(el, props, (attributes) => {
|
|
2237
|
+
pass.onReversibleCommit(() => {
|
|
2238
|
+
for (const attribute of attributes) el.setAttributeNode(attribute);
|
|
2239
|
+
});
|
|
2240
|
+
});
|
|
2241
|
+
} catch (error) {
|
|
2242
|
+
throw new CommitMutationError(error);
|
|
2243
|
+
}
|
|
2244
|
+
node.dormant = null;
|
|
2245
|
+
dormantHosts.delete(el);
|
|
2246
|
+
});
|
|
2247
|
+
}
|
|
2248
|
+
function ownsChildren(props) {
|
|
2249
|
+
return !props.imperativeChildren && !isDangerousInnerHTMLPayload(props.dangerouslySetInnerHTML);
|
|
2250
|
+
}
|
|
2251
|
+
function patchHost(ctx, node, props) {
|
|
2252
|
+
const previous = node.props;
|
|
2253
|
+
if (previous === props) return;
|
|
2254
|
+
if (node.dormant) {
|
|
2255
|
+
ctx.pass.op(() => {
|
|
2256
|
+
const previous = node.props;
|
|
2257
|
+
ctx.pass.onReversibleCommit(() => {
|
|
2258
|
+
node.props = previous;
|
|
2259
|
+
});
|
|
2260
|
+
node.props = props;
|
|
2261
|
+
});
|
|
2262
|
+
return;
|
|
2263
|
+
}
|
|
2264
|
+
const wasManaged = ownsChildren(previous);
|
|
2265
|
+
const isManaged = ownsChildren(props);
|
|
2266
|
+
if (wasManaged && !isManaged) reconcileChildren(ctx, node, null, false);
|
|
2267
|
+
patchProps(ctx.pass, node, previous, props);
|
|
2268
|
+
if (node.tag === "option" && !Object.is(previous.value, props.value)) syncEnclosingSelect(ctx.pass, node.parent);
|
|
2269
|
+
if (!wasManaged && isManaged && node.children.length === 0) ctx.pass.op(() => {
|
|
2270
|
+
const children = Array.from(node.el.childNodes);
|
|
2271
|
+
ctx.pass.onReversibleCommit(() => restoreElementChildren(node.el, children));
|
|
2272
|
+
try {
|
|
2273
|
+
node.el.replaceChildren();
|
|
2274
|
+
} catch (error) {
|
|
2275
|
+
throw new CommitMutationError(error);
|
|
2276
|
+
}
|
|
2277
|
+
});
|
|
2278
|
+
if (isManaged) {
|
|
2279
|
+
reconcileChildren({
|
|
2280
|
+
...ctx,
|
|
2281
|
+
ns: childNamespace(node.tag, namespaceOf(node.el))
|
|
2282
|
+
}, node, props.children, false);
|
|
2283
|
+
if (props.children === void 0 || props.children === null || props.children === false) ctx.pass.op(() => {
|
|
2284
|
+
if (!node.el.firstChild) return;
|
|
2285
|
+
const previous = Array.from(node.el.childNodes);
|
|
2286
|
+
ctx.pass.onReversibleCommit(() => restoreElementChildren(node.el, previous));
|
|
2287
|
+
try {
|
|
2288
|
+
node.el.replaceChildren();
|
|
2289
|
+
} catch (error) {
|
|
2290
|
+
throw new CommitMutationError(error);
|
|
2291
|
+
}
|
|
2292
|
+
});
|
|
2293
|
+
}
|
|
2294
|
+
ctx.pass.op(() => {
|
|
2295
|
+
const previous = node.props;
|
|
2296
|
+
ctx.pass.onReversibleCommit(() => {
|
|
2297
|
+
node.props = previous;
|
|
2298
|
+
});
|
|
2299
|
+
node.props = props;
|
|
2300
|
+
});
|
|
2301
|
+
if (node.tag === "select") ctx.pass.op(() => {
|
|
2302
|
+
applyTrailingProps(ctx.pass, node, props);
|
|
2303
|
+
markSelectSynced(ctx.pass, node);
|
|
2304
|
+
});
|
|
2305
|
+
if (props.ref !== previous.ref) attachRef(ctx.pass, {
|
|
2306
|
+
...node,
|
|
2307
|
+
props
|
|
2308
|
+
}, previous.ref);
|
|
2309
|
+
}
|
|
2310
|
+
function namespaceOf(el) {
|
|
2311
|
+
const uri = el.namespaceURI;
|
|
2312
|
+
return uri === SVG_NS || uri === MATHML_NS ? uri : null;
|
|
2313
|
+
}
|
|
2314
|
+
function createComponent(ctx, parent, key, fn, props) {
|
|
2315
|
+
const instance = new ComponentInstance(ctx.owner, fn, props);
|
|
2316
|
+
ctx.pass.own(instance);
|
|
2317
|
+
const node = {
|
|
2318
|
+
kind: 2,
|
|
2319
|
+
parent,
|
|
2320
|
+
key,
|
|
2321
|
+
instance,
|
|
2322
|
+
children: []
|
|
2323
|
+
};
|
|
2324
|
+
instance.view = node;
|
|
2325
|
+
node.children = renderInstance(ctx, node, true);
|
|
2326
|
+
return node;
|
|
2327
|
+
}
|
|
2328
|
+
function propsChanged(previous, next) {
|
|
2329
|
+
if (previous === next) return false;
|
|
2330
|
+
let count = 0;
|
|
2331
|
+
for (const key in previous) {
|
|
2332
|
+
count++;
|
|
2333
|
+
if (!(key in next) || !Object.is(previous[key], next[key])) return true;
|
|
2334
|
+
}
|
|
2335
|
+
for (const key in next) count--;
|
|
2336
|
+
return count !== 0;
|
|
2337
|
+
}
|
|
2338
|
+
function patchComponent(ctx, node, props) {
|
|
2339
|
+
const instance = node.instance;
|
|
2340
|
+
if (!propsChanged(instance.props, props) && !instance.computation.stale && !instance.computation._hasError && instance.seenAncestorContextRevision === ancestorContextRevision(instance)) return;
|
|
2341
|
+
instance.setProps(props);
|
|
2342
|
+
renderInstance(ctx, node, false);
|
|
2343
|
+
}
|
|
2344
|
+
function nearestComponent(instance) {
|
|
2345
|
+
for (let owner = instance.parent; owner; owner = owner.parent) if (owner instanceof ComponentInstance) return owner;
|
|
2346
|
+
return null;
|
|
2347
|
+
}
|
|
2348
|
+
/**
|
|
2349
|
+
* Highest `contextRevision` among `instance`'s component ancestors. Each
|
|
2350
|
+
* instance caches its inherited value for the current context epoch, so
|
|
2351
|
+
* patching every component in a deep chain stays linear. The cache relies on
|
|
2352
|
+
* a live instance's parent chain never changing (owners are not reparented).
|
|
2353
|
+
* The epoch is global, so a `provide()` anywhere invalidates every cache; the
|
|
2354
|
+
* worst case is the uncached O(depth) walk.
|
|
2355
|
+
*/
|
|
2356
|
+
function ancestorContextRevision(instance) {
|
|
2357
|
+
const epoch = getContextEpoch();
|
|
2358
|
+
let chain = null;
|
|
2359
|
+
let revision = 0;
|
|
2360
|
+
for (let current = nearestComponent(instance); current;) {
|
|
2361
|
+
if (current.inheritedContextEpoch === epoch) {
|
|
2362
|
+
revision = Math.max(current.inheritedContextRevision, current.contextRevision);
|
|
2363
|
+
break;
|
|
2364
|
+
}
|
|
2365
|
+
(chain ??= []).push(current);
|
|
2366
|
+
current = nearestComponent(current);
|
|
2367
|
+
}
|
|
2368
|
+
if (!chain) return revision;
|
|
2369
|
+
for (let index = chain.length - 1; index >= 0; index--) {
|
|
2370
|
+
const ancestor = chain[index];
|
|
2371
|
+
ancestor.inheritedContextRevision = revision;
|
|
2372
|
+
ancestor.inheritedContextEpoch = epoch;
|
|
2373
|
+
revision = Math.max(revision, ancestor.contextRevision);
|
|
2374
|
+
}
|
|
2375
|
+
return revision;
|
|
2376
|
+
}
|
|
2377
|
+
/**
|
|
2378
|
+
* Render a component and reconcile its output. An error boundary catches a
|
|
2379
|
+
* failure anywhere in its subtree: the work recorded since it started is
|
|
2380
|
+
* rewound and it renders again showing its fallback.
|
|
2381
|
+
*/
|
|
2382
|
+
/** The component rendering now while its root hydrates. */
|
|
2383
|
+
let hydratingRender = null;
|
|
2384
|
+
function renderComponent(ctx, node) {
|
|
2385
|
+
const previous = hydratingRender;
|
|
2386
|
+
hydratingRender = ctx.hydrate ? {
|
|
2387
|
+
ctx,
|
|
2388
|
+
node
|
|
2389
|
+
} : null;
|
|
2390
|
+
node.instance.hydrationResourceKeys = ctx.hydrate ? ctx.hydrate.cursor.claimResourceSlots(ctx.hydrate.container) : null;
|
|
2391
|
+
try {
|
|
2392
|
+
return node.instance.render((undo) => ctx.pass.onDiscard(undo));
|
|
2393
|
+
} finally {
|
|
2394
|
+
node.instance.hydrationResourceKeys = null;
|
|
2395
|
+
hydratingRender = previous;
|
|
2396
|
+
}
|
|
2397
|
+
}
|
|
2398
|
+
function isHydratingRender() {
|
|
2399
|
+
return hydratingRender !== null;
|
|
2400
|
+
}
|
|
2401
|
+
function commitSeenAncestorContextRevision(pass, instance, revision) {
|
|
2402
|
+
pass.op(() => {
|
|
2403
|
+
const previous = instance.seenAncestorContextRevision;
|
|
2404
|
+
pass.onReversibleCommit(() => {
|
|
2405
|
+
instance.seenAncestorContextRevision = previous;
|
|
2406
|
+
});
|
|
2407
|
+
instance.seenAncestorContextRevision = revision;
|
|
2408
|
+
});
|
|
2409
|
+
}
|
|
2410
|
+
/**
|
|
2411
|
+
* Render the hydrating component again after the rest of its root, claiming
|
|
2412
|
+
* the server nodes reserved at its position (portal hosts whose content is
|
|
2413
|
+
* written later in the same render).
|
|
2414
|
+
*/
|
|
2415
|
+
function deferHydratingRender(instance) {
|
|
2416
|
+
const active = hydratingRender;
|
|
2417
|
+
if (!active || active.node.instance !== instance) return false;
|
|
2418
|
+
const { ctx, node } = active;
|
|
2419
|
+
const hydrate = ctx.hydrate;
|
|
2420
|
+
const cursor = hydrate.cursor.reserve(hydrate.container);
|
|
2421
|
+
hydrate.cursor.deferred.push({ render: () => {
|
|
2422
|
+
if (instance.disposed) return;
|
|
2423
|
+
if (!instance.computation.stale) {
|
|
2424
|
+
if (ctx.pass.hasDormantPortalWriter) {
|
|
2425
|
+
const held = cursor.heldNodes(hydrate.container);
|
|
2426
|
+
node.children = held.map((dom) => ({
|
|
2427
|
+
kind: 7,
|
|
2428
|
+
parent: node,
|
|
2429
|
+
key: void 0,
|
|
2430
|
+
node: dom
|
|
2431
|
+
}));
|
|
2432
|
+
node.deferredHydration = {
|
|
2433
|
+
cursor,
|
|
2434
|
+
container: hydrate.container
|
|
2435
|
+
};
|
|
2436
|
+
}
|
|
2437
|
+
return;
|
|
2438
|
+
}
|
|
2439
|
+
renderInstance({
|
|
2440
|
+
...ctx,
|
|
2441
|
+
hydrate: {
|
|
2442
|
+
cursor,
|
|
2443
|
+
container: hydrate.container
|
|
2444
|
+
}
|
|
2445
|
+
}, node, true);
|
|
2446
|
+
} });
|
|
2447
|
+
return true;
|
|
2448
|
+
}
|
|
2449
|
+
/** Adopt held server portal output when its deferred writer first runs. */
|
|
2450
|
+
function hydrateDeferredComponent(ctx, node, output) {
|
|
2451
|
+
const deferred = node.deferredHydration;
|
|
2452
|
+
const previous = node.children;
|
|
2453
|
+
const next = reconcileChildren(withOwner({
|
|
2454
|
+
...ctx,
|
|
2455
|
+
hydrate: deferred
|
|
2456
|
+
}, node.instance), node, output, true);
|
|
2457
|
+
const container = deferred.container;
|
|
2458
|
+
const after = nextDomAfter(node);
|
|
2459
|
+
ctx.pass.op(() => {
|
|
2460
|
+
const previousDom = Array.from(container.childNodes);
|
|
2461
|
+
ctx.pass.onReversibleCommit(() => {
|
|
2462
|
+
try {
|
|
2463
|
+
restoreNodeChildren(container, previousDom);
|
|
2464
|
+
} finally {
|
|
2465
|
+
node.children = previous;
|
|
2466
|
+
node.deferredHydration = deferred;
|
|
2467
|
+
}
|
|
2468
|
+
});
|
|
2469
|
+
const retained = new Set(next.flatMap((child) => collectDom(child)));
|
|
2470
|
+
try {
|
|
2471
|
+
for (const child of previous) for (const dom of collectDom(child)) if (!retained.has(dom)) dom.parentNode?.removeChild(dom);
|
|
2472
|
+
let before = after;
|
|
2473
|
+
for (let index = next.length - 1; index >= 0; index--) {
|
|
2474
|
+
const doms = collectDom(next[index]);
|
|
2475
|
+
for (let item = doms.length - 1; item >= 0; item--) {
|
|
2476
|
+
const dom = doms[item];
|
|
2477
|
+
if (dom.parentNode !== container || dom.nextSibling !== before) container.insertBefore(dom, before);
|
|
2478
|
+
before = dom;
|
|
2479
|
+
}
|
|
2480
|
+
}
|
|
2481
|
+
} catch (error) {
|
|
2482
|
+
throw new CommitMutationError(error);
|
|
2483
|
+
}
|
|
2484
|
+
node.children = next;
|
|
2485
|
+
node.deferredHydration = null;
|
|
2486
|
+
});
|
|
2487
|
+
ctx.pass.markRendered(node.instance);
|
|
2488
|
+
}
|
|
2489
|
+
function renderInstance(ctx, node, fresh) {
|
|
2490
|
+
const instance = node.instance;
|
|
2491
|
+
const inner = withOwner(ctx, instance);
|
|
2492
|
+
const mark = ctx.pass.mark();
|
|
2493
|
+
try {
|
|
2494
|
+
const output = renderComponent(ctx, node);
|
|
2495
|
+
if (instance.mounted) ctx.pass.onDiscard(() => {
|
|
2496
|
+
if (ctx.pass.commitAborted) instance.computation.deferRetry();
|
|
2497
|
+
else instance.computation.invalidate();
|
|
2498
|
+
});
|
|
2499
|
+
const children = reconcileComponentOutput(inner, node, output, fresh);
|
|
2500
|
+
ctx.pass.markRendered(instance);
|
|
2501
|
+
const revision = ancestorContextRevision(instance);
|
|
2502
|
+
commitSeenAncestorContextRevision(ctx.pass, instance, revision);
|
|
2503
|
+
return children;
|
|
2504
|
+
} catch (caught) {
|
|
2505
|
+
const error = clarifyRenderError(caught);
|
|
2506
|
+
const boundary = instance.boundary;
|
|
2507
|
+
if (!boundary) {
|
|
2508
|
+
noteErrorOrigin(instance.parent, error);
|
|
2509
|
+
throw error;
|
|
2510
|
+
}
|
|
2511
|
+
for (const failure of ctx.pass.rewind(mark)) reportUncaughtErrorLater(failure);
|
|
2512
|
+
if (!boundary(error)) throw error;
|
|
2513
|
+
const children = reconcileChildren(inner, node, componentOutput(renderComponent(ctx, node)), fresh);
|
|
2514
|
+
ctx.pass.markRendered(instance);
|
|
2515
|
+
const revision = ancestorContextRevision(instance);
|
|
2516
|
+
commitSeenAncestorContextRevision(ctx.pass, instance, revision);
|
|
2517
|
+
return children;
|
|
2518
|
+
}
|
|
2519
|
+
}
|
|
2520
|
+
/**
|
|
2521
|
+
* A component that returns the same component type directly can form a very
|
|
2522
|
+
* deep transparent chain. Walk the hook-free part of that chain explicitly;
|
|
2523
|
+
* all other output still goes through the ordinary child reconciler.
|
|
2524
|
+
*/
|
|
2525
|
+
function reconcileComponentOutput(ctx, node, value, fresh) {
|
|
2526
|
+
let output = componentOutput(value);
|
|
2527
|
+
let current = node;
|
|
2528
|
+
let inner = ctx;
|
|
2529
|
+
let creating = fresh;
|
|
2530
|
+
const rendered = [];
|
|
2531
|
+
for (;;) {
|
|
2532
|
+
const next = current.instance.hooks.length === 0 && !current.instance.boundary ? selfChild(output, current.instance.fn) : null;
|
|
2533
|
+
if (!next) {
|
|
2534
|
+
const children = reconcileChildren(inner, current, output, creating);
|
|
2535
|
+
if (creating) current.children = children;
|
|
2536
|
+
break;
|
|
2537
|
+
}
|
|
2538
|
+
let child;
|
|
2539
|
+
if (creating) {
|
|
2540
|
+
const instance = new ComponentInstance(current.instance, next.fn, next.props);
|
|
2541
|
+
ctx.pass.own(instance);
|
|
2542
|
+
child = {
|
|
2543
|
+
kind: 2,
|
|
2544
|
+
parent: current,
|
|
2545
|
+
key: next.key,
|
|
2546
|
+
instance,
|
|
2547
|
+
children: []
|
|
2548
|
+
};
|
|
2549
|
+
instance.view = child;
|
|
2550
|
+
current.children = [child];
|
|
2551
|
+
} else {
|
|
2552
|
+
const previous = current.children;
|
|
2553
|
+
if (previous.length !== 1 || previous[0].kind !== 2 || previous[0].key !== next.key || previous[0].instance.fn !== next.fn) {
|
|
2554
|
+
reconcileChildren(inner, current, output, false);
|
|
2555
|
+
break;
|
|
2556
|
+
}
|
|
2557
|
+
child = previous[0];
|
|
2558
|
+
const instance = child.instance;
|
|
2559
|
+
if (!propsChanged(instance.props, next.props) && !instance.computation.stale && !instance.computation._hasError && instance.seenAncestorContextRevision === ancestorContextRevision(instance)) break;
|
|
2560
|
+
instance.setProps(next.props);
|
|
2561
|
+
}
|
|
2562
|
+
const instance = child.instance;
|
|
2563
|
+
rendered.push(child);
|
|
2564
|
+
inner = withOwner(inner, instance);
|
|
2565
|
+
current = child;
|
|
2566
|
+
output = componentOutput(renderComponent(inner, child));
|
|
2567
|
+
if (instance.mounted) ctx.pass.onDiscard(() => {
|
|
2568
|
+
if (ctx.pass.commitAborted) instance.computation.deferRetry();
|
|
2569
|
+
else instance.computation.invalidate();
|
|
2570
|
+
});
|
|
2571
|
+
creating = fresh;
|
|
2572
|
+
}
|
|
2573
|
+
for (let i = rendered.length - 1; i >= 0; i--) {
|
|
2574
|
+
const instance = rendered[i].instance;
|
|
2575
|
+
ctx.pass.markRendered(instance);
|
|
2576
|
+
const revision = ancestorContextRevision(instance);
|
|
2577
|
+
commitSeenAncestorContextRevision(ctx.pass, instance, revision);
|
|
2578
|
+
}
|
|
2579
|
+
return node.children;
|
|
2580
|
+
}
|
|
2581
|
+
function restoreNodeChildren(container, previous) {
|
|
2582
|
+
for (let index = 0; index < previous.length; index += 1) {
|
|
2583
|
+
const current = container.childNodes[index] ?? null;
|
|
2584
|
+
if (current !== previous[index]) container.insertBefore(previous[index], current);
|
|
2585
|
+
}
|
|
2586
|
+
while (container.childNodes.length > previous.length) container.removeChild(container.lastChild);
|
|
2587
|
+
}
|
|
2588
|
+
function selfChild(output, fn) {
|
|
2589
|
+
if (!output || typeof output !== "object" || Array.isArray(output) || output.type !== fn) return null;
|
|
2590
|
+
const children = normalizeChildren(output);
|
|
2591
|
+
return children.length === 1 && children[0].kind === 2 ? children[0] : null;
|
|
2592
|
+
}
|
|
2593
|
+
/** A function returned by a component is a value, not a child slot. */
|
|
2594
|
+
function componentOutput(value) {
|
|
2595
|
+
return typeof value === "function" ? null : value;
|
|
2596
|
+
}
|
|
2597
|
+
function createDynamic(ctx, parent, fn) {
|
|
2598
|
+
const node = {
|
|
2599
|
+
kind: DYNAMIC,
|
|
2600
|
+
parent,
|
|
2601
|
+
key: void 0,
|
|
2602
|
+
fn,
|
|
2603
|
+
children: [],
|
|
2604
|
+
depth: (nearestInstance(ctx.owner)?.depth ?? 0) + 1
|
|
2605
|
+
};
|
|
2606
|
+
node.instance = new ComponentInstance(ctx.owner, () => functionChildOutput(readValue(node.fn)), {}, () => scheduleDynamicUpdate(node));
|
|
2607
|
+
node.computation = node.instance.computation;
|
|
2608
|
+
ctx.pass.own(node.instance);
|
|
2609
|
+
node.children = reconcileChildren(withOwner(ctx, node.instance), node, readDynamic(node), true, false);
|
|
2610
|
+
ctx.pass.markRendered(node.instance);
|
|
2611
|
+
return node;
|
|
2612
|
+
}
|
|
2613
|
+
/** Run a function child's read now, tracking what it reads. */
|
|
2614
|
+
function readDynamic(node, pass) {
|
|
2615
|
+
const output = node.instance.render(pass ? (undo) => pass.onDiscard(undo) : void 0);
|
|
2616
|
+
if (pass && node.instance.mounted) pass.onDiscard(() => {
|
|
2617
|
+
if (pass.commitAborted) node.computation.deferRetry();
|
|
2618
|
+
else node.computation.invalidate();
|
|
2619
|
+
});
|
|
2620
|
+
return componentOutput(output);
|
|
2621
|
+
}
|
|
2622
|
+
function patchDynamic(ctx, node, fn) {
|
|
2623
|
+
const previous = node.fn;
|
|
2624
|
+
node.fn = fn;
|
|
2625
|
+
ctx.pass.onDiscard(() => {
|
|
2626
|
+
node.fn = previous;
|
|
2627
|
+
});
|
|
2628
|
+
updateDynamic(ctx, node);
|
|
2629
|
+
}
|
|
2630
|
+
/** Reconcile a function child, remounting only when its own hooks change. */
|
|
2631
|
+
function updateDynamic(ctx, node) {
|
|
2632
|
+
const mark = ctx.pass.mark();
|
|
2633
|
+
try {
|
|
2634
|
+
reconcileChildren(withOwner(ctx, node.instance), node, readDynamic(node, ctx.pass), false, false);
|
|
2635
|
+
ctx.pass.markRendered(node.instance);
|
|
2636
|
+
} catch (error) {
|
|
2637
|
+
if (!(error instanceof HookOrderChangeError) || error.instance !== node.instance) throw error;
|
|
2638
|
+
for (const failure of ctx.pass.rewind(mark)) reportUncaughtErrorLater(failure);
|
|
2639
|
+
const slot = ctx.pass.reserve();
|
|
2640
|
+
const replacement = createDynamic(withOwner(ctx, node.instance.parent), node.parent, node.fn);
|
|
2641
|
+
ctx.pass.fill(slot, () => {
|
|
2642
|
+
const parent = node.parent;
|
|
2643
|
+
const container = containerOf(parent);
|
|
2644
|
+
const next = nextDomAfter(node);
|
|
2645
|
+
const oldDom = collectDom(node);
|
|
2646
|
+
const replacementDom = collectDom(replacement);
|
|
2647
|
+
const index = parent.children.indexOf(node);
|
|
2648
|
+
ctx.pass.onReversibleCommit(() => {
|
|
2649
|
+
for (const dom of replacementDom) dom.parentNode?.removeChild(dom);
|
|
2650
|
+
parent.children[index] = node;
|
|
2651
|
+
}, () => {
|
|
2652
|
+
const errors = [];
|
|
2653
|
+
for (const dom of oldDom) dom.parentNode?.removeChild(dom);
|
|
2654
|
+
release(node, errors);
|
|
2655
|
+
reportTeardown(errors);
|
|
2656
|
+
});
|
|
2657
|
+
parent.children[index] = replacement;
|
|
2658
|
+
try {
|
|
2659
|
+
for (const dom of replacementDom) container.insertBefore(dom, next);
|
|
2660
|
+
} catch (error) {
|
|
2661
|
+
throw new CommitMutationError(error);
|
|
2662
|
+
}
|
|
2663
|
+
});
|
|
2664
|
+
}
|
|
2665
|
+
}
|
|
2666
|
+
function release(node, errors) {
|
|
2667
|
+
const pending = [{
|
|
2668
|
+
node,
|
|
2669
|
+
finish: false
|
|
2670
|
+
}];
|
|
2671
|
+
while (pending.length) {
|
|
2672
|
+
const frame = pending.pop();
|
|
2673
|
+
const current = frame.node;
|
|
2674
|
+
if (frame.finish) {
|
|
2675
|
+
if (current.kind === HOST) {
|
|
2676
|
+
if (current.dormant) dormantHosts.delete(current.el);
|
|
2677
|
+
releaseProps(current, errors);
|
|
2678
|
+
}
|
|
2679
|
+
if (current.kind === 2 || current.kind === DYNAMIC) current.instance.dispose(errors);
|
|
2680
|
+
continue;
|
|
2681
|
+
}
|
|
2682
|
+
if (current.kind === 1 || current.kind === 7) continue;
|
|
2683
|
+
pending.push({
|
|
2684
|
+
node: current,
|
|
2685
|
+
finish: true
|
|
2686
|
+
});
|
|
2687
|
+
for (let i = current.children.length - 1; i >= 0; i--) pending.push({
|
|
2688
|
+
node: current.children[i],
|
|
2689
|
+
finish: false
|
|
2690
|
+
});
|
|
2691
|
+
}
|
|
2692
|
+
}
|
|
2693
|
+
/**
|
|
2694
|
+
* A select's controlled value depends on its options. When a pass changes
|
|
2695
|
+
* them without patching the select (a child component, `For`, or function
|
|
2696
|
+
* child adds, removes, or edits options), re-apply the select's value after
|
|
2697
|
+
* the pass commits, once per select. Each change registers its own sync so
|
|
2698
|
+
* an error boundary that rewinds part of the pass drops only its own.
|
|
2699
|
+
*/
|
|
2700
|
+
const scheduledSelectSyncs = /* @__PURE__ */ new WeakMap();
|
|
2701
|
+
const writtenSelectValues = /* @__PURE__ */ new WeakMap();
|
|
2702
|
+
function syncEnclosingSelect(pass, parent) {
|
|
2703
|
+
const select = enclosingSelect(parent);
|
|
2704
|
+
if (!select || select.dormant || !("value" in select.props)) return;
|
|
2705
|
+
let scheduled = scheduledSelectSyncs.get(pass);
|
|
2706
|
+
if (!scheduled) scheduledSelectSyncs.set(pass, scheduled = /* @__PURE__ */ new Set());
|
|
2707
|
+
if (scheduled.has(select)) return;
|
|
2708
|
+
scheduled.add(select);
|
|
2709
|
+
pass.onDiscard(() => scheduled.delete(select));
|
|
2710
|
+
recordSelectUndo(pass, select);
|
|
2711
|
+
pass.beforeJournalSettle(() => {
|
|
2712
|
+
if (writtenSelectValues.get(pass)?.has(select)) return;
|
|
2713
|
+
try {
|
|
2714
|
+
resyncSelect(select);
|
|
2715
|
+
} catch (error) {
|
|
2716
|
+
throw new CommitMutationError(error);
|
|
2717
|
+
}
|
|
2718
|
+
});
|
|
2719
|
+
}
|
|
2720
|
+
/** The select's own patch applied its value after its options. */
|
|
2721
|
+
function markSelectSynced(pass, select) {
|
|
2722
|
+
let written = writtenSelectValues.get(pass);
|
|
2723
|
+
if (!written) writtenSelectValues.set(pass, written = /* @__PURE__ */ new Set());
|
|
2724
|
+
written.add(select);
|
|
2725
|
+
}
|
|
2726
|
+
const domNodes = {
|
|
2727
|
+
listChanged(ctx, parent) {
|
|
2728
|
+
syncEnclosingSelect(ctx.pass, parent);
|
|
2729
|
+
},
|
|
2730
|
+
create(ctx, parent, child) {
|
|
2731
|
+
switch (child.kind) {
|
|
2732
|
+
case 1: {
|
|
2733
|
+
const hydrate = ctx.hydrate;
|
|
2734
|
+
const claimed = hydrate?.cursor.claimText(hydrate.container, child.text);
|
|
2735
|
+
if (claimed && claimed.data !== child.text) {
|
|
2736
|
+
const text = child.text;
|
|
2737
|
+
ctx.pass.op(() => {
|
|
2738
|
+
const previous = claimed.data;
|
|
2739
|
+
ctx.pass.onReversibleCommit(() => {
|
|
2740
|
+
claimed.data = previous;
|
|
2741
|
+
});
|
|
2742
|
+
try {
|
|
2743
|
+
claimed.data = text;
|
|
2744
|
+
} catch (error) {
|
|
2745
|
+
throw new CommitMutationError(error);
|
|
2746
|
+
}
|
|
2747
|
+
});
|
|
2748
|
+
}
|
|
2749
|
+
return {
|
|
2750
|
+
kind: 1,
|
|
2751
|
+
parent,
|
|
2752
|
+
key: void 0,
|
|
2753
|
+
node: claimed ?? document.createTextNode(child.text),
|
|
2754
|
+
text: child.text
|
|
2755
|
+
};
|
|
2756
|
+
}
|
|
2757
|
+
case 0: return createHost(ctx, parent, child.key, child.tag, child.props);
|
|
2758
|
+
case 2: return createComponent(ctx, parent, child.key, child.fn, child.props);
|
|
2759
|
+
case 3: {
|
|
2760
|
+
const node = {
|
|
2761
|
+
kind: 3,
|
|
2762
|
+
parent,
|
|
2763
|
+
key: child.key,
|
|
2764
|
+
children: []
|
|
2765
|
+
};
|
|
2766
|
+
node.children = reconcileChildren(withOwner(ctx, child.owner ?? ctx.owner), node, child.children, true);
|
|
2767
|
+
return node;
|
|
2768
|
+
}
|
|
2769
|
+
case 4: return createDynamic(ctx, parent, child.fn);
|
|
2770
|
+
case 7: return {
|
|
2771
|
+
kind: 7,
|
|
2772
|
+
parent,
|
|
2773
|
+
key: void 0,
|
|
2774
|
+
node: child.node
|
|
2775
|
+
};
|
|
2776
|
+
}
|
|
2777
|
+
},
|
|
2778
|
+
patch(ctx, node, child) {
|
|
2779
|
+
switch (node.kind) {
|
|
2780
|
+
case 1: {
|
|
2781
|
+
const text = child.text;
|
|
2782
|
+
if (node.text !== text) {
|
|
2783
|
+
ctx.pass.op(() => {
|
|
2784
|
+
const previous = node.text;
|
|
2785
|
+
const previousData = node.node.data;
|
|
2786
|
+
ctx.pass.onReversibleCommit(() => {
|
|
2787
|
+
try {
|
|
2788
|
+
node.node.data = previousData;
|
|
2789
|
+
} finally {
|
|
2790
|
+
node.text = previous;
|
|
2791
|
+
}
|
|
2792
|
+
});
|
|
2793
|
+
try {
|
|
2794
|
+
node.node.data = text;
|
|
2795
|
+
node.text = text;
|
|
2796
|
+
} catch (error) {
|
|
2797
|
+
throw new CommitMutationError(error);
|
|
2798
|
+
}
|
|
2799
|
+
});
|
|
2800
|
+
syncEnclosingSelect(ctx.pass, node.parent);
|
|
2801
|
+
}
|
|
2802
|
+
return;
|
|
2803
|
+
}
|
|
2804
|
+
case HOST:
|
|
2805
|
+
patchHost(ctx, node, child.props);
|
|
2806
|
+
return;
|
|
2807
|
+
case 2:
|
|
2808
|
+
patchComponent(ctx, node, child.props);
|
|
2809
|
+
return;
|
|
2810
|
+
case DYNAMIC:
|
|
2811
|
+
patchDynamic(ctx, node, child.fn);
|
|
2812
|
+
return;
|
|
2813
|
+
case 7: return;
|
|
2814
|
+
case 3: {
|
|
2815
|
+
const owner = child.owner;
|
|
2816
|
+
reconcileChildren(withOwner(ctx, owner ?? ctx.owner), node, child.children, false);
|
|
2817
|
+
return;
|
|
2818
|
+
}
|
|
2819
|
+
}
|
|
2820
|
+
},
|
|
2821
|
+
release
|
|
2822
|
+
};
|
|
2823
|
+
//#endregion
|
|
2824
|
+
//#region src/core/dom/updates.ts
|
|
2825
|
+
/**
|
|
2826
|
+
* Standalone updates: a component or function child whose own reads changed
|
|
2827
|
+
* re-renders in its own pass, independent of its parent.
|
|
2828
|
+
*
|
|
2829
|
+
* A failed update discards its pass and is delivered to the nearest error
|
|
2830
|
+
* boundary above it; without one, the error propagates to the scheduler,
|
|
2831
|
+
* which reports it after the flush.
|
|
2832
|
+
*/
|
|
2833
|
+
function runPass(owner, render) {
|
|
2834
|
+
const pass = new Pass();
|
|
2835
|
+
try {
|
|
2836
|
+
pass.run(() => render(pass));
|
|
2837
|
+
} catch (error) {
|
|
2838
|
+
for (const failure of pass.discard()) reportUncaughtErrorLater(failure);
|
|
2839
|
+
routeError(owner, clarifyRenderError(error));
|
|
2840
|
+
return;
|
|
2841
|
+
}
|
|
2842
|
+
try {
|
|
2843
|
+
pass.commit();
|
|
2844
|
+
} catch (error) {
|
|
2845
|
+
if (pass.commitAborted) routeError(owner, error);
|
|
2846
|
+
else throw error;
|
|
2847
|
+
}
|
|
2848
|
+
}
|
|
2849
|
+
function rerenderInstance(instance) {
|
|
2850
|
+
const node = instance.view;
|
|
2851
|
+
if (!node || instance.disposed || !instance.mounted) return;
|
|
2852
|
+
if (!instance.computation.stale) return;
|
|
2853
|
+
runPass(instance.parent, (pass) => {
|
|
2854
|
+
const before = instance.renderCount;
|
|
2855
|
+
const computation = instance.computation;
|
|
2856
|
+
const sources = new Set(computation._sources ?? []);
|
|
2857
|
+
pass.onDiscard(() => computation.restoreSources(sources));
|
|
2858
|
+
try {
|
|
2859
|
+
computation.update();
|
|
2860
|
+
if (computation._hasError) throw computation._error;
|
|
2861
|
+
} catch (error) {
|
|
2862
|
+
if (!instance.boundary) throw error;
|
|
2863
|
+
}
|
|
2864
|
+
if (instance.renderCount === before && !computation._hasError) return;
|
|
2865
|
+
renderAfterUpdate(createRenderContext(pass, instance.parent, namespaceAt(node.parent)), node);
|
|
2866
|
+
});
|
|
2867
|
+
}
|
|
2868
|
+
/**
|
|
2869
|
+
* The instance already ran with fresh reads; reconcile that output (or, for
|
|
2870
|
+
* a boundary whose render failed, render its fallback).
|
|
2871
|
+
*/
|
|
2872
|
+
function renderAfterUpdate(ctx, node) {
|
|
2873
|
+
const instance = node.instance;
|
|
2874
|
+
const computation = instance.computation;
|
|
2875
|
+
if (computation._hasError) {
|
|
2876
|
+
renderInstance(ctx, node, false);
|
|
2877
|
+
return;
|
|
2878
|
+
}
|
|
2879
|
+
const mark = ctx.pass.mark();
|
|
2880
|
+
try {
|
|
2881
|
+
if (node.deferredHydration) {
|
|
2882
|
+
hydrateDeferredComponent(ctx, node, componentOutput(computation._value));
|
|
2883
|
+
return;
|
|
2884
|
+
}
|
|
2885
|
+
reconcileChildren(withOwner(ctx, instance), node, componentOutput(computation._value), false);
|
|
2886
|
+
ctx.pass.markRendered(instance);
|
|
2887
|
+
} catch (caught) {
|
|
2888
|
+
const error = clarifyRenderError(caught);
|
|
2889
|
+
if (!instance.boundary) throw error;
|
|
2890
|
+
ctx.pass.rewind(mark);
|
|
2891
|
+
if (!instance.boundary(error)) throw error;
|
|
2892
|
+
renderInstance(ctx, node, false);
|
|
2893
|
+
}
|
|
2894
|
+
}
|
|
2895
|
+
const dynamicJobs = /* @__PURE__ */ new WeakMap();
|
|
2896
|
+
function scheduleDynamic(node) {
|
|
2897
|
+
let job = dynamicJobs.get(node);
|
|
2898
|
+
if (!job) {
|
|
2899
|
+
job = {
|
|
2900
|
+
depth: node.depth,
|
|
2901
|
+
run: () => rerenderDynamic(node),
|
|
2902
|
+
cancel: () => node.computation.dropScheduled(),
|
|
2903
|
+
get skip() {
|
|
2904
|
+
return node.computation.disposed || !node.computation.stale;
|
|
2905
|
+
}
|
|
2906
|
+
};
|
|
2907
|
+
dynamicJobs.set(node, job);
|
|
2908
|
+
}
|
|
2909
|
+
schedule(job, "render");
|
|
2910
|
+
}
|
|
2911
|
+
function rerenderDynamic(node) {
|
|
2912
|
+
const computation = node.computation;
|
|
2913
|
+
if (computation.disposed || !computation.stale) return;
|
|
2914
|
+
runPass(node.instance.parent, (pass) => {
|
|
2915
|
+
updateDynamic(createRenderContext(pass, node.instance, namespaceAt(node.parent)), node);
|
|
2916
|
+
});
|
|
2917
|
+
}
|
|
2918
|
+
/** Bind standalone updates when a DOM root is created. */
|
|
2919
|
+
function installRenderUpdates() {
|
|
2920
|
+
setRenderHost({
|
|
2921
|
+
rerender: rerenderInstance,
|
|
2922
|
+
isHydrating: isHydratingRender,
|
|
2923
|
+
deferHydration: deferHydratingRender
|
|
2924
|
+
});
|
|
2925
|
+
setDynamicUpdateScheduler(scheduleDynamic);
|
|
2926
|
+
}
|
|
2927
|
+
//#endregion
|
|
2928
|
+
//#region src/core/dom/root.ts
|
|
2929
|
+
/**
|
|
2930
|
+
* A root renders a value into a container element and owns everything it
|
|
2931
|
+
* renders.
|
|
2932
|
+
*
|
|
2933
|
+
* `prepare()` runs the render phase and returns work that is either
|
|
2934
|
+
* committed or discarded as a unit, so a caller updating several roots (a
|
|
2935
|
+
* navigation) can commit all of them or none. `render()` prepares and
|
|
2936
|
+
* commits in one step.
|
|
2937
|
+
*/
|
|
2938
|
+
function createRoot(container, options = {}) {
|
|
2939
|
+
installRenderUpdates();
|
|
2940
|
+
const owner = new Owner(options.owner === void 0 ? getOwner() : options.owner);
|
|
2941
|
+
(owner.context ??= /* @__PURE__ */ new Map()).set(EVENT_ROOT_CONTAINER, container);
|
|
2942
|
+
const node = {
|
|
2943
|
+
kind: 5,
|
|
2944
|
+
el: container,
|
|
2945
|
+
children: [],
|
|
2946
|
+
parent: null,
|
|
2947
|
+
key: void 0,
|
|
2948
|
+
tail: options.before ?? null
|
|
2949
|
+
};
|
|
2950
|
+
let mounted = false;
|
|
2951
|
+
let hydrate = options.hydrate === true;
|
|
2952
|
+
let releaseEvents = null;
|
|
2953
|
+
function prepare(value) {
|
|
2954
|
+
const pass = new Pass();
|
|
2955
|
+
const fresh = !mounted;
|
|
2956
|
+
const tail = node.tail?.parentNode === container ? node.tail : null;
|
|
2957
|
+
const cursor = fresh && hydrate ? new HydrationCursor(tail) : null;
|
|
2958
|
+
const ctx = createRenderContext(pass, owner, namespaceAt(node), cursor ? {
|
|
2959
|
+
cursor,
|
|
2960
|
+
container
|
|
2961
|
+
} : null);
|
|
2962
|
+
try {
|
|
2963
|
+
pass.run(() => runWithOwner(owner, () => {
|
|
2964
|
+
if (!fresh) {
|
|
2965
|
+
reconcileChildren(ctx, node, value, false);
|
|
2966
|
+
return;
|
|
2967
|
+
}
|
|
2968
|
+
const children = reconcileChildren(ctx, node, value, true);
|
|
2969
|
+
for (const deferred of cursor?.deferred ?? []) deferred.render();
|
|
2970
|
+
const adopting = ctx.hydrate !== null;
|
|
2971
|
+
pass.op(() => {
|
|
2972
|
+
const before = node.tail?.parentNode === container ? node.tail : null;
|
|
2973
|
+
const dom = children.flatMap((child) => collectDom(child));
|
|
2974
|
+
if (adopting) {
|
|
2975
|
+
const previous = node.children;
|
|
2976
|
+
pass.onReversibleCommit(() => {
|
|
2977
|
+
node.children = previous;
|
|
2978
|
+
});
|
|
2979
|
+
syncChildren(pass, container, dom, before);
|
|
2980
|
+
node.children = children;
|
|
2981
|
+
return;
|
|
2982
|
+
}
|
|
2983
|
+
const previousChildren = node.children;
|
|
2984
|
+
if (!before) {
|
|
2985
|
+
const replaced = Array.from(container.childNodes);
|
|
2986
|
+
pass.onReversibleCommit(() => {
|
|
2987
|
+
try {
|
|
2988
|
+
restoreRootChildren(container, replaced);
|
|
2989
|
+
} finally {
|
|
2990
|
+
node.children = previousChildren;
|
|
2991
|
+
}
|
|
2992
|
+
});
|
|
2993
|
+
try {
|
|
2994
|
+
container.replaceChildren(...dom);
|
|
2995
|
+
} catch (error) {
|
|
2996
|
+
if (error instanceof CommitMutationError) throw error;
|
|
2997
|
+
throw new CommitMutationError(error);
|
|
2998
|
+
}
|
|
2999
|
+
node.children = children;
|
|
3000
|
+
return;
|
|
3001
|
+
}
|
|
3002
|
+
const previous = Array.from(container.childNodes);
|
|
3003
|
+
pass.onReversibleCommit(() => {
|
|
3004
|
+
try {
|
|
3005
|
+
restoreRootChildren(container, previous);
|
|
3006
|
+
} finally {
|
|
3007
|
+
node.children = previousChildren;
|
|
3008
|
+
}
|
|
3009
|
+
});
|
|
3010
|
+
try {
|
|
3011
|
+
for (const item of dom) container.insertBefore(item, before);
|
|
3012
|
+
} catch (error) {
|
|
3013
|
+
if (error instanceof CommitMutationError) throw error;
|
|
3014
|
+
throw new CommitMutationError(error);
|
|
3015
|
+
}
|
|
3016
|
+
node.children = children;
|
|
3017
|
+
});
|
|
3018
|
+
}));
|
|
3019
|
+
} catch (error) {
|
|
3020
|
+
for (const failure of pass.discard()) reportUncaughtErrorLater(failure);
|
|
3021
|
+
throw clarifyRenderOverflow(error);
|
|
3022
|
+
}
|
|
3023
|
+
let settled = false;
|
|
3024
|
+
let phase = "prepared";
|
|
3025
|
+
let registeredEventsForPass = false;
|
|
3026
|
+
const apply = () => {
|
|
3027
|
+
if (settled || phase !== "prepared") return;
|
|
3028
|
+
try {
|
|
3029
|
+
if (!releaseEvents) {
|
|
3030
|
+
releaseEvents = registerEventRoot(container);
|
|
3031
|
+
registeredEventsForPass = true;
|
|
3032
|
+
}
|
|
3033
|
+
pass.apply();
|
|
3034
|
+
phase = "applied";
|
|
3035
|
+
} catch (error) {
|
|
3036
|
+
phase = "discarded";
|
|
3037
|
+
settled = true;
|
|
3038
|
+
for (const failure of pass.discard()) reportUncaughtErrorLater(failure);
|
|
3039
|
+
if (registeredEventsForPass) {
|
|
3040
|
+
try {
|
|
3041
|
+
releaseEvents?.();
|
|
3042
|
+
} catch (cleanupError) {
|
|
3043
|
+
reportUncaughtErrorLater(cleanupError);
|
|
3044
|
+
}
|
|
3045
|
+
releaseEvents = null;
|
|
3046
|
+
registeredEventsForPass = false;
|
|
3047
|
+
}
|
|
3048
|
+
throw error;
|
|
3049
|
+
}
|
|
3050
|
+
};
|
|
3051
|
+
const publish = () => {
|
|
3052
|
+
if (settled) return;
|
|
3053
|
+
if (phase === "prepared") apply();
|
|
3054
|
+
if (phase !== "applied") throw new Error("[Askr] Cannot publish a render that was not applied.");
|
|
3055
|
+
settled = true;
|
|
3056
|
+
phase = "published";
|
|
3057
|
+
try {
|
|
3058
|
+
pass.publish();
|
|
3059
|
+
} finally {
|
|
3060
|
+
mounted = true;
|
|
3061
|
+
hydrate = false;
|
|
3062
|
+
}
|
|
3063
|
+
};
|
|
3064
|
+
const rollback = () => {
|
|
3065
|
+
if (settled || phase === "discarded") return [];
|
|
3066
|
+
settled = true;
|
|
3067
|
+
phase = "discarded";
|
|
3068
|
+
const errors = pass.rollback();
|
|
3069
|
+
if (registeredEventsForPass) {
|
|
3070
|
+
try {
|
|
3071
|
+
releaseEvents?.();
|
|
3072
|
+
} catch (error) {
|
|
3073
|
+
errors.push(error);
|
|
3074
|
+
}
|
|
3075
|
+
releaseEvents = null;
|
|
3076
|
+
registeredEventsForPass = false;
|
|
3077
|
+
}
|
|
3078
|
+
return errors;
|
|
3079
|
+
};
|
|
3080
|
+
const discard = () => {
|
|
3081
|
+
if (settled || phase === "discarded") return [];
|
|
3082
|
+
if (phase === "applied") return rollback();
|
|
3083
|
+
settled = true;
|
|
3084
|
+
phase = "discarded";
|
|
3085
|
+
return pass.discard();
|
|
3086
|
+
};
|
|
3087
|
+
return {
|
|
3088
|
+
apply,
|
|
3089
|
+
publish,
|
|
3090
|
+
commit() {
|
|
3091
|
+
apply();
|
|
3092
|
+
publish();
|
|
3093
|
+
},
|
|
3094
|
+
rollback,
|
|
3095
|
+
discard,
|
|
3096
|
+
get aborted() {
|
|
3097
|
+
return pass.commitAborted;
|
|
3098
|
+
}
|
|
3099
|
+
};
|
|
3100
|
+
}
|
|
3101
|
+
return {
|
|
3102
|
+
node,
|
|
3103
|
+
owner,
|
|
3104
|
+
prepare,
|
|
3105
|
+
render(value) {
|
|
3106
|
+
prepare(value).commit();
|
|
3107
|
+
},
|
|
3108
|
+
dispose() {
|
|
3109
|
+
const errors = [];
|
|
3110
|
+
for (const child of node.children) {
|
|
3111
|
+
for (const dom of collectDom(child)) dom.parentNode?.removeChild(dom);
|
|
3112
|
+
domNodes.release(child, errors);
|
|
3113
|
+
}
|
|
3114
|
+
node.children = [];
|
|
3115
|
+
mounted = false;
|
|
3116
|
+
owner.dispose(errors);
|
|
3117
|
+
releaseEvents?.();
|
|
3118
|
+
releaseEvents = null;
|
|
3119
|
+
return errors;
|
|
3120
|
+
}
|
|
3121
|
+
};
|
|
3122
|
+
}
|
|
3123
|
+
function restoreRootChildren(container, previous) {
|
|
3124
|
+
for (let index = 0; index < previous.length; index += 1) {
|
|
3125
|
+
const current = container.childNodes[index] ?? null;
|
|
3126
|
+
if (current !== previous[index]) container.insertBefore(previous[index], current);
|
|
3127
|
+
}
|
|
3128
|
+
while (container.childNodes.length > previous.length) container.removeChild(container.lastChild);
|
|
3129
|
+
}
|
|
3130
|
+
//#endregion
|
|
3131
|
+
//#region src/boot/root-element.ts
|
|
3132
|
+
function resolveRootElement(root) {
|
|
3133
|
+
if (typeof root !== "string") return root;
|
|
3134
|
+
const id = root.startsWith("#") ? root.slice(1) : root;
|
|
3135
|
+
return document.getElementById(id);
|
|
3136
|
+
}
|
|
3137
|
+
//#endregion
|
|
3138
|
+
//#region src/boot/root-lifecycle.ts
|
|
3139
|
+
/**
|
|
3140
|
+
* Application roots: one per mounted container element.
|
|
3141
|
+
*
|
|
3142
|
+
* An application root owns a core render root, its application runtime (route
|
|
3143
|
+
* data, data runtime), its default portal, and its cleanup callbacks. The
|
|
3144
|
+
* rendered tree is `RootView`, which provides the application runtime and
|
|
3145
|
+
* renders the current route handler followed by the automatic default portal
|
|
3146
|
+
* host. The root view keeps shared route shells in place across navigation.
|
|
3147
|
+
*/
|
|
3148
|
+
var AppRoot = class {
|
|
3149
|
+
constructor(element, component, hydrate) {
|
|
3150
|
+
this.cleanupStrict = false;
|
|
3151
|
+
this.generation = 0;
|
|
3152
|
+
this.routed = false;
|
|
3153
|
+
this.callbacks = /* @__PURE__ */ new Set();
|
|
3154
|
+
this.element = element;
|
|
3155
|
+
this.root = createRoot(element, {
|
|
3156
|
+
owner: null,
|
|
3157
|
+
hydrate
|
|
3158
|
+
});
|
|
3159
|
+
provideDefaultPortal(this.root.owner, element);
|
|
3160
|
+
this.component = component;
|
|
3161
|
+
this.handler = component;
|
|
3162
|
+
}
|
|
3163
|
+
view() {
|
|
3164
|
+
return {
|
|
3165
|
+
$$typeof: ELEMENT_TYPE,
|
|
3166
|
+
type: RootView,
|
|
3167
|
+
props: {
|
|
3168
|
+
handler: this.handler,
|
|
3169
|
+
runtime: this.appRuntime
|
|
3170
|
+
},
|
|
3171
|
+
key: this.routed ? 0 : this.generation
|
|
3172
|
+
};
|
|
3173
|
+
}
|
|
3174
|
+
};
|
|
3175
|
+
function RootView(props, context) {
|
|
3176
|
+
provideAppRuntime(props.runtime);
|
|
3177
|
+
const instance = currentComponent();
|
|
3178
|
+
if (instance) instance.name = props.handler.name || null;
|
|
3179
|
+
return props.handler({}, context);
|
|
3180
|
+
}
|
|
3181
|
+
const appRoots = /* @__PURE__ */ new WeakMap();
|
|
3182
|
+
let routedRootCount = 0;
|
|
3183
|
+
function registerRootCleanupCallback(rootElement, callback) {
|
|
3184
|
+
const app = appRoots.get(rootElement);
|
|
3185
|
+
if (!app) return () => {};
|
|
3186
|
+
app.callbacks.add(callback);
|
|
3187
|
+
return () => app.callbacks.delete(callback);
|
|
3188
|
+
}
|
|
3189
|
+
function disposeAppRoot(app) {
|
|
3190
|
+
if (appRoots.get(app.element) !== app) return;
|
|
3191
|
+
appRoots.delete(app.element);
|
|
3192
|
+
restoreInnerHTML(app.element);
|
|
3193
|
+
const errors = app.root.dispose();
|
|
3194
|
+
for (const callback of Array.from(app.callbacks)) try {
|
|
3195
|
+
callback();
|
|
3196
|
+
} catch (error) {
|
|
3197
|
+
errors.push(error);
|
|
3198
|
+
}
|
|
3199
|
+
app.callbacks.clear();
|
|
3200
|
+
unregisterAppInstance(app);
|
|
3201
|
+
if (app.routed) {
|
|
3202
|
+
app.routed = false;
|
|
3203
|
+
routedRootCount--;
|
|
3204
|
+
if (routedRootCount === 0) clearRouteState();
|
|
3205
|
+
}
|
|
3206
|
+
if (errors.length === 0) return;
|
|
3207
|
+
if (app.cleanupStrict) throw new AggregateError(errors, "cleanup failed for app root");
|
|
3208
|
+
reportUncaughtErrorLater(errors.length === 1 ? errors[0] : new AggregateError(errors, "cleanup failed for app root"));
|
|
3209
|
+
}
|
|
3210
|
+
/**
|
|
3211
|
+
* Clearing a root's markup with `innerHTML = ''` tears the app down first,
|
|
3212
|
+
* so its lifetimes end with the DOM they own.
|
|
3213
|
+
*/
|
|
3214
|
+
function interceptInnerHTML(app) {
|
|
3215
|
+
const element = app.element;
|
|
3216
|
+
const descriptor = Object.getOwnPropertyDescriptor(Element.prototype, "innerHTML") ?? Object.getOwnPropertyDescriptor(Object.getPrototypeOf(element), "innerHTML");
|
|
3217
|
+
if (!descriptor?.set || !descriptor.get) return;
|
|
3218
|
+
try {
|
|
3219
|
+
Object.defineProperty(element, "innerHTML", {
|
|
3220
|
+
configurable: true,
|
|
3221
|
+
get() {
|
|
3222
|
+
return descriptor.get.call(this);
|
|
3223
|
+
},
|
|
3224
|
+
set(value) {
|
|
3225
|
+
if (value === "" && appRoots.get(this) === app) {
|
|
3226
|
+
disposeAppRoot(app);
|
|
3227
|
+
if (appRoots.has(this)) return;
|
|
3228
|
+
}
|
|
3229
|
+
descriptor.set.call(this, value);
|
|
3230
|
+
}
|
|
3231
|
+
});
|
|
3232
|
+
} catch {}
|
|
3233
|
+
}
|
|
3234
|
+
function restoreInnerHTML(element) {
|
|
3235
|
+
if (Object.prototype.hasOwnProperty.call(element, "innerHTML")) Reflect.deleteProperty(element, "innerHTML");
|
|
3236
|
+
}
|
|
3237
|
+
/** Mount `component` at `rootElement`, or re-render the app already there. */
|
|
3238
|
+
function mountOrUpdate(rootElement, component, options) {
|
|
3239
|
+
ensureBrowserRuntime();
|
|
3240
|
+
const nonce = validateCspNonce(options?.cspNonce);
|
|
3241
|
+
let app = appRoots.get(rootElement);
|
|
3242
|
+
const replacingIsland = app !== void 0 && app.component !== component && !app.routed;
|
|
3243
|
+
if (!app) {
|
|
3244
|
+
app = new AppRoot(rootElement, component, options?.hydrate === true);
|
|
3245
|
+
appRoots.set(rootElement, app);
|
|
3246
|
+
interceptInnerHTML(app);
|
|
3247
|
+
} else if (app.component !== component) app.generation++;
|
|
3248
|
+
app.component = component;
|
|
3249
|
+
app.handler = wrapRootRouteHandler(component, nonce);
|
|
3250
|
+
app.cspNonce = nonce;
|
|
3251
|
+
app.appRuntime = options?.appRuntime;
|
|
3252
|
+
if (typeof options?.cleanupStrict === "boolean") app.cleanupStrict = options.cleanupStrict;
|
|
3253
|
+
app.root.render(app.view());
|
|
3254
|
+
options?.onCommit?.();
|
|
3255
|
+
flushSync();
|
|
3256
|
+
if (replacingIsland) {
|
|
3257
|
+
const callbacks = Array.from(app.callbacks);
|
|
3258
|
+
app.callbacks.clear();
|
|
3259
|
+
const errors = [];
|
|
3260
|
+
for (const callback of callbacks) try {
|
|
3261
|
+
callback();
|
|
3262
|
+
} catch (error) {
|
|
3263
|
+
errors.push(error);
|
|
3264
|
+
}
|
|
3265
|
+
if (errors.length > 0) {
|
|
3266
|
+
if (app.cleanupStrict) throw new AggregateError(errors, "cleanup failed for app root");
|
|
3267
|
+
reportUncaughtErrorLater(errors.length === 1 ? errors[0] : new AggregateError(errors, "cleanup failed for app root"));
|
|
3268
|
+
}
|
|
3269
|
+
}
|
|
3270
|
+
return app;
|
|
3271
|
+
}
|
|
3272
|
+
async function registerAppNavigation(rootElement, path, source) {
|
|
3273
|
+
ensureBrowserRuntime();
|
|
3274
|
+
const app = appRoots.get(rootElement);
|
|
3275
|
+
if (!app) throw new Error("Internal error: app instance missing");
|
|
3276
|
+
if (!app.routed) {
|
|
3277
|
+
app.routed = true;
|
|
3278
|
+
routedRootCount++;
|
|
3279
|
+
}
|
|
3280
|
+
registerAppInstance(app, path, source);
|
|
3281
|
+
initializeNavigation();
|
|
3282
|
+
}
|
|
3283
|
+
function activateHydrationBoundary(rootElement, boundary) {
|
|
3284
|
+
const app = appRoots.get(rootElement);
|
|
3285
|
+
if (!app || !rootElement.contains(boundary)) return false;
|
|
3286
|
+
const host = dormantHostFor(boundary);
|
|
3287
|
+
if (!host) return false;
|
|
3288
|
+
let parent = host.parent;
|
|
3289
|
+
while (parent && parent.kind !== 5) parent = parent.parent;
|
|
3290
|
+
if (parent !== app.root.node) return false;
|
|
3291
|
+
const pass = new Pass();
|
|
3292
|
+
try {
|
|
3293
|
+
pass.run(() => hydrateDormantHost(pass, host));
|
|
3294
|
+
} catch (error) {
|
|
3295
|
+
for (const failure of pass.discard()) reportUncaughtErrorLater(failure);
|
|
3296
|
+
throw clarifyRenderOverflow(error);
|
|
3297
|
+
}
|
|
3298
|
+
pass.commit();
|
|
3299
|
+
flushSync();
|
|
3300
|
+
return true;
|
|
3301
|
+
}
|
|
3302
|
+
/**
|
|
3303
|
+
* Tear down the app mounted at `root`: its content, lifetimes, and cleanup
|
|
3304
|
+
* callbacks, and its route registration.
|
|
3305
|
+
*/
|
|
3306
|
+
function cleanupApp(root) {
|
|
3307
|
+
const rootElement = resolveRootElement(root);
|
|
3308
|
+
if (!rootElement) return;
|
|
3309
|
+
const app = appRoots.get(rootElement);
|
|
3310
|
+
if (app) disposeAppRoot(app);
|
|
3311
|
+
}
|
|
3312
|
+
/** Check whether an app is currently mounted at `root`. */
|
|
3313
|
+
function hasApp(root) {
|
|
3314
|
+
const rootElement = resolveRootElement(root);
|
|
3315
|
+
return rootElement ? appRoots.has(rootElement) : false;
|
|
3316
|
+
}
|
|
3317
|
+
//#endregion
|
|
3318
|
+
//#region src/boot/islands.ts
|
|
3319
|
+
/**
|
|
3320
|
+
* createIsland: Enhances existing DOM (no router, mounts once)
|
|
3321
|
+
*/
|
|
3322
|
+
function createIsland(config) {
|
|
3323
|
+
assertExecutionModel("islands");
|
|
3324
|
+
if (!config || typeof config !== "object") throw new Error("createIsland requires a config object");
|
|
3325
|
+
if (typeof config.component !== "function") throw new Error("createIsland: component must be a function");
|
|
3326
|
+
validateCspNonce(config.cspNonce);
|
|
3327
|
+
const rootElement = resolveRootElement(config.root);
|
|
3328
|
+
if (!rootElement) throw new Error(`Root element not found: ${config.root}`);
|
|
3329
|
+
if ("routes" in config) throw new Error("createIsland does not accept routes; use createSPA for routed apps");
|
|
3330
|
+
if (hasRegisteredRoutes()) throw new Error("Routes are not supported with islands. Use createSPA or hydrateSPA with a route registry instead.");
|
|
3331
|
+
mountOrUpdate(rootElement, config.component, {
|
|
3332
|
+
cleanupStrict: config.cleanupStrict,
|
|
3333
|
+
cspNonce: config.cspNonce,
|
|
3334
|
+
appRuntime: createAppRenderRuntime({ dataRuntime: config.dataRuntime ?? getDefaultDataRuntime() })
|
|
3335
|
+
});
|
|
3336
|
+
}
|
|
3337
|
+
/**
|
|
3338
|
+
* createIslands: Enhances one or more existing DOM roots (no router).
|
|
3339
|
+
* The only public islands constructor.
|
|
3340
|
+
*/
|
|
3341
|
+
function createIslands(config) {
|
|
3342
|
+
assertExecutionModel("islands");
|
|
3343
|
+
if (!config || typeof config !== "object") throw new Error("createIslands requires a config object");
|
|
3344
|
+
validateCspNonce(config.cspNonce);
|
|
3345
|
+
if (!Array.isArray(config.islands) || config.islands.length === 0) throw new Error("createIslands requires a non-empty islands array");
|
|
3346
|
+
for (const island of config.islands) createIsland({
|
|
3347
|
+
...island,
|
|
3348
|
+
cspNonce: island.cspNonce ?? config.cspNonce
|
|
3349
|
+
});
|
|
3350
|
+
}
|
|
3351
|
+
//#endregion
|
|
3352
|
+
//#region src/boot/route-startup.ts
|
|
3353
|
+
const MAX_INITIAL_ROUTE_REDIRECTS = 20;
|
|
3354
|
+
function bindResolvedRouteHandler(resolved) {
|
|
3355
|
+
return () => resolved.handler(resolved.params);
|
|
3356
|
+
}
|
|
3357
|
+
function createDeniedStatusNode(status) {
|
|
3358
|
+
return {
|
|
3359
|
+
type: "div",
|
|
3360
|
+
props: { "data-route-denied": String(status) },
|
|
3361
|
+
children: [String(status)]
|
|
3362
|
+
};
|
|
3363
|
+
}
|
|
3364
|
+
function bindDeniedStatus(status) {
|
|
3365
|
+
return () => createDeniedStatusNode(status);
|
|
3366
|
+
}
|
|
3367
|
+
function bindDeniedRouteHandler(status) {
|
|
3368
|
+
return () => createDeniedStatusNode(status);
|
|
3369
|
+
}
|
|
3370
|
+
async function reconcileInitialRouteMetadata(resolved) {
|
|
3371
|
+
if (resolved?.kind !== "render" || !resolved.record) return;
|
|
3372
|
+
if (!resolved.record.options.title && !resolved.record.options.meta && !resolved.record.metaChain?.length) return;
|
|
3373
|
+
const context = getRouteRenderContext(resolved);
|
|
3374
|
+
if (!context) return;
|
|
3375
|
+
reconcileRouteMeta(await resolveRouteMeta(resolved.record, context));
|
|
3376
|
+
}
|
|
3377
|
+
/**
|
|
3378
|
+
* Resolve the route for the current location, following redirects (and
|
|
3379
|
+
* replacing the history entry for each) until a render, deny, or no-match
|
|
3380
|
+
* result is reached. Returns `null` when a redirect leaves the origin: the
|
|
3381
|
+
* browser is loading that document, so there is nothing to mount.
|
|
3382
|
+
*/
|
|
3383
|
+
async function resolveInitialRoute(auth, source) {
|
|
3384
|
+
let path = typeof window !== "undefined" ? window.location.pathname : "/";
|
|
3385
|
+
let href = typeof window !== "undefined" ? `${window.location.pathname}${window.location.search}${window.location.hash}` : path;
|
|
3386
|
+
const visited = /* @__PURE__ */ new Set();
|
|
3387
|
+
for (let redirects = 0; redirects <= MAX_INITIAL_ROUTE_REDIRECTS; redirects++) {
|
|
3388
|
+
if (visited.has(href)) throw new Error(`[Askr] Route redirect cycle detected at ${href}.`);
|
|
3389
|
+
visited.add(href);
|
|
3390
|
+
if (!source?.registry) throw new TypeError("resolveInitialRoute requires a route registry.");
|
|
3391
|
+
const resolved = await resolveRouteRequest(href, {
|
|
3392
|
+
registry: source.registry,
|
|
3393
|
+
auth,
|
|
3394
|
+
load: source.load,
|
|
3395
|
+
authContext: source.authContext,
|
|
3396
|
+
dataRuntime: source.dataRuntime
|
|
3397
|
+
});
|
|
3398
|
+
if (!resolved || resolved.kind !== "redirect") return {
|
|
3399
|
+
path,
|
|
3400
|
+
href,
|
|
3401
|
+
resolved
|
|
3402
|
+
};
|
|
3403
|
+
const base = typeof window !== "undefined" ? window.location.href : new URL(href, "http://localhost").href;
|
|
3404
|
+
const redirectTarget = resolveNavigationUrl(resolved.to, base);
|
|
3405
|
+
if (redirectTarget.origin !== new URL(base).origin) {
|
|
3406
|
+
if (typeof window !== "undefined") loadDocument(redirectTarget.href, "replace");
|
|
3407
|
+
return null;
|
|
3408
|
+
}
|
|
3409
|
+
const redirectHref = `${redirectTarget.pathname}${redirectTarget.search}${redirectTarget.hash}`;
|
|
3410
|
+
if (typeof window !== "undefined") window.history.replaceState({
|
|
3411
|
+
path: redirectHref,
|
|
3412
|
+
askrIndex: readHistoryIndex(window.history.state)
|
|
3413
|
+
}, "", redirectTarget.href);
|
|
3414
|
+
path = redirectTarget.pathname;
|
|
3415
|
+
href = redirectHref;
|
|
3416
|
+
}
|
|
3417
|
+
throw new Error(`[Askr] Route redirect limit exceeded (${MAX_INITIAL_ROUTE_REDIRECTS}).`);
|
|
3418
|
+
}
|
|
3419
|
+
//#endregion
|
|
3420
|
+
//#region src/boot/spa.ts
|
|
3421
|
+
/**
|
|
3422
|
+
* App bootstrap and mount
|
|
3423
|
+
*/
|
|
3424
|
+
/**
|
|
3425
|
+
* createSPA: Initializes the router and mounts the app with an explicit route registry.
|
|
3426
|
+
* ```ts
|
|
3427
|
+
* import { createRouteRegistry } from '@askrjs/askr/router';
|
|
3428
|
+
* const registry = createRouteRegistry(() => { ... });
|
|
3429
|
+
* await createSPA({ root: '#app', registry });
|
|
3430
|
+
* ```
|
|
3431
|
+
*
|
|
3432
|
+
*/
|
|
3433
|
+
async function createSPA(config) {
|
|
3434
|
+
assertExecutionModel("spa");
|
|
3435
|
+
if (!config || typeof config !== "object") throw new Error("createSPA requires a config object");
|
|
3436
|
+
validateCspNonce(config.cspNonce);
|
|
3437
|
+
if (!config.registry) throw new Error("createSPA requires a route registry.");
|
|
3438
|
+
const { manifest } = config.registry;
|
|
3439
|
+
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.");
|
|
3440
|
+
const rootElement = resolveRootElement(config.root);
|
|
3441
|
+
if (!rootElement) throw new Error(`Root element not found: ${config.root}`);
|
|
3442
|
+
configureScrollRestoration(config.scrollRestoration);
|
|
3443
|
+
clearRouteState();
|
|
3444
|
+
_applyManifest(manifest);
|
|
3445
|
+
const routeAuth = config.auth ?? manifest.auth;
|
|
3446
|
+
const dataRuntime = config.dataRuntime ?? getDefaultDataRuntime();
|
|
3447
|
+
const appRouteSource = {
|
|
3448
|
+
registry: config.registry,
|
|
3449
|
+
auth: routeAuth
|
|
3450
|
+
};
|
|
3451
|
+
_setActiveRouteAuthOptions(routeAuth);
|
|
3452
|
+
if (isProductionEnvironment()) lockRouteRegistration();
|
|
3453
|
+
const initialRoute = await resolveInitialRoute(routeAuth, {
|
|
3454
|
+
registry: config.registry,
|
|
3455
|
+
dataRuntime
|
|
3456
|
+
});
|
|
3457
|
+
if (!initialRoute) return;
|
|
3458
|
+
const { path, href: currentUrl, resolved } = initialRoute;
|
|
3459
|
+
setServerLocation(currentUrl);
|
|
3460
|
+
const appRuntime = createAppRenderRuntime({
|
|
3461
|
+
...resolved?.kind === "render" && hasRouteRenderData(resolved) ? {
|
|
3462
|
+
route: getRouteRenderData(resolved),
|
|
3463
|
+
hasRoute: true
|
|
3464
|
+
} : {},
|
|
3465
|
+
dataRuntime,
|
|
3466
|
+
routeRegistry: config.registry,
|
|
3467
|
+
routeAuth
|
|
3468
|
+
});
|
|
3469
|
+
if (!resolved) {
|
|
3470
|
+
mountOrUpdate(rootElement, () => ({
|
|
3471
|
+
type: "div",
|
|
3472
|
+
children: []
|
|
3473
|
+
}), {
|
|
3474
|
+
cleanupStrict: config.cleanupStrict,
|
|
3475
|
+
appRuntime,
|
|
3476
|
+
cspNonce: config.cspNonce
|
|
3477
|
+
});
|
|
3478
|
+
await registerAppNavigation(rootElement, path, {
|
|
3479
|
+
...appRouteSource,
|
|
3480
|
+
runtime: appRuntime
|
|
3481
|
+
});
|
|
3482
|
+
return;
|
|
3483
|
+
}
|
|
3484
|
+
await reconcileInitialRouteMetadata(resolved);
|
|
3485
|
+
mountOrUpdate(rootElement, resolved.kind === "deny" ? bindDeniedStatus(resolved.status) : bindResolvedRouteHandler({
|
|
3486
|
+
handler: resolved.handler,
|
|
3487
|
+
params: resolved.params
|
|
3488
|
+
}), {
|
|
3489
|
+
cleanupStrict: config.cleanupStrict,
|
|
3490
|
+
appRuntime,
|
|
3491
|
+
cspNonce: config.cspNonce
|
|
3492
|
+
});
|
|
3493
|
+
await registerAppNavigation(rootElement, path, {
|
|
3494
|
+
...appRouteSource,
|
|
3495
|
+
runtime: appRuntime
|
|
3496
|
+
});
|
|
3497
|
+
}
|
|
3498
|
+
//#endregion
|
|
3499
|
+
//#region src/boot/hydration.ts
|
|
3500
|
+
const DEFERRED_PAYLOAD = "__askr_deferred__";
|
|
3501
|
+
const SSR_STYLE_REGISTRY_ATTR = "data-askr-style-registry";
|
|
3502
|
+
function applyDeferredStreamPatches(rootElement) {
|
|
3503
|
+
const patches = Array.from(rootElement.querySelectorAll("template[data-askr-deferred-patch]"));
|
|
3504
|
+
for (const template of patches) {
|
|
3505
|
+
const id = template.getAttribute("data-askr-deferred-patch");
|
|
3506
|
+
if (!id) continue;
|
|
3507
|
+
const boundary = Array.from(rootElement.querySelectorAll("askr-resolve[data-askr-deferred]")).find((candidate) => candidate.getAttribute("data-askr-deferred") === id);
|
|
3508
|
+
if (boundary) boundary.replaceWith(template.content.cloneNode(true));
|
|
3509
|
+
template.remove();
|
|
3510
|
+
for (const script of Array.from(rootElement.querySelectorAll("script[data-askr-deferred-apply]"))) if (script.getAttribute("data-askr-deferred-apply") === id) script.remove();
|
|
3511
|
+
}
|
|
3512
|
+
}
|
|
3513
|
+
function hydrationReviver(_key, value) {
|
|
3514
|
+
if (!value || typeof value !== "object") return value;
|
|
3515
|
+
const payload = value;
|
|
3516
|
+
if (payload[DEFERRED_PAYLOAD] === "fulfilled") return reviveDeferredValue("fulfilled", payload.value, void 0);
|
|
3517
|
+
if (payload[DEFERRED_PAYLOAD] === "rejected") return reviveDeferredValue("rejected", void 0, payload.error);
|
|
3518
|
+
return value;
|
|
3519
|
+
}
|
|
3520
|
+
/**
|
|
3521
|
+
* Relocate the SSR style registry carrier out of the hydration root.
|
|
3522
|
+
*
|
|
3523
|
+
* `@askrjs/server` prepends the collected styles to the page body, so they land
|
|
3524
|
+
* as a direct child of the mount root. That extra element makes the root's
|
|
3525
|
+
* child list disagree with the rendered tree, which costs the app in-place
|
|
3526
|
+
* adoption and discards the carried CSS during reconciliation. The carrier is
|
|
3527
|
+
* transport rather than application markup, so move it into `<head>`, where it
|
|
3528
|
+
* still applies and no longer participates in reconciliation.
|
|
3529
|
+
*
|
|
3530
|
+
* Deferred style patches resolve the carrier with a document-wide query, so
|
|
3531
|
+
* they keep working from its new position.
|
|
3532
|
+
*/
|
|
3533
|
+
function adoptSsrStyleCarriers(rootElement) {
|
|
3534
|
+
const head = document.head;
|
|
3535
|
+
if (!head) return;
|
|
3536
|
+
for (const child of Array.from(rootElement.children)) if (child instanceof HTMLStyleElement && child.hasAttribute(SSR_STYLE_REGISTRY_ATTR)) head.append(child);
|
|
3537
|
+
}
|
|
3538
|
+
function takeHydrationRenderData(rootElement) {
|
|
3539
|
+
for (const child of Array.from(rootElement.children)) if (child instanceof HTMLScriptElement && child.getAttribute("data-askr-render-data") === "true") {
|
|
3540
|
+
const raw = child.textContent ?? "";
|
|
3541
|
+
child.remove();
|
|
3542
|
+
if (!raw) return pageRenderEnvelope(void 0);
|
|
3543
|
+
try {
|
|
3544
|
+
return pageRenderEnvelope(JSON.parse(raw, hydrationReviver));
|
|
3545
|
+
} catch (err) {
|
|
3546
|
+
const error = /* @__PURE__ */ new Error("[Askr] Failed to parse embedded SSR render data during hydration.");
|
|
3547
|
+
error.cause = err;
|
|
3548
|
+
throw error;
|
|
3549
|
+
}
|
|
3550
|
+
}
|
|
3551
|
+
return null;
|
|
3552
|
+
}
|
|
3553
|
+
function markSkippedElements(root, skipSelectors) {
|
|
3554
|
+
if (skipSelectors.length === 0) return;
|
|
3555
|
+
const selectorList = Array.from(new Set(skipSelectors)).join(", ");
|
|
3556
|
+
root.querySelectorAll(selectorList).forEach((el) => el.setAttribute("data-skip-hydrate", "true"));
|
|
3557
|
+
}
|
|
3558
|
+
function collectDeferredBelowFoldBoundaries(root, foldY) {
|
|
3559
|
+
const boundaries = [];
|
|
3560
|
+
const stack = [];
|
|
3561
|
+
for (let index = root.children.length - 1; index >= 0; index -= 1) stack.push(root.children[index]);
|
|
3562
|
+
while (stack.length > 0) {
|
|
3563
|
+
const element = stack.pop();
|
|
3564
|
+
if (element.hasAttribute("data-skip-hydrate")) continue;
|
|
3565
|
+
if (element.getBoundingClientRect().top >= foldY) {
|
|
3566
|
+
element.setAttribute("data-skip-hydrate", "true");
|
|
3567
|
+
boundaries.push(element);
|
|
3568
|
+
continue;
|
|
3569
|
+
}
|
|
3570
|
+
for (let index = element.children.length - 1; index >= 0; index -= 1) stack.push(element.children[index]);
|
|
3571
|
+
}
|
|
3572
|
+
return boundaries;
|
|
3573
|
+
}
|
|
3574
|
+
function activateVisibleDeferredBoundaries(root, boundaries, foldY, activate, reportedDepthErrors) {
|
|
3575
|
+
let activated = false;
|
|
3576
|
+
let remaining = 0;
|
|
3577
|
+
for (const element of boundaries) {
|
|
3578
|
+
if (!element.hasAttribute("data-skip-hydrate")) continue;
|
|
3579
|
+
if (element.getBoundingClientRect().top < foldY) try {
|
|
3580
|
+
if (activate(root, element)) {
|
|
3581
|
+
activated = true;
|
|
3582
|
+
reportedDepthErrors.delete(element);
|
|
3583
|
+
}
|
|
3584
|
+
} catch (error) {
|
|
3585
|
+
const clarified = clarifyRenderOverflow(error);
|
|
3586
|
+
if (clarified instanceof RenderDepthError && !reportedDepthErrors.has(element)) {
|
|
3587
|
+
reportedDepthErrors.add(element);
|
|
3588
|
+
reportUncaughtErrorLater(clarified);
|
|
3589
|
+
}
|
|
3590
|
+
}
|
|
3591
|
+
if (element.hasAttribute("data-skip-hydrate")) remaining += 1;
|
|
3592
|
+
}
|
|
3593
|
+
return {
|
|
3594
|
+
activated,
|
|
3595
|
+
remaining
|
|
3596
|
+
};
|
|
3597
|
+
}
|
|
3598
|
+
function queueIdleWork(work) {
|
|
3599
|
+
return new Promise((resolve, reject) => {
|
|
3600
|
+
const run = () => {
|
|
3601
|
+
try {
|
|
3602
|
+
work();
|
|
3603
|
+
resolve();
|
|
3604
|
+
} catch (error) {
|
|
3605
|
+
reject(error);
|
|
3606
|
+
}
|
|
3607
|
+
};
|
|
3608
|
+
if (typeof requestIdleCallback !== "undefined") {
|
|
3609
|
+
requestIdleCallback(run, { timeout: 2e3 });
|
|
3610
|
+
return;
|
|
3611
|
+
}
|
|
3612
|
+
setTimeout(run, 0);
|
|
3613
|
+
});
|
|
3614
|
+
}
|
|
3615
|
+
function shouldVerifyHydrationMarkup(config) {
|
|
3616
|
+
const explicit = config.hydrate?.verifyMarkup;
|
|
3617
|
+
if (typeof explicit === "boolean") return explicit;
|
|
3618
|
+
return !isProductionEnvironment();
|
|
3619
|
+
}
|
|
3620
|
+
async function applySelectiveHydration(rootElement, resolved, path, cleanupStrict, hydrateOptions, source, hooks, interactionReplay) {
|
|
3621
|
+
const hasBelowFoldDeferral = !!hydrateOptions.deferBelowFold;
|
|
3622
|
+
let releaseSelectiveHydrationResources = () => {};
|
|
3623
|
+
let registerSelectiveHydrationCleanup = () => {};
|
|
3624
|
+
if (hydrateOptions.skipSelectors?.length) markSkippedElements(rootElement, hydrateOptions.skipSelectors);
|
|
3625
|
+
let deferredBoundaries = [];
|
|
3626
|
+
const reportedDepthErrors = /* @__PURE__ */ new WeakSet();
|
|
3627
|
+
if (hydrateOptions.deferBelowFold) {
|
|
3628
|
+
const foldY = hydrateOptions.foldThreshold ?? window.innerHeight;
|
|
3629
|
+
deferredBoundaries = collectDeferredBelowFoldBoundaries(rootElement, foldY);
|
|
3630
|
+
interactionReplay.registerDeferredBoundaries(deferredBoundaries);
|
|
3631
|
+
let selectiveHydrationResourcesReleased = false;
|
|
3632
|
+
let unregisterRootCleanupCallback = () => {};
|
|
3633
|
+
function handleScroll() {
|
|
3634
|
+
const { activated, remaining } = activateVisibleDeferredBoundaries(rootElement, deferredBoundaries, foldY, hooks.activateHydrationBoundary, reportedDepthErrors);
|
|
3635
|
+
if (!activated) return;
|
|
3636
|
+
if (remaining === 0) releaseSelectiveHydrationResources();
|
|
3637
|
+
}
|
|
3638
|
+
releaseSelectiveHydrationResources = () => {
|
|
3639
|
+
if (selectiveHydrationResourcesReleased) return;
|
|
3640
|
+
selectiveHydrationResourcesReleased = true;
|
|
3641
|
+
unregisterRootCleanupCallback();
|
|
3642
|
+
window.removeEventListener("scroll", handleScroll);
|
|
3643
|
+
interactionReplay.clearDeferredBoundaries();
|
|
3644
|
+
};
|
|
3645
|
+
interactionReplay.setOnDeferredBoundariesDrained(releaseSelectiveHydrationResources);
|
|
3646
|
+
registerSelectiveHydrationCleanup = () => {
|
|
3647
|
+
unregisterRootCleanupCallback = hooks.registerRootCleanupCallback(rootElement, releaseSelectiveHydrationResources);
|
|
3648
|
+
};
|
|
3649
|
+
window.addEventListener("scroll", handleScroll, { passive: true });
|
|
3650
|
+
}
|
|
3651
|
+
if (hydrateOptions.deferUntilIdle && !hasBelowFoldDeferral) {
|
|
3652
|
+
await queueIdleWork(() => {
|
|
3653
|
+
hooks.mountOrUpdate(rootElement, (() => resolved.handler(resolved.params)), {
|
|
3654
|
+
cleanupStrict,
|
|
3655
|
+
appRuntime: source?.runtime
|
|
3656
|
+
});
|
|
3657
|
+
});
|
|
3658
|
+
await hooks.registerAppNavigation(rootElement, path, source);
|
|
3659
|
+
return;
|
|
3660
|
+
}
|
|
3661
|
+
try {
|
|
3662
|
+
hooks.mountOrUpdate(rootElement, (() => resolved.handler(resolved.params)), {
|
|
3663
|
+
cleanupStrict,
|
|
3664
|
+
appRuntime: source?.runtime
|
|
3665
|
+
});
|
|
3666
|
+
registerSelectiveHydrationCleanup();
|
|
3667
|
+
await hooks.registerAppNavigation(rootElement, path, source);
|
|
3668
|
+
} catch (error) {
|
|
3669
|
+
releaseSelectiveHydrationResources();
|
|
3670
|
+
throw error;
|
|
3671
|
+
}
|
|
3672
|
+
if (hydrateOptions.deferUntilIdle && deferredBoundaries.length > 0) await queueIdleWork(() => {
|
|
3673
|
+
try {
|
|
3674
|
+
activateVisibleDeferredBoundaries(rootElement, deferredBoundaries, Number.POSITIVE_INFINITY, hooks.activateHydrationBoundary, reportedDepthErrors);
|
|
3675
|
+
} finally {
|
|
3676
|
+
releaseSelectiveHydrationResources();
|
|
3677
|
+
}
|
|
3678
|
+
});
|
|
3679
|
+
if (deferredBoundaries.length === 0) releaseSelectiveHydrationResources();
|
|
3680
|
+
}
|
|
3681
|
+
//#endregion
|
|
3682
|
+
//#region src/boot/hydration-interaction-replay.ts
|
|
3683
|
+
const HYDRATION_INTERACTION_TYPES = [
|
|
3684
|
+
"click",
|
|
3685
|
+
"dblclick",
|
|
3686
|
+
"pointerdown",
|
|
3687
|
+
"pointerup",
|
|
3688
|
+
"keydown",
|
|
3689
|
+
"keyup",
|
|
3690
|
+
"input",
|
|
3691
|
+
"change",
|
|
3692
|
+
"submit"
|
|
3693
|
+
];
|
|
3694
|
+
function cloneInteractionEvent(event) {
|
|
3695
|
+
const EventConstructor = event.constructor;
|
|
3696
|
+
const eventWithDetails = event;
|
|
3697
|
+
const init = {
|
|
3698
|
+
bubbles: event.bubbles,
|
|
3699
|
+
cancelable: event.cancelable,
|
|
3700
|
+
composed: event.composed,
|
|
3701
|
+
detail: eventWithDetails.detail,
|
|
3702
|
+
view: eventWithDetails.view,
|
|
3703
|
+
key: eventWithDetails.key,
|
|
3704
|
+
code: eventWithDetails.code,
|
|
3705
|
+
location: eventWithDetails.location,
|
|
3706
|
+
repeat: eventWithDetails.repeat,
|
|
3707
|
+
isComposing: eventWithDetails.isComposing,
|
|
3708
|
+
ctrlKey: eventWithDetails.ctrlKey,
|
|
3709
|
+
shiftKey: eventWithDetails.shiftKey,
|
|
3710
|
+
altKey: eventWithDetails.altKey,
|
|
3711
|
+
metaKey: eventWithDetails.metaKey,
|
|
3712
|
+
button: eventWithDetails.button,
|
|
3713
|
+
buttons: eventWithDetails.buttons,
|
|
3714
|
+
clientX: eventWithDetails.clientX,
|
|
3715
|
+
clientY: eventWithDetails.clientY,
|
|
3716
|
+
screenX: eventWithDetails.screenX,
|
|
3717
|
+
screenY: eventWithDetails.screenY,
|
|
3718
|
+
pointerId: eventWithDetails.pointerId,
|
|
3719
|
+
pointerType: eventWithDetails.pointerType,
|
|
3720
|
+
isPrimary: eventWithDetails.isPrimary,
|
|
3721
|
+
data: eventWithDetails.data,
|
|
3722
|
+
inputType: eventWithDetails.inputType
|
|
3723
|
+
};
|
|
3724
|
+
try {
|
|
3725
|
+
return new EventConstructor(event.type, init);
|
|
3726
|
+
} catch {
|
|
3727
|
+
return new Event(event.type, {
|
|
3728
|
+
bubbles: event.bubbles,
|
|
3729
|
+
cancelable: event.cancelable,
|
|
3730
|
+
composed: event.composed
|
|
3731
|
+
});
|
|
3732
|
+
}
|
|
3733
|
+
}
|
|
3734
|
+
/**
|
|
3735
|
+
* Capture discrete interactions before hydrated handlers exist, then replay
|
|
3736
|
+
* them after the listener transaction commits. Deferred boundaries remain
|
|
3737
|
+
* interaction-activatable until they are revealed or the root is cleaned up.
|
|
3738
|
+
*/
|
|
3739
|
+
function beginHydrationInteractionReplay(root, activateBoundary, permanentSkipSelectors = []) {
|
|
3740
|
+
const queued = [];
|
|
3741
|
+
const deferredBoundaries = /* @__PURE__ */ new Set();
|
|
3742
|
+
let active = true;
|
|
3743
|
+
let hydratingRoot = true;
|
|
3744
|
+
let replaying = false;
|
|
3745
|
+
let hadDeferredBoundaries = false;
|
|
3746
|
+
let onDeferredBoundariesDrained;
|
|
3747
|
+
function isPermanentlySkipped(target) {
|
|
3748
|
+
const element = target instanceof Element ? target : target instanceof Node ? target.parentElement : null;
|
|
3749
|
+
if (!element) return false;
|
|
3750
|
+
return permanentSkipSelectors.some((selector) => {
|
|
3751
|
+
try {
|
|
3752
|
+
return element.closest(selector) !== null;
|
|
3753
|
+
} catch {
|
|
3754
|
+
return false;
|
|
3755
|
+
}
|
|
3756
|
+
});
|
|
3757
|
+
}
|
|
3758
|
+
function findDeferredBoundary(target) {
|
|
3759
|
+
if (!(target instanceof Node)) return;
|
|
3760
|
+
for (const boundary of deferredBoundaries) if (boundary.hasAttribute("data-skip-hydrate") && boundary.contains(target)) return boundary;
|
|
3761
|
+
}
|
|
3762
|
+
function removeCaptureListeners() {
|
|
3763
|
+
if (!active) return;
|
|
3764
|
+
active = false;
|
|
3765
|
+
for (const type of HYDRATION_INTERACTION_TYPES) root.removeEventListener(type, captureInteraction, true);
|
|
3766
|
+
}
|
|
3767
|
+
function releaseIfFinished() {
|
|
3768
|
+
if (!hydratingRoot && deferredBoundaries.size === 0) removeCaptureListeners();
|
|
3769
|
+
}
|
|
3770
|
+
function notifyIfDeferredBoundariesDrained() {
|
|
3771
|
+
if (hadDeferredBoundaries && deferredBoundaries.size === 0) onDeferredBoundariesDrained?.();
|
|
3772
|
+
}
|
|
3773
|
+
function replayInteraction(interaction) {
|
|
3774
|
+
if (!(interaction.target instanceof Node) || !root.contains(interaction.target)) return;
|
|
3775
|
+
const boundary = findDeferredBoundary(interaction.target);
|
|
3776
|
+
if (boundary) {
|
|
3777
|
+
try {
|
|
3778
|
+
if (!activateBoundary(boundary)) return;
|
|
3779
|
+
} catch {
|
|
3780
|
+
return;
|
|
3781
|
+
}
|
|
3782
|
+
deferredBoundaries.delete(boundary);
|
|
3783
|
+
notifyIfDeferredBoundariesDrained();
|
|
3784
|
+
}
|
|
3785
|
+
replaying = true;
|
|
3786
|
+
try {
|
|
3787
|
+
interaction.target.dispatchEvent(interaction.event);
|
|
3788
|
+
} finally {
|
|
3789
|
+
replaying = false;
|
|
3790
|
+
}
|
|
3791
|
+
releaseIfFinished();
|
|
3792
|
+
}
|
|
3793
|
+
function captureInteraction(event) {
|
|
3794
|
+
if (!active || replaying) return;
|
|
3795
|
+
const target = event.target;
|
|
3796
|
+
if (!target || isPermanentlySkipped(target)) return;
|
|
3797
|
+
const boundary = findDeferredBoundary(target);
|
|
3798
|
+
if (!hydratingRoot && !boundary) return;
|
|
3799
|
+
event.preventDefault();
|
|
3800
|
+
event.stopImmediatePropagation();
|
|
3801
|
+
const interaction = {
|
|
3802
|
+
target,
|
|
3803
|
+
event: cloneInteractionEvent(event)
|
|
3804
|
+
};
|
|
3805
|
+
if (hydratingRoot) {
|
|
3806
|
+
queued.push(interaction);
|
|
3807
|
+
return;
|
|
3808
|
+
}
|
|
3809
|
+
replayInteraction(interaction);
|
|
3810
|
+
}
|
|
3811
|
+
const installedTypes = [];
|
|
3812
|
+
try {
|
|
3813
|
+
for (const type of HYDRATION_INTERACTION_TYPES) {
|
|
3814
|
+
root.addEventListener(type, captureInteraction, true);
|
|
3815
|
+
installedTypes.push(type);
|
|
3816
|
+
}
|
|
3817
|
+
} catch (error) {
|
|
3818
|
+
active = false;
|
|
3819
|
+
for (const type of installedTypes) root.removeEventListener(type, captureInteraction, true);
|
|
3820
|
+
throw error;
|
|
3821
|
+
}
|
|
3822
|
+
return {
|
|
3823
|
+
registerDeferredBoundaries(boundaries) {
|
|
3824
|
+
for (const boundary of boundaries) deferredBoundaries.add(boundary);
|
|
3825
|
+
hadDeferredBoundaries ||= boundaries.length > 0;
|
|
3826
|
+
},
|
|
3827
|
+
setOnDeferredBoundariesDrained(callback) {
|
|
3828
|
+
onDeferredBoundariesDrained = callback;
|
|
3829
|
+
},
|
|
3830
|
+
clearDeferredBoundaries() {
|
|
3831
|
+
deferredBoundaries.clear();
|
|
3832
|
+
releaseIfFinished();
|
|
3833
|
+
},
|
|
3834
|
+
complete() {
|
|
3835
|
+
if (!active || !hydratingRoot) return;
|
|
3836
|
+
hydratingRoot = false;
|
|
3837
|
+
const pending = queued.splice(0);
|
|
3838
|
+
for (const interaction of pending) replayInteraction(interaction);
|
|
3839
|
+
releaseIfFinished();
|
|
3840
|
+
},
|
|
3841
|
+
abort() {
|
|
3842
|
+
queued.length = 0;
|
|
3843
|
+
deferredBoundaries.clear();
|
|
3844
|
+
onDeferredBoundariesDrained = void 0;
|
|
3845
|
+
hydratingRoot = false;
|
|
3846
|
+
removeCaptureListeners();
|
|
3847
|
+
}
|
|
3848
|
+
};
|
|
3849
|
+
}
|
|
3850
|
+
//#endregion
|
|
3851
|
+
//#region src/boot/hydrate-spa.ts
|
|
3852
|
+
/**
|
|
3853
|
+
* App bootstrap and mount
|
|
3854
|
+
*/
|
|
3855
|
+
/**
|
|
3856
|
+
* hydrateSPA: Hydrate server-rendered HTML with an explicit route registry.
|
|
3857
|
+
*/
|
|
3858
|
+
async function hydrateSPA(config) {
|
|
3859
|
+
assertExecutionModel("spa");
|
|
3860
|
+
if (!config || typeof config !== "object") throw new Error("hydrateSPA requires a config object");
|
|
3861
|
+
validateCspNonce(config.cspNonce);
|
|
3862
|
+
if (!config.registry) throw new Error("hydrateSPA requires a route registry.");
|
|
3863
|
+
const { manifest } = config.registry;
|
|
3864
|
+
if (manifest.records.length === 0) throw new Error("hydrateSPA requires a route registry with at least one route. If you are enhancing existing HTML, use createIsland instead.");
|
|
3865
|
+
const rootElement = resolveRootElement(config.root);
|
|
3866
|
+
if (!rootElement) throw new Error(`Root element not found: ${config.root}`);
|
|
3867
|
+
const interactionReplay = beginHydrationInteractionReplay(rootElement, (boundary) => activateHydrationBoundary(rootElement, boundary), config.hydrate?.skipSelectors);
|
|
3868
|
+
try {
|
|
3869
|
+
applyDeferredStreamPatches(rootElement);
|
|
3870
|
+
adoptSsrStyleCarriers(rootElement);
|
|
3871
|
+
const hydrationRenderData = takeHydrationRenderData(rootElement);
|
|
3872
|
+
const hydrationQueryCache = hydrationRenderData?.queries;
|
|
3873
|
+
const dataRuntime = config.dataRuntime ?? getDefaultDataRuntime();
|
|
3874
|
+
if (hydrationQueryCache) hydrateDataRuntime(dataRuntime, hydrationQueryCache);
|
|
3875
|
+
const hydrationRenderDataForApp = withoutHydratedAuth(hydrationRenderData);
|
|
3876
|
+
configureScrollRestoration(config.scrollRestoration);
|
|
3877
|
+
clearRouteState();
|
|
3878
|
+
_applyManifest(manifest);
|
|
3879
|
+
const hydratedAuth = readHydratedAuth(hydrationRenderData);
|
|
3880
|
+
const routeAuth = config.auth ?? manifest.auth;
|
|
3881
|
+
const appRouteSource = {
|
|
3882
|
+
registry: config.registry,
|
|
3883
|
+
auth: routeAuth,
|
|
3884
|
+
runtime: createAppRenderRuntime({
|
|
3885
|
+
hydrationResources: hydrationRenderDataForApp?.resources,
|
|
3886
|
+
framework: hydrationRenderDataForApp?.framework,
|
|
3887
|
+
route: hydrationRenderData?.route,
|
|
3888
|
+
hasRoute: hydrationRenderData !== null,
|
|
3889
|
+
dataRuntime,
|
|
3890
|
+
routeRegistry: config.registry,
|
|
3891
|
+
routeAuth
|
|
3892
|
+
})
|
|
3893
|
+
};
|
|
3894
|
+
_setActiveRouteAuthOptions(routeAuth);
|
|
3895
|
+
const initialRoute = await resolveInitialRoute(routeAuth, {
|
|
3896
|
+
registry: config.registry,
|
|
3897
|
+
load: false,
|
|
3898
|
+
authContext: hydratedAuth,
|
|
3899
|
+
dataRuntime
|
|
3900
|
+
});
|
|
3901
|
+
if (!initialRoute) {
|
|
3902
|
+
interactionReplay.abort();
|
|
3903
|
+
return;
|
|
3904
|
+
}
|
|
3905
|
+
const { path, href: currentUrl, resolved } = initialRoute;
|
|
3906
|
+
setServerLocation(currentUrl);
|
|
3907
|
+
if (isProductionEnvironment()) lockRouteRegistration();
|
|
3908
|
+
if (!resolved) throw new Error(`hydrateSPA: no route found for current path (${path}).`);
|
|
3909
|
+
await reconcileInitialRouteMetadata(resolved);
|
|
3910
|
+
const hydrationResolved = resolved.kind === "deny" ? {
|
|
3911
|
+
handler: bindDeniedRouteHandler(resolved.status),
|
|
3912
|
+
params: {}
|
|
3913
|
+
} : resolved;
|
|
3914
|
+
let verifyClientMarkup;
|
|
3915
|
+
let clientMarkup = null;
|
|
3916
|
+
const captureClientMarkup = () => {
|
|
3917
|
+
if (verifyClientMarkup) clientMarkup = rootElement.innerHTML;
|
|
3918
|
+
};
|
|
3919
|
+
const mountHydratedRoot = (...args) => mountOrUpdate(args[0], args[1], {
|
|
3920
|
+
...args[2],
|
|
3921
|
+
cspNonce: config.cspNonce,
|
|
3922
|
+
hydrate: true,
|
|
3923
|
+
onCommit: captureClientMarkup
|
|
3924
|
+
});
|
|
3925
|
+
if (shouldVerifyHydrationMarkup(config)) {
|
|
3926
|
+
const { captureServerHydrationMarkup, verifyClientHydrationMarkup, verifyHydrationSyncForUrl } = await import("./verify-hydration-DpVaDnqz.js");
|
|
3927
|
+
if (!verifyHydrationSyncForUrl({
|
|
3928
|
+
root: rootElement,
|
|
3929
|
+
url: currentUrl,
|
|
3930
|
+
registry: config.registry,
|
|
3931
|
+
resolved: hydrationResolved,
|
|
3932
|
+
options: {
|
|
3933
|
+
data: hydrationRenderDataForApp?.resources,
|
|
3934
|
+
dataRuntime,
|
|
3935
|
+
envelope: hydrationRenderDataForApp ?? void 0,
|
|
3936
|
+
cspNonce: config.cspNonce
|
|
3937
|
+
}
|
|
3938
|
+
})) throw new Error("[Askr] Hydration mismatch detected. Server HTML does not match expected server-render output.");
|
|
3939
|
+
const serverMarkup = captureServerHydrationMarkup(rootElement, currentUrl, hydrationRenderDataForApp ?? void 0);
|
|
3940
|
+
if (serverMarkup !== null) verifyClientMarkup = async () => {
|
|
3941
|
+
await Promise.resolve();
|
|
3942
|
+
if (!(clientMarkup !== null && verifyClientHydrationMarkup(clientMarkup, serverMarkup)) && !verifyClientHydrationMarkup(rootElement.innerHTML, serverMarkup)) throw new Error("[Askr] Hydration mismatch detected between server and client markup.");
|
|
3943
|
+
};
|
|
3944
|
+
}
|
|
3945
|
+
const hydrateOptions = config.hydrate;
|
|
3946
|
+
if (hydrateOptions) {
|
|
3947
|
+
if (hydrateOptions.deferUntilIdle || hydrateOptions.deferBelowFold) {
|
|
3948
|
+
if (hydrationRenderDataForApp) startHydrationRenderPhase(hydrationRenderDataForApp);
|
|
3949
|
+
try {
|
|
3950
|
+
await applySelectiveHydration(rootElement, hydrationResolved, path, config.cleanupStrict, hydrateOptions, appRouteSource, {
|
|
3951
|
+
mountOrUpdate: mountHydratedRoot,
|
|
3952
|
+
registerAppNavigation,
|
|
3953
|
+
registerRootCleanupCallback,
|
|
3954
|
+
activateHydrationBoundary
|
|
3955
|
+
}, interactionReplay);
|
|
3956
|
+
} finally {
|
|
3957
|
+
if (hydrationRenderDataForApp) stopHydrationRenderPhase();
|
|
3958
|
+
}
|
|
3959
|
+
flushSync();
|
|
3960
|
+
if (!rootElement.querySelector("[data-skip-hydrate]")) await verifyClientMarkup?.();
|
|
3961
|
+
interactionReplay.complete();
|
|
3962
|
+
return;
|
|
3963
|
+
}
|
|
3964
|
+
if (hydrateOptions.skipSelectors?.length) markSkippedElements(rootElement, hydrateOptions.skipSelectors);
|
|
3965
|
+
}
|
|
3966
|
+
if (hydrationRenderDataForApp) startHydrationRenderPhase(hydrationRenderDataForApp);
|
|
3967
|
+
try {
|
|
3968
|
+
mountHydratedRoot(rootElement, resolved.kind === "deny" ? bindDeniedStatus(resolved.status) : bindResolvedRouteHandler(hydrationResolved), {
|
|
3969
|
+
cleanupStrict: config.cleanupStrict,
|
|
3970
|
+
appRuntime: appRouteSource.runtime
|
|
3971
|
+
});
|
|
3972
|
+
flushSync();
|
|
3973
|
+
} finally {
|
|
3974
|
+
if (hydrationRenderDataForApp) stopHydrationRenderPhase();
|
|
3975
|
+
}
|
|
3976
|
+
await verifyClientMarkup?.();
|
|
3977
|
+
interactionReplay.complete();
|
|
3978
|
+
await registerAppNavigation(rootElement, path, { ...appRouteSource });
|
|
3979
|
+
} catch (error) {
|
|
3980
|
+
interactionReplay.abort();
|
|
3981
|
+
throw error;
|
|
3982
|
+
}
|
|
3983
|
+
}
|
|
3984
|
+
//#endregion
|
|
3985
|
+
//#region src/boot/index.ts
|
|
3986
|
+
/** Stable bootstrap exports; each mounting mode owns its orchestration. */
|
|
3987
|
+
composeBrowserRuntime();
|
|
3988
|
+
//#endregion
|
|
3989
|
+
export { cleanupApp as a, createIslands as i, createSPA as n, hasApp as o, createIsland as r, hydrateSPA as t };
|