@c9up/aurora 0.1.14 → 0.1.16
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/README.md +1 -1
- package/dist/AuroraManager.d.ts +19 -1
- package/dist/AuroraManager.js +31 -3
- package/dist/AuroraProvider.js +4 -2
- package/dist/hydrate.js +76 -47
- package/package.json +6 -2
- package/src/AuroraManager.ts +41 -3
- package/src/AuroraProvider.ts +10 -2
- package/src/hydrate.ts +104 -58
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@ providers: [
|
|
|
24
24
|
|
|
25
25
|
## Entry points
|
|
26
26
|
|
|
27
|
-
- `@c9up/aurora` — main API: reactive primitives (`signal`/`effect`/`html`/`component`/`hydrate`) plus the client toolkit — `WebStorage`/`persistedSignal`, reactive browser signals (`prefersDark`/`online`/`windowSize`/…), SPA navigation (`navigate`/`queryParam`), `cookie`/`clipboard`/`share`, the `HttpClient` fetch wrapper, `command()` (async action + reactive loading/data/error),
|
|
27
|
+
- `@c9up/aurora` — main API: reactive primitives (`signal`/`effect`/`html`/`component`/`hydrate`) plus the client toolkit — `WebStorage`/`persistedSignal`, reactive browser signals (`prefersDark`/`online`/`windowSize`/…), SPA navigation (`navigate`/`queryParam`), `cookie`/`clipboard`/`share`, the `HttpClient` fetch wrapper, `createRpcClient()` (JSON-RPC 2.0), `command()` (async action + reactive loading/data/error), `form()` (reactive form controller; optional rune validation + rosetta i18n), `urlFor()` (isomorphic named-route URLs, paired with Ream's `router.namedManifest()`), and `cn()` (zero-dependency Tailwind v4 class merge — `clsx` + `tailwind-merge` reimplemented)
|
|
28
28
|
- `@c9up/aurora/provider` — Ream IoC provider
|
|
29
29
|
- `@c9up/aurora/services/main` — container service accessor
|
|
30
30
|
- `@c9up/aurora/relay` — realtime adapter
|
package/dist/AuroraManager.d.ts
CHANGED
|
@@ -22,13 +22,31 @@ export interface AuroraManagerConfig {
|
|
|
22
22
|
* Override only if you want to serve a custom build.
|
|
23
23
|
*/
|
|
24
24
|
auroraDistRoot?: string;
|
|
25
|
+
/**
|
|
26
|
+
* URL prefix the asset routes mount under. The aurora runtime is served
|
|
27
|
+
* from `<assetsPrefix>/aurora/*` and the app's pages from
|
|
28
|
+
* `<assetsPrefix>/pages/*`, and the SSR importmap + page URLs derive from
|
|
29
|
+
* it. Default `/_assets` (the leading underscore namespaces framework
|
|
30
|
+
* assets away from app routes, Next.js `/_next` style). Set e.g. `/assets`
|
|
31
|
+
* for an underscore-free scheme. An explicit `pages.urlPrefix` still wins.
|
|
32
|
+
*/
|
|
33
|
+
assetsPrefix?: string;
|
|
25
34
|
}
|
|
26
35
|
export declare class AuroraManager {
|
|
27
36
|
readonly pages: Pages;
|
|
28
37
|
readonly auroraDistRoot: string;
|
|
38
|
+
/** Resolved asset prefix (default `/_assets`). */
|
|
39
|
+
readonly assetsPrefix: string;
|
|
40
|
+
/** Mount path for the aurora runtime — `<assetsPrefix>/aurora`. */
|
|
41
|
+
readonly auroraAssetPath: string;
|
|
42
|
+
/** Mount path for the app's pages — `<assetsPrefix>/pages`. */
|
|
43
|
+
readonly pageAssetPath: string;
|
|
29
44
|
constructor(config: AuroraManagerConfig);
|
|
30
45
|
/**
|
|
31
|
-
* SSR + hydrate + ship the document.
|
|
46
|
+
* SSR + hydrate + ship the document. The importmap default points
|
|
47
|
+
* `@c9up/aurora` at this manager's `assetsPrefix`; a caller's
|
|
48
|
+
* `options.importmap` still overrides (e.g. to remap to an app-curated
|
|
49
|
+
* browser entry).
|
|
32
50
|
*/
|
|
33
51
|
render(ctx: RenderHttpContext, name: string, props: unknown, options?: RenderPageOptions): Promise<void>;
|
|
34
52
|
/**
|
package/dist/AuroraManager.js
CHANGED
|
@@ -17,18 +17,46 @@ import { Pages } from "./Pages.js";
|
|
|
17
17
|
import { renderPage, } from "./server/renderPage.js";
|
|
18
18
|
import { serveAssets } from "./server/serveAssets.js";
|
|
19
19
|
const DEFAULT_AURORA_DIST = resolvePath(dirname(fileURLToPath(import.meta.url)), "../dist");
|
|
20
|
+
/** Normalize an asset prefix: ensure a leading slash, drop trailing slashes. */
|
|
21
|
+
function normalizePrefix(prefix) {
|
|
22
|
+
const withLead = prefix.startsWith("/") ? prefix : `/${prefix}`;
|
|
23
|
+
return withLead.replace(/\/+$/, "") || "/";
|
|
24
|
+
}
|
|
20
25
|
export class AuroraManager {
|
|
21
26
|
pages;
|
|
22
27
|
auroraDistRoot;
|
|
28
|
+
/** Resolved asset prefix (default `/_assets`). */
|
|
29
|
+
assetsPrefix;
|
|
30
|
+
/** Mount path for the aurora runtime — `<assetsPrefix>/aurora`. */
|
|
31
|
+
auroraAssetPath;
|
|
32
|
+
/** Mount path for the app's pages — `<assetsPrefix>/pages`. */
|
|
33
|
+
pageAssetPath;
|
|
23
34
|
constructor(config) {
|
|
24
|
-
this.
|
|
35
|
+
this.assetsPrefix = normalizePrefix(config.assetsPrefix ?? "/_assets");
|
|
36
|
+
this.auroraAssetPath = `${this.assetsPrefix}/aurora`;
|
|
37
|
+
this.pageAssetPath = `${this.assetsPrefix}/pages`;
|
|
38
|
+
// Pages serve their compiled JS from the same prefix unless the app
|
|
39
|
+
// pins an explicit urlPrefix.
|
|
40
|
+
this.pages = new Pages({
|
|
41
|
+
...config.pages,
|
|
42
|
+
urlPrefix: config.pages.urlPrefix ?? this.pageAssetPath,
|
|
43
|
+
});
|
|
25
44
|
this.auroraDistRoot = config.auroraDistRoot ?? DEFAULT_AURORA_DIST;
|
|
26
45
|
}
|
|
27
46
|
/**
|
|
28
|
-
* SSR + hydrate + ship the document.
|
|
47
|
+
* SSR + hydrate + ship the document. The importmap default points
|
|
48
|
+
* `@c9up/aurora` at this manager's `assetsPrefix`; a caller's
|
|
49
|
+
* `options.importmap` still overrides (e.g. to remap to an app-curated
|
|
50
|
+
* browser entry).
|
|
29
51
|
*/
|
|
30
52
|
render(ctx, name, props, options) {
|
|
31
|
-
return renderPage(ctx, this.pages, name, props,
|
|
53
|
+
return renderPage(ctx, this.pages, name, props, {
|
|
54
|
+
...options,
|
|
55
|
+
importmap: {
|
|
56
|
+
"@c9up/aurora": `${this.auroraAssetPath}/index.js`,
|
|
57
|
+
...options?.importmap,
|
|
58
|
+
},
|
|
59
|
+
});
|
|
32
60
|
}
|
|
33
61
|
/**
|
|
34
62
|
* Handler for aurora's pre-built ESM runtime. Mount on
|
package/dist/AuroraProvider.js
CHANGED
|
@@ -63,8 +63,10 @@ export default class AuroraProvider {
|
|
|
63
63
|
return;
|
|
64
64
|
const router = this.app.container.resolve("router");
|
|
65
65
|
const manager = this.app.container.resolve(AuroraManager);
|
|
66
|
-
|
|
67
|
-
|
|
66
|
+
// Mount paths derive from the configured `assetsPrefix` (default
|
|
67
|
+
// `/_assets`) — set `config.aurora.assetsPrefix` to change the scheme.
|
|
68
|
+
router.get(`${manager.auroraAssetPath}/*`, adaptHandler(manager.auroraAssetsHandler()));
|
|
69
|
+
router.get(`${manager.pageAssetPath}/*`, adaptHandler(manager.pageAssetsHandler()));
|
|
68
70
|
}
|
|
69
71
|
async ready() { }
|
|
70
72
|
async shutdown() { }
|
package/dist/hydrate.js
CHANGED
|
@@ -115,11 +115,17 @@ function hydrateReactiveStructured(fn, pair, cleanups, mountHooks, markerCursor)
|
|
|
115
115
|
if (firstRun) {
|
|
116
116
|
firstRun = false;
|
|
117
117
|
// Reuse SSR markup: hydrate reactive bindings INSIDE the nested
|
|
118
|
-
//
|
|
119
|
-
//
|
|
118
|
+
// value against the captured nodes. Inner boundary markers are
|
|
119
|
+
// consumed from the same cursor (document order) — for an ARRAY this
|
|
120
|
+
// MUST recurse into every item, else the items' marker pairs go
|
|
121
|
+
// unconsumed and the cursor desyncs, wiring slots AFTER the list to
|
|
122
|
+
// the wrong range (SSR list "present but not painted").
|
|
120
123
|
if (isTemplateResult(next)) {
|
|
121
124
|
hydrateTemplateResult(next, currentNodes, localCleanups, mountHooks, markerCursor);
|
|
122
125
|
}
|
|
126
|
+
else if (Array.isArray(next)) {
|
|
127
|
+
hydrateArrayItems(next, currentNodes, localCleanups, mountHooks, markerCursor);
|
|
128
|
+
}
|
|
123
129
|
return;
|
|
124
130
|
}
|
|
125
131
|
// Signal changed post-hydration: tear down the old subtree's
|
|
@@ -146,6 +152,50 @@ function hydrateReactiveStructured(fn, pair, cleanups, mountHooks, markerCursor)
|
|
|
146
152
|
localCleanups = [];
|
|
147
153
|
});
|
|
148
154
|
}
|
|
155
|
+
/**
|
|
156
|
+
* Top-level live (SSR) node count a value contributes inside an array slot: a
|
|
157
|
+
* TemplateResult contributes its template's root-node count, a nested array the
|
|
158
|
+
* sum of its items, a non-empty scalar one text node, null/undefined/false none.
|
|
159
|
+
* Used to slice the array's range per item during hydration.
|
|
160
|
+
*/
|
|
161
|
+
function liveNodeCount(value) {
|
|
162
|
+
if (value === null || value === undefined || value === false)
|
|
163
|
+
return 0;
|
|
164
|
+
if (isTemplateResult(value)) {
|
|
165
|
+
return getTemplate(value.strings).element.content.childNodes.length;
|
|
166
|
+
}
|
|
167
|
+
if (Array.isArray(value)) {
|
|
168
|
+
let n = 0;
|
|
169
|
+
for (const v of value)
|
|
170
|
+
n += liveNodeCount(v);
|
|
171
|
+
return n;
|
|
172
|
+
}
|
|
173
|
+
return 1; // scalar → one inlined text node
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Hydrate the items of a reactive array against the SSR nodes inside its marker
|
|
177
|
+
* range. Each item is hydrated against its own slice of the (marker-collapsed)
|
|
178
|
+
* range, IN ORDER, so every item's inner marker pairs are consumed in document
|
|
179
|
+
* order and the global cursor stays aligned for slots AFTER the list. Item
|
|
180
|
+
* templates need a stable top-level node count (the common
|
|
181
|
+
* `arr.map(x => html`<li>…</li>`)` shape — single root, no surrounding
|
|
182
|
+
* whitespace); bare adjacent scalar items can merge in the browser, so use
|
|
183
|
+
* template items for hydrated lists.
|
|
184
|
+
*/
|
|
185
|
+
function hydrateArrayItems(items, rangeNodes, cleanups, mountHooks, markerCursor) {
|
|
186
|
+
const nodes = collapseMarkerRanges(rangeNodes);
|
|
187
|
+
let offset = 0;
|
|
188
|
+
for (const item of items) {
|
|
189
|
+
const count = liveNodeCount(item);
|
|
190
|
+
if (isTemplateResult(item)) {
|
|
191
|
+
hydrateTemplateResult(item, nodes.slice(offset, offset + count), cleanups, mountHooks, markerCursor);
|
|
192
|
+
}
|
|
193
|
+
else if (Array.isArray(item)) {
|
|
194
|
+
hydrateArrayItems(item, nodes.slice(offset, offset + count), cleanups, mountHooks, markerCursor);
|
|
195
|
+
}
|
|
196
|
+
offset += count;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
149
199
|
/**
|
|
150
200
|
* Adopt SSR markup inside `container`. `factory` is the same function
|
|
151
201
|
* that was rendered server-side — its output (a TemplateResult tree)
|
|
@@ -308,29 +358,11 @@ function hydrateSlot(slot, node, value, cleanups, mountHooks, markerCursor) {
|
|
|
308
358
|
}
|
|
309
359
|
}
|
|
310
360
|
/**
|
|
311
|
-
* Hydrate a text slot
|
|
312
|
-
*
|
|
313
|
-
*
|
|
314
|
-
*
|
|
315
|
-
*
|
|
316
|
-
* For reactive values (signals/functions), the effect updates the
|
|
317
|
-
* existing text node in place. For nested TemplateResults, we
|
|
318
|
-
* recursively hydrate against the captured sibling range.
|
|
319
|
-
*/
|
|
320
|
-
/**
|
|
321
|
-
* First text node inside a marker pair's range, or a fresh empty one inserted
|
|
322
|
-
* before the end marker (when the SSR value was empty → no text node yet).
|
|
361
|
+
* Hydrate a text slot against its SSR boundary-marker pair. A reactive slot
|
|
362
|
+
* (signal/function) always goes through the swap-capable structured path so a
|
|
363
|
+
* value that changes type (scalar ↔ template ↔ array) re-renders correctly; a
|
|
364
|
+
* direct template/array adopts its SSR range once; a static scalar is left as-is.
|
|
323
365
|
*/
|
|
324
|
-
function reactiveTextNode(pair) {
|
|
325
|
-
for (let n = pair.start.nextSibling; n !== null && n !== pair.end; n = n.nextSibling) {
|
|
326
|
-
if (n.nodeType === 3 /* TEXT */)
|
|
327
|
-
return n;
|
|
328
|
-
}
|
|
329
|
-
const doc = pair.end.ownerDocument ?? document;
|
|
330
|
-
const fresh = doc.createTextNode("");
|
|
331
|
-
pair.end.parentNode?.insertBefore(fresh, pair.end);
|
|
332
|
-
return fresh;
|
|
333
|
-
}
|
|
334
366
|
function hydrateTextSlot(commentMarker, value, cleanups, mountHooks, markerCursor) {
|
|
335
367
|
// Every text slot is SSR-wrapped in a <!--$-->…<!--/$--> pair, and its path
|
|
336
368
|
// resolves (via collapseMarkerRanges) to the start marker. Consume the
|
|
@@ -345,35 +377,32 @@ function hydrateTextSlot(commentMarker, value, cleanups, mountHooks, markerCurso
|
|
|
345
377
|
const reactiveFn = isSignal(value) || typeof value === "function"
|
|
346
378
|
? value
|
|
347
379
|
: null;
|
|
348
|
-
|
|
349
|
-
//
|
|
350
|
-
//
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
380
|
+
// Reactive slot — its value TYPE can change across renders (scalar ↔ template
|
|
381
|
+
// ↔ array), e.g. `${() => collapsed() ? '' : html`<span>…</span>`}`. Always
|
|
382
|
+
// use the swap-capable structured path: its effect re-renders the value
|
|
383
|
+
// (whatever type) into the marker range via renderValueToNodes. Locking a
|
|
384
|
+
// reactive slot to a scalar text-node effect (based on its FIRST value) would
|
|
385
|
+
// String() a later template/array into "[object Object]".
|
|
386
|
+
if (reactiveFn) {
|
|
387
|
+
hydrateReactiveStructured(reactiveFn, pair, cleanups, mountHooks, markerCursor);
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
// Non-reactive (direct) value — adopt the SSR range once.
|
|
391
|
+
if (isTemplateResult(value) || Array.isArray(value)) {
|
|
356
392
|
const range = [];
|
|
357
393
|
for (let n = pair.start.nextSibling; n !== null && n !== pair.end; n = n.nextSibling) {
|
|
358
394
|
range.push(n);
|
|
359
395
|
}
|
|
360
|
-
if (isTemplateResult(
|
|
361
|
-
hydrateTemplateResult(
|
|
396
|
+
if (isTemplateResult(value)) {
|
|
397
|
+
hydrateTemplateResult(value, range, cleanups, mountHooks, markerCursor);
|
|
398
|
+
}
|
|
399
|
+
else if (Array.isArray(value)) {
|
|
400
|
+
// Direct array — hydrate each item so its inner marker pairs are
|
|
401
|
+
// consumed and the cursor stays aligned.
|
|
402
|
+
hydrateArrayItems(value, range, cleanups, mountHooks, markerCursor);
|
|
362
403
|
}
|
|
363
|
-
// A direct (non-reactive) array is static SSR markup; per-item reactive
|
|
364
|
-
// bindings aren't individually re-hydrated (use `${() => arr.map(…)}`).
|
|
365
|
-
return;
|
|
366
|
-
}
|
|
367
|
-
// Scalar: a reactive scalar updates the range's text node on change; a static
|
|
368
|
-
// scalar is already rendered between the markers (nothing to wire).
|
|
369
|
-
if (reactiveFn) {
|
|
370
|
-
const textNode = reactiveTextNode(pair);
|
|
371
|
-
const dispose = effect(() => {
|
|
372
|
-
const v = reactiveFn();
|
|
373
|
-
textNode.data = v == null || v === false ? "" : String(v);
|
|
374
|
-
});
|
|
375
|
-
cleanups.push(dispose);
|
|
376
404
|
}
|
|
405
|
+
// else: static scalar — already rendered between the markers.
|
|
377
406
|
}
|
|
378
407
|
/**
|
|
379
408
|
* Pre-marker fallback — best-effort hydration when the SSR markup carries no
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@c9up/aurora",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.16",
|
|
4
4
|
"description": "Aurora — reactive UI runtime for the Ream framework. Tagged-template DOM, signal-based state, isomorphic SSR + hydration, zero build step.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -46,7 +46,10 @@
|
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@types/node": "^22.19.15",
|
|
49
|
+
"@vitest/browser": "4.1.6",
|
|
50
|
+
"@vitest/browser-playwright": "^4.1.9",
|
|
49
51
|
"happy-dom": "^15.11.7",
|
|
52
|
+
"playwright": "^1.61.1",
|
|
50
53
|
"typescript": "^6.0.2",
|
|
51
54
|
"vitest": "^4.1.2"
|
|
52
55
|
},
|
|
@@ -68,6 +71,7 @@
|
|
|
68
71
|
"typecheck": "tsc --noEmit",
|
|
69
72
|
"test": "vitest run",
|
|
70
73
|
"lint": "biome check src/",
|
|
71
|
-
"test:coverage": "vitest run --coverage"
|
|
74
|
+
"test:coverage": "vitest run --coverage",
|
|
75
|
+
"test:browser": "vitest run -c vitest.browser.config.ts"
|
|
72
76
|
}
|
|
73
77
|
}
|
package/src/AuroraManager.ts
CHANGED
|
@@ -30,6 +30,15 @@ export interface AuroraManagerConfig {
|
|
|
30
30
|
* Override only if you want to serve a custom build.
|
|
31
31
|
*/
|
|
32
32
|
auroraDistRoot?: string;
|
|
33
|
+
/**
|
|
34
|
+
* URL prefix the asset routes mount under. The aurora runtime is served
|
|
35
|
+
* from `<assetsPrefix>/aurora/*` and the app's pages from
|
|
36
|
+
* `<assetsPrefix>/pages/*`, and the SSR importmap + page URLs derive from
|
|
37
|
+
* it. Default `/_assets` (the leading underscore namespaces framework
|
|
38
|
+
* assets away from app routes, Next.js `/_next` style). Set e.g. `/assets`
|
|
39
|
+
* for an underscore-free scheme. An explicit `pages.urlPrefix` still wins.
|
|
40
|
+
*/
|
|
41
|
+
assetsPrefix?: string;
|
|
33
42
|
}
|
|
34
43
|
|
|
35
44
|
const DEFAULT_AURORA_DIST = resolvePath(
|
|
@@ -37,17 +46,40 @@ const DEFAULT_AURORA_DIST = resolvePath(
|
|
|
37
46
|
"../dist",
|
|
38
47
|
);
|
|
39
48
|
|
|
49
|
+
/** Normalize an asset prefix: ensure a leading slash, drop trailing slashes. */
|
|
50
|
+
function normalizePrefix(prefix: string): string {
|
|
51
|
+
const withLead = prefix.startsWith("/") ? prefix : `/${prefix}`;
|
|
52
|
+
return withLead.replace(/\/+$/, "") || "/";
|
|
53
|
+
}
|
|
54
|
+
|
|
40
55
|
export class AuroraManager {
|
|
41
56
|
readonly pages: Pages;
|
|
42
57
|
readonly auroraDistRoot: string;
|
|
58
|
+
/** Resolved asset prefix (default `/_assets`). */
|
|
59
|
+
readonly assetsPrefix: string;
|
|
60
|
+
/** Mount path for the aurora runtime — `<assetsPrefix>/aurora`. */
|
|
61
|
+
readonly auroraAssetPath: string;
|
|
62
|
+
/** Mount path for the app's pages — `<assetsPrefix>/pages`. */
|
|
63
|
+
readonly pageAssetPath: string;
|
|
43
64
|
|
|
44
65
|
constructor(config: AuroraManagerConfig) {
|
|
45
|
-
this.
|
|
66
|
+
this.assetsPrefix = normalizePrefix(config.assetsPrefix ?? "/_assets");
|
|
67
|
+
this.auroraAssetPath = `${this.assetsPrefix}/aurora`;
|
|
68
|
+
this.pageAssetPath = `${this.assetsPrefix}/pages`;
|
|
69
|
+
// Pages serve their compiled JS from the same prefix unless the app
|
|
70
|
+
// pins an explicit urlPrefix.
|
|
71
|
+
this.pages = new Pages({
|
|
72
|
+
...config.pages,
|
|
73
|
+
urlPrefix: config.pages.urlPrefix ?? this.pageAssetPath,
|
|
74
|
+
});
|
|
46
75
|
this.auroraDistRoot = config.auroraDistRoot ?? DEFAULT_AURORA_DIST;
|
|
47
76
|
}
|
|
48
77
|
|
|
49
78
|
/**
|
|
50
|
-
* SSR + hydrate + ship the document.
|
|
79
|
+
* SSR + hydrate + ship the document. The importmap default points
|
|
80
|
+
* `@c9up/aurora` at this manager's `assetsPrefix`; a caller's
|
|
81
|
+
* `options.importmap` still overrides (e.g. to remap to an app-curated
|
|
82
|
+
* browser entry).
|
|
51
83
|
*/
|
|
52
84
|
render(
|
|
53
85
|
ctx: RenderHttpContext,
|
|
@@ -55,7 +87,13 @@ export class AuroraManager {
|
|
|
55
87
|
props: unknown,
|
|
56
88
|
options?: RenderPageOptions,
|
|
57
89
|
): Promise<void> {
|
|
58
|
-
return renderPage(ctx, this.pages, name, props,
|
|
90
|
+
return renderPage(ctx, this.pages, name, props, {
|
|
91
|
+
...options,
|
|
92
|
+
importmap: {
|
|
93
|
+
"@c9up/aurora": `${this.auroraAssetPath}/index.js`,
|
|
94
|
+
...options?.importmap,
|
|
95
|
+
},
|
|
96
|
+
});
|
|
59
97
|
}
|
|
60
98
|
|
|
61
99
|
/**
|
package/src/AuroraProvider.ts
CHANGED
|
@@ -91,8 +91,16 @@ export default class AuroraProvider {
|
|
|
91
91
|
if (!this.app.container.has("router")) return;
|
|
92
92
|
const router = this.app.container.resolve<ReamRouter>("router");
|
|
93
93
|
const manager = this.app.container.resolve<AuroraManager>(AuroraManager);
|
|
94
|
-
|
|
95
|
-
|
|
94
|
+
// Mount paths derive from the configured `assetsPrefix` (default
|
|
95
|
+
// `/_assets`) — set `config.aurora.assetsPrefix` to change the scheme.
|
|
96
|
+
router.get(
|
|
97
|
+
`${manager.auroraAssetPath}/*`,
|
|
98
|
+
adaptHandler(manager.auroraAssetsHandler()),
|
|
99
|
+
);
|
|
100
|
+
router.get(
|
|
101
|
+
`${manager.pageAssetPath}/*`,
|
|
102
|
+
adaptHandler(manager.pageAssetsHandler()),
|
|
103
|
+
);
|
|
96
104
|
}
|
|
97
105
|
|
|
98
106
|
async ready(): Promise<void> {}
|
package/src/hydrate.ts
CHANGED
|
@@ -174,8 +174,11 @@ function hydrateReactiveStructured(
|
|
|
174
174
|
if (firstRun) {
|
|
175
175
|
firstRun = false;
|
|
176
176
|
// Reuse SSR markup: hydrate reactive bindings INSIDE the nested
|
|
177
|
-
//
|
|
178
|
-
//
|
|
177
|
+
// value against the captured nodes. Inner boundary markers are
|
|
178
|
+
// consumed from the same cursor (document order) — for an ARRAY this
|
|
179
|
+
// MUST recurse into every item, else the items' marker pairs go
|
|
180
|
+
// unconsumed and the cursor desyncs, wiring slots AFTER the list to
|
|
181
|
+
// the wrong range (SSR list "present but not painted").
|
|
179
182
|
if (isTemplateResult(next)) {
|
|
180
183
|
hydrateTemplateResult(
|
|
181
184
|
next,
|
|
@@ -184,6 +187,14 @@ function hydrateReactiveStructured(
|
|
|
184
187
|
mountHooks,
|
|
185
188
|
markerCursor,
|
|
186
189
|
);
|
|
190
|
+
} else if (Array.isArray(next)) {
|
|
191
|
+
hydrateArrayItems(
|
|
192
|
+
next,
|
|
193
|
+
currentNodes,
|
|
194
|
+
localCleanups,
|
|
195
|
+
mountHooks,
|
|
196
|
+
markerCursor,
|
|
197
|
+
);
|
|
187
198
|
}
|
|
188
199
|
return;
|
|
189
200
|
}
|
|
@@ -213,6 +224,67 @@ function hydrateReactiveStructured(
|
|
|
213
224
|
});
|
|
214
225
|
}
|
|
215
226
|
|
|
227
|
+
/**
|
|
228
|
+
* Top-level live (SSR) node count a value contributes inside an array slot: a
|
|
229
|
+
* TemplateResult contributes its template's root-node count, a nested array the
|
|
230
|
+
* sum of its items, a non-empty scalar one text node, null/undefined/false none.
|
|
231
|
+
* Used to slice the array's range per item during hydration.
|
|
232
|
+
*/
|
|
233
|
+
function liveNodeCount(value: unknown): number {
|
|
234
|
+
if (value === null || value === undefined || value === false) return 0;
|
|
235
|
+
if (isTemplateResult(value)) {
|
|
236
|
+
return getTemplate(value.strings).element.content.childNodes.length;
|
|
237
|
+
}
|
|
238
|
+
if (Array.isArray(value)) {
|
|
239
|
+
let n = 0;
|
|
240
|
+
for (const v of value) n += liveNodeCount(v);
|
|
241
|
+
return n;
|
|
242
|
+
}
|
|
243
|
+
return 1; // scalar → one inlined text node
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Hydrate the items of a reactive array against the SSR nodes inside its marker
|
|
248
|
+
* range. Each item is hydrated against its own slice of the (marker-collapsed)
|
|
249
|
+
* range, IN ORDER, so every item's inner marker pairs are consumed in document
|
|
250
|
+
* order and the global cursor stays aligned for slots AFTER the list. Item
|
|
251
|
+
* templates need a stable top-level node count (the common
|
|
252
|
+
* `arr.map(x => html`<li>…</li>`)` shape — single root, no surrounding
|
|
253
|
+
* whitespace); bare adjacent scalar items can merge in the browser, so use
|
|
254
|
+
* template items for hydrated lists.
|
|
255
|
+
*/
|
|
256
|
+
function hydrateArrayItems(
|
|
257
|
+
items: unknown[],
|
|
258
|
+
rangeNodes: ChildNode[],
|
|
259
|
+
cleanups: Disposer[],
|
|
260
|
+
mountHooks: Array<EffectCallback>,
|
|
261
|
+
markerCursor: MarkerCursor,
|
|
262
|
+
): void {
|
|
263
|
+
const nodes = collapseMarkerRanges(rangeNodes);
|
|
264
|
+
let offset = 0;
|
|
265
|
+
for (const item of items) {
|
|
266
|
+
const count = liveNodeCount(item);
|
|
267
|
+
if (isTemplateResult(item)) {
|
|
268
|
+
hydrateTemplateResult(
|
|
269
|
+
item,
|
|
270
|
+
nodes.slice(offset, offset + count),
|
|
271
|
+
cleanups,
|
|
272
|
+
mountHooks,
|
|
273
|
+
markerCursor,
|
|
274
|
+
);
|
|
275
|
+
} else if (Array.isArray(item)) {
|
|
276
|
+
hydrateArrayItems(
|
|
277
|
+
item,
|
|
278
|
+
nodes.slice(offset, offset + count),
|
|
279
|
+
cleanups,
|
|
280
|
+
mountHooks,
|
|
281
|
+
markerCursor,
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
offset += count;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
216
288
|
/**
|
|
217
289
|
* Adopt SSR markup inside `container`. `factory` is the same function
|
|
218
290
|
* that was rendered server-side — its output (a TemplateResult tree)
|
|
@@ -409,33 +481,11 @@ function hydrateSlot(
|
|
|
409
481
|
}
|
|
410
482
|
|
|
411
483
|
/**
|
|
412
|
-
* Hydrate a text slot
|
|
413
|
-
*
|
|
414
|
-
*
|
|
415
|
-
*
|
|
416
|
-
*
|
|
417
|
-
* For reactive values (signals/functions), the effect updates the
|
|
418
|
-
* existing text node in place. For nested TemplateResults, we
|
|
419
|
-
* recursively hydrate against the captured sibling range.
|
|
420
|
-
*/
|
|
421
|
-
/**
|
|
422
|
-
* First text node inside a marker pair's range, or a fresh empty one inserted
|
|
423
|
-
* before the end marker (when the SSR value was empty → no text node yet).
|
|
484
|
+
* Hydrate a text slot against its SSR boundary-marker pair. A reactive slot
|
|
485
|
+
* (signal/function) always goes through the swap-capable structured path so a
|
|
486
|
+
* value that changes type (scalar ↔ template ↔ array) re-renders correctly; a
|
|
487
|
+
* direct template/array adopts its SSR range once; a static scalar is left as-is.
|
|
424
488
|
*/
|
|
425
|
-
function reactiveTextNode(pair: MarkerPair): Text {
|
|
426
|
-
for (
|
|
427
|
-
let n = pair.start.nextSibling;
|
|
428
|
-
n !== null && n !== pair.end;
|
|
429
|
-
n = n.nextSibling
|
|
430
|
-
) {
|
|
431
|
-
if (n.nodeType === 3 /* TEXT */) return n as Text;
|
|
432
|
-
}
|
|
433
|
-
const doc = pair.end.ownerDocument ?? document;
|
|
434
|
-
const fresh = doc.createTextNode("");
|
|
435
|
-
pair.end.parentNode?.insertBefore(fresh, pair.end);
|
|
436
|
-
return fresh;
|
|
437
|
-
}
|
|
438
|
-
|
|
439
489
|
function hydrateTextSlot(
|
|
440
490
|
commentMarker: Node,
|
|
441
491
|
value: unknown,
|
|
@@ -464,21 +514,26 @@ function hydrateTextSlot(
|
|
|
464
514
|
isSignal(value) || typeof value === "function"
|
|
465
515
|
? (value as () => unknown)
|
|
466
516
|
: null;
|
|
467
|
-
const current = reactiveFn ? reactiveFn() : value;
|
|
468
517
|
|
|
469
|
-
//
|
|
470
|
-
//
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
518
|
+
// Reactive slot — its value TYPE can change across renders (scalar ↔ template
|
|
519
|
+
// ↔ array), e.g. `${() => collapsed() ? '' : html`<span>…</span>`}`. Always
|
|
520
|
+
// use the swap-capable structured path: its effect re-renders the value
|
|
521
|
+
// (whatever type) into the marker range via renderValueToNodes. Locking a
|
|
522
|
+
// reactive slot to a scalar text-node effect (based on its FIRST value) would
|
|
523
|
+
// String() a later template/array into "[object Object]".
|
|
524
|
+
if (reactiveFn) {
|
|
525
|
+
hydrateReactiveStructured(
|
|
526
|
+
reactiveFn,
|
|
527
|
+
pair,
|
|
528
|
+
cleanups,
|
|
529
|
+
mountHooks,
|
|
530
|
+
markerCursor,
|
|
531
|
+
);
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// Non-reactive (direct) value — adopt the SSR range once.
|
|
536
|
+
if (isTemplateResult(value) || Array.isArray(value)) {
|
|
482
537
|
const range: ChildNode[] = [];
|
|
483
538
|
for (
|
|
484
539
|
let n = pair.start.nextSibling;
|
|
@@ -487,24 +542,15 @@ function hydrateTextSlot(
|
|
|
487
542
|
) {
|
|
488
543
|
range.push(n as ChildNode);
|
|
489
544
|
}
|
|
490
|
-
if (isTemplateResult(
|
|
491
|
-
hydrateTemplateResult(
|
|
545
|
+
if (isTemplateResult(value)) {
|
|
546
|
+
hydrateTemplateResult(value, range, cleanups, mountHooks, markerCursor);
|
|
547
|
+
} else if (Array.isArray(value)) {
|
|
548
|
+
// Direct array — hydrate each item so its inner marker pairs are
|
|
549
|
+
// consumed and the cursor stays aligned.
|
|
550
|
+
hydrateArrayItems(value, range, cleanups, mountHooks, markerCursor);
|
|
492
551
|
}
|
|
493
|
-
// A direct (non-reactive) array is static SSR markup; per-item reactive
|
|
494
|
-
// bindings aren't individually re-hydrated (use `${() => arr.map(…)}`).
|
|
495
|
-
return;
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
// Scalar: a reactive scalar updates the range's text node on change; a static
|
|
499
|
-
// scalar is already rendered between the markers (nothing to wire).
|
|
500
|
-
if (reactiveFn) {
|
|
501
|
-
const textNode = reactiveTextNode(pair);
|
|
502
|
-
const dispose = effect(() => {
|
|
503
|
-
const v = reactiveFn();
|
|
504
|
-
textNode.data = v == null || v === false ? "" : String(v);
|
|
505
|
-
});
|
|
506
|
-
cleanups.push(dispose);
|
|
507
552
|
}
|
|
553
|
+
// else: static scalar — already rendered between the markers.
|
|
508
554
|
}
|
|
509
555
|
|
|
510
556
|
/**
|