@c9up/aurora 0.1.3
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/LICENSE +21 -0
- package/README.md +36 -0
- package/dist/AuroraManager.d.ts +44 -0
- package/dist/AuroraManager.js +47 -0
- package/dist/AuroraProvider.d.ts +52 -0
- package/dist/AuroraProvider.js +145 -0
- package/dist/Pages.d.ts +78 -0
- package/dist/Pages.js +116 -0
- package/dist/component.d.ts +55 -0
- package/dist/component.js +97 -0
- package/dist/html.d.ts +30 -0
- package/dist/html.js +246 -0
- package/dist/hydrate.d.ts +29 -0
- package/dist/hydrate.js +379 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +12 -0
- package/dist/reactive.d.ts +83 -0
- package/dist/reactive.js +217 -0
- package/dist/relay.d.ts +43 -0
- package/dist/relay.js +144 -0
- package/dist/render.d.ts +25 -0
- package/dist/render.js +283 -0
- package/dist/route.d.ts +64 -0
- package/dist/route.js +49 -0
- package/dist/server/renderPage.d.ts +62 -0
- package/dist/server/renderPage.js +83 -0
- package/dist/server/serveAssets.d.ts +43 -0
- package/dist/server/serveAssets.js +89 -0
- package/dist/services/main.d.ts +18 -0
- package/dist/services/main.js +31 -0
- package/dist/ssr.d.ts +22 -0
- package/dist/ssr.js +179 -0
- package/dist/types.d.ts +78 -0
- package/dist/types.js +15 -0
- package/package.json +69 -0
- package/src/AuroraManager.ts +76 -0
- package/src/AuroraProvider.ts +187 -0
- package/src/Pages.ts +164 -0
- package/src/component.ts +138 -0
- package/src/html.ts +296 -0
- package/src/hydrate.ts +518 -0
- package/src/index.ts +43 -0
- package/src/reactive.ts +265 -0
- package/src/relay.ts +171 -0
- package/src/render.ts +378 -0
- package/src/route.ts +96 -0
- package/src/server/renderPage.ts +135 -0
- package/src/server/serveAssets.ts +135 -0
- package/src/services/main.ts +40 -0
- package/src/ssr.ts +179 -0
- package/src/types.ts +97 -0
package/src/hydrate.ts
ADDED
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hydration — adopt SSR-rendered HTML in the browser without rebuilding
|
|
3
|
+
* the DOM.
|
|
4
|
+
*
|
|
5
|
+
* `hydrate(container, factory)` runs the same component factory used
|
|
6
|
+
* server-side, recomputes the slot bindings, and attaches them to the
|
|
7
|
+
* existing nodes. Where SSR emitted plain text for `${signal}`, hydrate
|
|
8
|
+
* locates the same text node (via path resolution against the cloned
|
|
9
|
+
* template) and starts an effect that updates it on signal change.
|
|
10
|
+
*
|
|
11
|
+
* Implementation note: we still run `getTemplate(strings)` to know
|
|
12
|
+
* where each slot lives, then walk the LIVE container tree using the
|
|
13
|
+
* same path. SSR output must match the shape of the parsed template
|
|
14
|
+
* for hydration to find the right node — same constraint as React's
|
|
15
|
+
* hydration mismatch warning.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { readComponentLifecycle } from "./component.js";
|
|
19
|
+
import { getTemplate } from "./html.js";
|
|
20
|
+
import { effect, isSignal } from "./reactive.js";
|
|
21
|
+
import { type Disposer, mount } from "./render.js";
|
|
22
|
+
import {
|
|
23
|
+
type AttrSlot,
|
|
24
|
+
type BooleanAttrSlot,
|
|
25
|
+
type EffectCallback,
|
|
26
|
+
type EventSlot,
|
|
27
|
+
isTemplateResult,
|
|
28
|
+
type NodePath,
|
|
29
|
+
type PropSlot,
|
|
30
|
+
type Slot,
|
|
31
|
+
type TemplateResult,
|
|
32
|
+
} from "./types.js";
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Process-scoped flag so the "reactive nested template not reactive
|
|
36
|
+
* after hydration" warning fires once, not on every matching slot.
|
|
37
|
+
* Only reached on LEGACY markup that predates SSR boundary markers
|
|
38
|
+
* (the markered path keeps the subtree reactive — no warning).
|
|
39
|
+
*/
|
|
40
|
+
let nestedReactiveWarned = false;
|
|
41
|
+
|
|
42
|
+
/** @internal Reset the warn-once flag (tests). */
|
|
43
|
+
export function resetHydrateWarnings(): void {
|
|
44
|
+
nestedReactiveWarned = false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Boundary-marker comment payloads (kept in sync with ssr.ts).
|
|
48
|
+
const SLOT_START = "$";
|
|
49
|
+
const SLOT_END = "/$";
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* An SSR-emitted `<!--$-->…<!--/$-->` pair delimiting a reactive
|
|
53
|
+
* structured slot's rendered subtree.
|
|
54
|
+
*/
|
|
55
|
+
interface MarkerPair {
|
|
56
|
+
start: Comment;
|
|
57
|
+
end: Comment;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Document-ordered list of marker pairs + a consume cursor. Reactive
|
|
62
|
+
* structured text slots consume pairs in hydration order, which matches
|
|
63
|
+
* the document order of their `<!--$-->` start markers (a parent slot's
|
|
64
|
+
* start precedes its children's, and hydration visits parents first).
|
|
65
|
+
*/
|
|
66
|
+
interface MarkerCursor {
|
|
67
|
+
pairs: MarkerPair[];
|
|
68
|
+
i: number;
|
|
69
|
+
/**
|
|
70
|
+
* The Document this hydration root belongs to. Threaded through (not
|
|
71
|
+
* a module global) so concurrent `hydrate()` calls on different
|
|
72
|
+
* documents / iframes each create swapped-in text nodes in THEIR own
|
|
73
|
+
* document — a shared global would let a second root's document
|
|
74
|
+
* clobber the first's.
|
|
75
|
+
*/
|
|
76
|
+
doc: Document;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Collect every `<!--$-->…<!--/$-->` pair under `container`, ordered by
|
|
81
|
+
* the start marker's document position. Nesting is resolved with a
|
|
82
|
+
* stack so an inner pair's start/end never cross an outer pair's.
|
|
83
|
+
*/
|
|
84
|
+
function collectMarkerPairs(container: Node): MarkerPair[] {
|
|
85
|
+
// Depth-first, document-order walk. We DON'T use createTreeWalker:
|
|
86
|
+
// some DOM implementations (happy-dom under vitest) ignore the
|
|
87
|
+
// numeric `whatToShow` filter and yield nothing. A manual recursion
|
|
88
|
+
// over childNodes is portable and visits comments in document order,
|
|
89
|
+
// so the stack pairs each `<!--$-->` with its matching `<!--/$-->`
|
|
90
|
+
// and the result is already start-ordered (no sort needed).
|
|
91
|
+
const pairs: MarkerPair[] = [];
|
|
92
|
+
const stack: Comment[] = [];
|
|
93
|
+
const visit = (node: Node): void => {
|
|
94
|
+
if (node.nodeType === 8 /* Comment */) {
|
|
95
|
+
const c = node as Comment;
|
|
96
|
+
if (c.data === SLOT_START) {
|
|
97
|
+
stack.push(c);
|
|
98
|
+
} else if (c.data === SLOT_END) {
|
|
99
|
+
const start = stack.pop();
|
|
100
|
+
if (start !== undefined) pairs.push({ start, end: c });
|
|
101
|
+
}
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
for (
|
|
105
|
+
let child = node.firstChild;
|
|
106
|
+
child !== null;
|
|
107
|
+
child = child.nextSibling
|
|
108
|
+
) {
|
|
109
|
+
visit(child);
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
visit(container);
|
|
113
|
+
// `pairs` is in END order (innermost closes first). Sort by start's
|
|
114
|
+
// document position so consumption matches hydration's
|
|
115
|
+
// parents-before-children visit order.
|
|
116
|
+
pairs.sort((a, b) =>
|
|
117
|
+
a.start.compareDocumentPosition(b.start) &
|
|
118
|
+
4 /* DOCUMENT_POSITION_FOLLOWING */
|
|
119
|
+
? -1
|
|
120
|
+
: 1,
|
|
121
|
+
);
|
|
122
|
+
return pairs;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Render a value (template / array / scalar) to detached client nodes. */
|
|
126
|
+
function renderValueToNodes(
|
|
127
|
+
value: unknown,
|
|
128
|
+
cleanups: Disposer[],
|
|
129
|
+
mountHooks: Array<EffectCallback>,
|
|
130
|
+
doc: Document,
|
|
131
|
+
): ChildNode[] {
|
|
132
|
+
if (value === null || value === undefined || value === false) return [];
|
|
133
|
+
if (Array.isArray(value)) {
|
|
134
|
+
const out: ChildNode[] = [];
|
|
135
|
+
for (const item of value) {
|
|
136
|
+
out.push(...renderValueToNodes(item, cleanups, mountHooks, doc));
|
|
137
|
+
}
|
|
138
|
+
return out;
|
|
139
|
+
}
|
|
140
|
+
if (isTemplateResult(value)) {
|
|
141
|
+
const frag = mount(value, cleanups, [], mountHooks);
|
|
142
|
+
return Array.from(frag.childNodes);
|
|
143
|
+
}
|
|
144
|
+
if (value instanceof Node) return [value as ChildNode];
|
|
145
|
+
return [doc.createTextNode(String(value))];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Wire a reactive structured slot (signal/function → nested template or
|
|
150
|
+
* array) using its SSR boundary-marker pair. The first run hydrates the
|
|
151
|
+
* initial value against the captured SSR nodes (reusing server markup,
|
|
152
|
+
* no flash); every subsequent signal change disposes the old subtree
|
|
153
|
+
* and client-renders the new value into the same `<!--$-->…<!--/$-->`
|
|
154
|
+
* range — so the DOM stays correct on branch changes instead of going
|
|
155
|
+
* stale.
|
|
156
|
+
*/
|
|
157
|
+
function hydrateReactiveStructured(
|
|
158
|
+
fn: () => unknown,
|
|
159
|
+
pair: MarkerPair,
|
|
160
|
+
cleanups: Disposer[],
|
|
161
|
+
mountHooks: Array<EffectCallback>,
|
|
162
|
+
markerCursor: MarkerCursor,
|
|
163
|
+
): void {
|
|
164
|
+
const { start, end } = pair;
|
|
165
|
+
let currentNodes: ChildNode[] = [];
|
|
166
|
+
for (let n = start.nextSibling; n !== null && n !== end; n = n.nextSibling) {
|
|
167
|
+
currentNodes.push(n as ChildNode);
|
|
168
|
+
}
|
|
169
|
+
let localCleanups: Disposer[] = [];
|
|
170
|
+
let firstRun = true;
|
|
171
|
+
|
|
172
|
+
const dispose = effect(() => {
|
|
173
|
+
const next = fn();
|
|
174
|
+
if (firstRun) {
|
|
175
|
+
firstRun = false;
|
|
176
|
+
// Reuse SSR markup: hydrate reactive bindings INSIDE the nested
|
|
177
|
+
// template against the captured nodes. Inner boundary markers
|
|
178
|
+
// are consumed from the same cursor (document order).
|
|
179
|
+
if (isTemplateResult(next)) {
|
|
180
|
+
hydrateTemplateResult(
|
|
181
|
+
next,
|
|
182
|
+
currentNodes,
|
|
183
|
+
localCleanups,
|
|
184
|
+
mountHooks,
|
|
185
|
+
markerCursor,
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
// Signal changed post-hydration: tear down the old subtree's
|
|
191
|
+
// effects/listeners, drop its nodes, client-render the new value
|
|
192
|
+
// into the same marker range.
|
|
193
|
+
for (const d of localCleanups) d();
|
|
194
|
+
localCleanups = [];
|
|
195
|
+
for (const n of currentNodes) n.remove();
|
|
196
|
+
currentNodes = [];
|
|
197
|
+
const parent = end.parentNode;
|
|
198
|
+
if (parent === null) return;
|
|
199
|
+
const fresh = renderValueToNodes(
|
|
200
|
+
next,
|
|
201
|
+
localCleanups,
|
|
202
|
+
mountHooks,
|
|
203
|
+
markerCursor.doc,
|
|
204
|
+
);
|
|
205
|
+
for (const n of fresh) parent.insertBefore(n, end);
|
|
206
|
+
currentNodes = fresh;
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
cleanups.push(() => {
|
|
210
|
+
dispose();
|
|
211
|
+
for (const d of localCleanups) d();
|
|
212
|
+
localCleanups = [];
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Adopt SSR markup inside `container`. `factory` is the same function
|
|
218
|
+
* that was rendered server-side — its output (a TemplateResult tree)
|
|
219
|
+
* tells hydrate which slots to wire.
|
|
220
|
+
*
|
|
221
|
+
* Returns a `Disposer` that detaches every effect and event listener,
|
|
222
|
+
* leaving the DOM in place.
|
|
223
|
+
*/
|
|
224
|
+
export function hydrate(
|
|
225
|
+
container: Element,
|
|
226
|
+
factory: () => TemplateResult,
|
|
227
|
+
): Disposer {
|
|
228
|
+
const cleanups: Disposer[] = [];
|
|
229
|
+
const mountHooks: Array<EffectCallback> = [];
|
|
230
|
+
const markerCursor: MarkerCursor = {
|
|
231
|
+
pairs: collectMarkerPairs(container),
|
|
232
|
+
i: 0,
|
|
233
|
+
doc: container.ownerDocument ?? document,
|
|
234
|
+
};
|
|
235
|
+
const result = factory();
|
|
236
|
+
hydrateTemplateResult(
|
|
237
|
+
result,
|
|
238
|
+
Array.from(container.childNodes),
|
|
239
|
+
cleanups,
|
|
240
|
+
mountHooks,
|
|
241
|
+
markerCursor,
|
|
242
|
+
);
|
|
243
|
+
for (const hook of mountHooks) {
|
|
244
|
+
try {
|
|
245
|
+
const teardown = hook();
|
|
246
|
+
if (typeof teardown === "function") cleanups.push(teardown);
|
|
247
|
+
} catch {
|
|
248
|
+
/* swallow */
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
let disposed = false;
|
|
252
|
+
return () => {
|
|
253
|
+
if (disposed) return;
|
|
254
|
+
disposed = true;
|
|
255
|
+
for (const c of cleanups.splice(0)) c();
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Hydrate a TemplateResult against a list of live root nodes. The list
|
|
261
|
+
* is sliced as we consume children — text-slot anchors don't exist in
|
|
262
|
+
* the SSR output (we inlined the value), so we count text-slot
|
|
263
|
+
* boundaries by reading the static `strings` between values.
|
|
264
|
+
*/
|
|
265
|
+
function hydrateTemplateResult(
|
|
266
|
+
result: TemplateResult,
|
|
267
|
+
liveNodes: ChildNode[],
|
|
268
|
+
cleanups: Disposer[],
|
|
269
|
+
mountHooks: Array<EffectCallback>,
|
|
270
|
+
markerCursor: MarkerCursor,
|
|
271
|
+
): void {
|
|
272
|
+
const lifecycle = readComponentLifecycle(result);
|
|
273
|
+
if (lifecycle) {
|
|
274
|
+
for (const hook of lifecycle.mountHooks) mountHooks.push(hook);
|
|
275
|
+
for (const c of lifecycle.cleanups) cleanups.push(c);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Hydration walks via the SAME path resolver as render, but against
|
|
279
|
+
// a synthetic root that mimics the parsed template's child list.
|
|
280
|
+
const tpl = getTemplate(result.strings);
|
|
281
|
+
// The live container's children should structurally match the
|
|
282
|
+
// template's content children. Wrap them in a transient DocumentFragment
|
|
283
|
+
// for path resolution — DocumentFragment.childNodes is the same view
|
|
284
|
+
// we walked during parse.
|
|
285
|
+
const syntheticRoot = {
|
|
286
|
+
childNodes: liveNodes,
|
|
287
|
+
} as unknown as ParentNode;
|
|
288
|
+
|
|
289
|
+
for (let i = 0; i < tpl.slots.length; i++) {
|
|
290
|
+
const slot = tpl.slots[i];
|
|
291
|
+
const liveNode = resolvePathLive(syntheticRoot, slot.path, liveNodes);
|
|
292
|
+
if (!liveNode) {
|
|
293
|
+
// Path missed in the live DOM — SSR markup diverges from the
|
|
294
|
+
// parsed template's shape. Surfacing the mismatch beats silent
|
|
295
|
+
// dead bindings: a stale slot doesn't update, but the developer
|
|
296
|
+
// has no clue why until they hit print-line debugging.
|
|
297
|
+
if (typeof console !== "undefined") {
|
|
298
|
+
console.warn(
|
|
299
|
+
`[aurora] hydration mismatch: slot ${i} (${slot.kind}) path ${slot.path.join(".")} not found in live DOM — SSR markup may diverge from the client template (did you forget to rerender after a server change?)`,
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
hydrateSlot(
|
|
305
|
+
slot,
|
|
306
|
+
liveNode,
|
|
307
|
+
result.values[i],
|
|
308
|
+
cleanups,
|
|
309
|
+
mountHooks,
|
|
310
|
+
markerCursor,
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Resolve a slot's path against the LIVE DOM. The first index of the
|
|
317
|
+
* path indexes into `liveNodes` directly (since we packaged them in a
|
|
318
|
+
* synthetic root); subsequent indices walk the child node list normally.
|
|
319
|
+
*
|
|
320
|
+
* Text-slot paths point to a comment marker that doesn't exist in
|
|
321
|
+
* hydration markup — we tolerate the miss and return null.
|
|
322
|
+
*/
|
|
323
|
+
function resolvePathLive(
|
|
324
|
+
_root: ParentNode,
|
|
325
|
+
path: NodePath,
|
|
326
|
+
rootNodes: ChildNode[],
|
|
327
|
+
): Node | null {
|
|
328
|
+
if (path.length === 0) return null;
|
|
329
|
+
let node: Node | null = rootNodes[path[0]] ?? null;
|
|
330
|
+
for (let i = 1; node && i < path.length; i++) {
|
|
331
|
+
node = node.childNodes[path[i]] ?? null;
|
|
332
|
+
}
|
|
333
|
+
return node;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function hydrateSlot(
|
|
337
|
+
slot: Slot,
|
|
338
|
+
node: Node,
|
|
339
|
+
value: unknown,
|
|
340
|
+
cleanups: Disposer[],
|
|
341
|
+
mountHooks: Array<EffectCallback>,
|
|
342
|
+
markerCursor: MarkerCursor,
|
|
343
|
+
): void {
|
|
344
|
+
switch (slot.kind) {
|
|
345
|
+
case "text":
|
|
346
|
+
hydrateTextSlot(node, value, cleanups, mountHooks, markerCursor);
|
|
347
|
+
return;
|
|
348
|
+
case "attr":
|
|
349
|
+
hydrateAttrSlot(slot, node as Element, value, cleanups);
|
|
350
|
+
return;
|
|
351
|
+
case "boolean-attr":
|
|
352
|
+
hydrateBooleanAttrSlot(slot, node as Element, value, cleanups);
|
|
353
|
+
return;
|
|
354
|
+
case "prop":
|
|
355
|
+
hydratePropSlot(slot, node as Element, value, cleanups);
|
|
356
|
+
return;
|
|
357
|
+
case "event":
|
|
358
|
+
hydrateEventSlot(slot, node as Element, value, cleanups);
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Hydrate a text slot. SSR inlined the value as a text node (or skipped
|
|
365
|
+
* it for null/false/undefined). We locate the **first text node sibling
|
|
366
|
+
* preceding the path's terminal index** — that's where SSR wrote the
|
|
367
|
+
* value — and wire an effect that overwrites its `data` on changes.
|
|
368
|
+
*
|
|
369
|
+
* For reactive values (signals/functions), the effect updates the
|
|
370
|
+
* existing text node in place. For nested TemplateResults, we
|
|
371
|
+
* recursively hydrate against the captured sibling range.
|
|
372
|
+
*/
|
|
373
|
+
function hydrateTextSlot(
|
|
374
|
+
commentMarker: Node,
|
|
375
|
+
value: unknown,
|
|
376
|
+
cleanups: Disposer[],
|
|
377
|
+
mountHooks: Array<EffectCallback>,
|
|
378
|
+
markerCursor: MarkerCursor,
|
|
379
|
+
): void {
|
|
380
|
+
// The path lands on the comment marker that EXISTS in the parsed
|
|
381
|
+
// template but not in SSR output. Hydration walks the live siblings
|
|
382
|
+
// to find the text node that holds the SSR value.
|
|
383
|
+
// Strategy: the comment was located at child index N inside its
|
|
384
|
+
// parent; SSR wrote the value as the immediately-preceding text
|
|
385
|
+
// node (or nothing for null/false). Live node here is whatever the
|
|
386
|
+
// path resolution returned — often a text node, sometimes an
|
|
387
|
+
// element (for nested templates). We rebind in-place.
|
|
388
|
+
if (isSignal(value) || typeof value === "function") {
|
|
389
|
+
const fn = value as () => unknown;
|
|
390
|
+
// First, evaluate eagerly to detect a structured value (nested
|
|
391
|
+
// TemplateResult / array) — those need a SWAP on change, which
|
|
392
|
+
// means a node range, which the SSR boundary markers give us.
|
|
393
|
+
const first = fn();
|
|
394
|
+
if (isTemplateResult(first) || Array.isArray(first)) {
|
|
395
|
+
const pair = markerCursor.pairs[markerCursor.i];
|
|
396
|
+
if (pair !== undefined) {
|
|
397
|
+
markerCursor.i += 1;
|
|
398
|
+
hydrateReactiveStructured(fn, pair, cleanups, mountHooks, markerCursor);
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
// LEGACY markup (no boundary markers — produced by an older
|
|
402
|
+
// SSR build): we can't locate the subtree's range, so we
|
|
403
|
+
// hydrate once and warn that the subtree won't stay reactive.
|
|
404
|
+
// Fresh SSR always emits markers, so this path is dead for
|
|
405
|
+
// matched server/client builds.
|
|
406
|
+
if (!nestedReactiveWarned && typeof console !== "undefined") {
|
|
407
|
+
nestedReactiveWarned = true;
|
|
408
|
+
console.warn(
|
|
409
|
+
"[aurora] a reactive expression hydrated to a nested template but " +
|
|
410
|
+
"the SSR markup has no boundary markers — the subtree will not update " +
|
|
411
|
+
"on signal changes. Re-render with a current @c9up/aurora SSR build.",
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
if (isTemplateResult(first)) {
|
|
415
|
+
hydrateTemplateResult(
|
|
416
|
+
first,
|
|
417
|
+
[commentMarker as ChildNode],
|
|
418
|
+
cleanups,
|
|
419
|
+
mountHooks,
|
|
420
|
+
markerCursor,
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
const textNode =
|
|
426
|
+
commentMarker.nodeType === 3 /* TEXT */
|
|
427
|
+
? (commentMarker as Text)
|
|
428
|
+
: commentMarker.previousSibling?.nodeType === 3
|
|
429
|
+
? (commentMarker.previousSibling as Text)
|
|
430
|
+
: null;
|
|
431
|
+
if (!textNode) return;
|
|
432
|
+
const dispose = effect(() => {
|
|
433
|
+
const v = fn();
|
|
434
|
+
textNode.data = v == null || v === false ? "" : String(v);
|
|
435
|
+
});
|
|
436
|
+
cleanups.push(dispose);
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
if (isTemplateResult(value)) {
|
|
440
|
+
hydrateTemplateResult(
|
|
441
|
+
value,
|
|
442
|
+
[commentMarker as ChildNode],
|
|
443
|
+
cleanups,
|
|
444
|
+
mountHooks,
|
|
445
|
+
markerCursor,
|
|
446
|
+
);
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
// Static value — SSR rendered it once and we don't need to do
|
|
450
|
+
// anything. The text already lives in the DOM.
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function hydrateAttrSlot(
|
|
454
|
+
slot: AttrSlot,
|
|
455
|
+
el: Element,
|
|
456
|
+
value: unknown,
|
|
457
|
+
cleanups: Disposer[],
|
|
458
|
+
): void {
|
|
459
|
+
function apply(v: unknown): void {
|
|
460
|
+
if (v === null || v === undefined || v === false) {
|
|
461
|
+
el.removeAttribute(slot.name);
|
|
462
|
+
} else if (v === true) {
|
|
463
|
+
el.setAttribute(slot.name, "");
|
|
464
|
+
} else {
|
|
465
|
+
el.setAttribute(slot.name, String(v));
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
if (isSignal(value) || typeof value === "function") {
|
|
469
|
+
const dispose = effect(() => apply((value as () => unknown)()));
|
|
470
|
+
cleanups.push(dispose);
|
|
471
|
+
}
|
|
472
|
+
// Static attrs need no hydration — SSR already wrote them.
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function hydrateBooleanAttrSlot(
|
|
476
|
+
slot: BooleanAttrSlot,
|
|
477
|
+
el: Element,
|
|
478
|
+
value: unknown,
|
|
479
|
+
cleanups: Disposer[],
|
|
480
|
+
): void {
|
|
481
|
+
function apply(v: unknown): void {
|
|
482
|
+
if (v) el.setAttribute(slot.name, "");
|
|
483
|
+
else el.removeAttribute(slot.name);
|
|
484
|
+
}
|
|
485
|
+
if (isSignal(value) || typeof value === "function") {
|
|
486
|
+
const dispose = effect(() => apply((value as () => unknown)()));
|
|
487
|
+
cleanups.push(dispose);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function hydratePropSlot(
|
|
492
|
+
slot: PropSlot,
|
|
493
|
+
el: Element,
|
|
494
|
+
value: unknown,
|
|
495
|
+
cleanups: Disposer[],
|
|
496
|
+
): void {
|
|
497
|
+
function apply(v: unknown): void {
|
|
498
|
+
(el as unknown as Record<string, unknown>)[slot.name] = v;
|
|
499
|
+
}
|
|
500
|
+
if (isSignal(value) || typeof value === "function") {
|
|
501
|
+
const dispose = effect(() => apply((value as () => unknown)()));
|
|
502
|
+
cleanups.push(dispose);
|
|
503
|
+
} else {
|
|
504
|
+
apply(value);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function hydrateEventSlot(
|
|
509
|
+
slot: EventSlot,
|
|
510
|
+
el: Element,
|
|
511
|
+
value: unknown,
|
|
512
|
+
cleanups: Disposer[],
|
|
513
|
+
): void {
|
|
514
|
+
if (typeof value !== "function") return;
|
|
515
|
+
const handler = value as EventListener;
|
|
516
|
+
el.addEventListener(slot.event, handler);
|
|
517
|
+
cleanups.push(() => el.removeEventListener(slot.event, handler));
|
|
518
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// ─── Inertia-shape server surface ─────────────────────────────────
|
|
2
|
+
export { AuroraManager, type AuroraManagerConfig } from "./AuroraManager.js";
|
|
3
|
+
export { component, onMount, onUnmount } from "./component.js";
|
|
4
|
+
export { html, isTemplateResult } from "./html.js";
|
|
5
|
+
export { hydrate } from "./hydrate.js";
|
|
6
|
+
export {
|
|
7
|
+
type PageFactory,
|
|
8
|
+
Pages,
|
|
9
|
+
type PagesConfig,
|
|
10
|
+
} from "./Pages.js";
|
|
11
|
+
export {
|
|
12
|
+
batch,
|
|
13
|
+
effect,
|
|
14
|
+
isSignal,
|
|
15
|
+
memo,
|
|
16
|
+
onCleanup,
|
|
17
|
+
type ReadSignal,
|
|
18
|
+
type Signal,
|
|
19
|
+
signal,
|
|
20
|
+
untrack,
|
|
21
|
+
} from "./reactive.js";
|
|
22
|
+
export { type Disposer, render } from "./render.js";
|
|
23
|
+
export {
|
|
24
|
+
type AuroraHttpContext,
|
|
25
|
+
type AuroraResponse,
|
|
26
|
+
type AuroraRouteConfig,
|
|
27
|
+
auroraRoute,
|
|
28
|
+
} from "./route.js";
|
|
29
|
+
export {
|
|
30
|
+
type RenderHttpContext,
|
|
31
|
+
type RenderPageOptions,
|
|
32
|
+
type RenderResponse,
|
|
33
|
+
renderPage,
|
|
34
|
+
} from "./server/renderPage.js";
|
|
35
|
+
export {
|
|
36
|
+
type AssetsHttpContext,
|
|
37
|
+
type AssetsRequest,
|
|
38
|
+
type AssetsResponse,
|
|
39
|
+
type ServeAssetsOptions,
|
|
40
|
+
serveAssets,
|
|
41
|
+
} from "./server/serveAssets.js";
|
|
42
|
+
export { renderToString } from "./ssr.js";
|
|
43
|
+
export type { TemplateResult } from "./types.js";
|