@c9up/aurora 0.1.12 → 0.1.14
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/dist/cn.d.ts +27 -0
- package/dist/cn.js +908 -0
- package/dist/command.js +26 -10
- package/dist/hydrate.js +130 -10
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/render.js +33 -2
- package/dist/server/renderPage.d.ts +8 -0
- package/dist/server/renderPage.js +8 -1
- package/dist/ssr.js +15 -44
- package/dist/url.d.ts +33 -0
- package/dist/url.js +68 -0
- package/package.json +1 -1
- package/src/cn.ts +979 -0
- package/src/command.ts +24 -6
- package/src/hydrate.ts +165 -10
- package/src/index.ts +2 -0
- package/src/render.ts +37 -2
- package/src/server/renderPage.ts +16 -1
- package/src/ssr.ts +14 -41
- package/src/url.ts +91 -0
package/dist/command.js
CHANGED
|
@@ -61,19 +61,35 @@ class CommandRunner {
|
|
|
61
61
|
this.#loading(true);
|
|
62
62
|
this.#error(null);
|
|
63
63
|
try {
|
|
64
|
-
|
|
64
|
+
let result;
|
|
65
|
+
try {
|
|
66
|
+
result = await this.#task(...args);
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
if (id !== this.#runId)
|
|
70
|
+
return; // superseded — drop
|
|
71
|
+
this.#error(error);
|
|
72
|
+
for (const handler of this.#onFail)
|
|
73
|
+
handler(error);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
65
76
|
if (id !== this.#runId)
|
|
66
77
|
return; // superseded by a newer run — drop
|
|
67
78
|
this.#data(result);
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
79
|
+
// onSuccess runs OUTSIDE the task's failure boundary: success is
|
|
80
|
+
// decided by the task, never by the callback. A throw here (e.g. a
|
|
81
|
+
// render error after the data lands) is a handler bug — surfacing it
|
|
82
|
+
// as a task failure would route to onFail, which on a guarded page
|
|
83
|
+
// masquerades as a logout. Report it, but never reclassify it.
|
|
84
|
+
try {
|
|
85
|
+
for (const handler of this.#onSuccess)
|
|
86
|
+
handler(result);
|
|
87
|
+
}
|
|
88
|
+
catch (handlerError) {
|
|
89
|
+
if (typeof console !== "undefined") {
|
|
90
|
+
console.error("[aurora] a command onSuccess handler threw — not treated as a task failure:", handlerError);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
77
93
|
}
|
|
78
94
|
finally {
|
|
79
95
|
if (id === this.#runId) {
|
package/dist/hydrate.js
CHANGED
|
@@ -231,16 +231,64 @@ function hydrateTemplateResult(result, liveNodes, cleanups, mountHooks, markerCu
|
|
|
231
231
|
* Text-slot paths point to a comment marker that doesn't exist in
|
|
232
232
|
* hydration markup — we tolerate the miss and return null.
|
|
233
233
|
*/
|
|
234
|
+
/**
|
|
235
|
+
* Collapse each top-level `<!--$-->…<!--/$-->` range in `nodes` to a SINGLE
|
|
236
|
+
* entry (its start marker), dropping the in-range content + end marker from the
|
|
237
|
+
* count. SSR expands a structured slot (reactive OR a direct nested template)
|
|
238
|
+
* to a node RANGE, but the parsed client template counts every slot as exactly
|
|
239
|
+
* ONE comment node — so without this collapse the extra range nodes shift the
|
|
240
|
+
* childNode index of every FOLLOWING sibling slot (dead bindings / "slot path
|
|
241
|
+
* not found"). Nested ranges (depth > 0) are skipped wholesale: they belong to
|
|
242
|
+
* the outer slot's content and are hydrated when we recurse into it.
|
|
243
|
+
*/
|
|
244
|
+
function collapseMarkerRanges(nodes) {
|
|
245
|
+
const out = [];
|
|
246
|
+
let depth = 0;
|
|
247
|
+
for (const n of nodes) {
|
|
248
|
+
if (n.nodeType === 8 /* Comment */) {
|
|
249
|
+
const data = n.data;
|
|
250
|
+
if (data === SLOT_START) {
|
|
251
|
+
if (depth === 0)
|
|
252
|
+
out.push(n); // the whole range counts as one node
|
|
253
|
+
depth += 1;
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
if (data === SLOT_END) {
|
|
257
|
+
if (depth > 0)
|
|
258
|
+
depth -= 1;
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
if (depth === 0)
|
|
263
|
+
out.push(n);
|
|
264
|
+
}
|
|
265
|
+
return out;
|
|
266
|
+
}
|
|
234
267
|
function resolvePathLive(_root, path, rootNodes) {
|
|
235
268
|
if (path.length === 0)
|
|
236
269
|
return null;
|
|
237
|
-
|
|
270
|
+
// Collapse marker ranges at EVERY level so the live child list matches the
|
|
271
|
+
// parsed template's one-node-per-slot shape (see collapseMarkerRanges).
|
|
272
|
+
let children = collapseMarkerRanges(rootNodes);
|
|
273
|
+
let node = children[path[0]] ?? null;
|
|
238
274
|
for (let i = 1; node && i < path.length; i++) {
|
|
239
|
-
|
|
275
|
+
children = collapseMarkerRanges(Array.from(node.childNodes));
|
|
276
|
+
node = children[path[i]] ?? null;
|
|
240
277
|
}
|
|
241
278
|
return node;
|
|
242
279
|
}
|
|
243
280
|
function hydrateSlot(slot, node, value, cleanups, mountHooks, markerCursor) {
|
|
281
|
+
// Type guard: an attr/bool/prop/event slot needs an Element. On a SSR↔client
|
|
282
|
+
// structural desync the path can resolve to an EXISTING node of the wrong
|
|
283
|
+
// type (Text/Comment); casting it to Element and calling setAttribute would
|
|
284
|
+
// throw "setAttribute is not a function". Skip the binding (fail-soft) rather
|
|
285
|
+
// than crash hydration. Text slots accept text/comment/element, so they pass.
|
|
286
|
+
if (slot.kind !== "text" && node.nodeType !== 1) {
|
|
287
|
+
if (typeof console !== "undefined") {
|
|
288
|
+
console.warn(`[aurora] hydrate: slot (${slot.kind}) path ${slot.path.join(".")} resolved to a non-element node — skipping binding`);
|
|
289
|
+
}
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
244
292
|
switch (slot.kind) {
|
|
245
293
|
case "text":
|
|
246
294
|
hydrateTextSlot(node, value, cleanups, mountHooks, markerCursor);
|
|
@@ -269,15 +317,70 @@ function hydrateSlot(slot, node, value, cleanups, mountHooks, markerCursor) {
|
|
|
269
317
|
* existing text node in place. For nested TemplateResults, we
|
|
270
318
|
* recursively hydrate against the captured sibling range.
|
|
271
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).
|
|
323
|
+
*/
|
|
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
|
+
}
|
|
272
334
|
function hydrateTextSlot(commentMarker, value, cleanups, mountHooks, markerCursor) {
|
|
273
|
-
//
|
|
274
|
-
//
|
|
275
|
-
//
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
335
|
+
// Every text slot is SSR-wrapped in a <!--$-->…<!--/$--> pair, and its path
|
|
336
|
+
// resolves (via collapseMarkerRanges) to the start marker. Consume the
|
|
337
|
+
// matching pair in document order and bind within its range.
|
|
338
|
+
const pair = markerCursor.pairs[markerCursor.i];
|
|
339
|
+
if (pair === undefined) {
|
|
340
|
+
// Legacy markup without per-slot markers (mismatched older SSR build).
|
|
341
|
+
legacyHydrateTextSlot(commentMarker, value, cleanups, mountHooks, markerCursor);
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
markerCursor.i += 1;
|
|
345
|
+
const reactiveFn = isSignal(value) || typeof value === "function"
|
|
346
|
+
? value
|
|
347
|
+
: null;
|
|
348
|
+
const current = reactiveFn ? reactiveFn() : value;
|
|
349
|
+
// Structured value (nested template / array): reactive → swap on change;
|
|
350
|
+
// direct → adopt the SSR range once (inner bindings wired against it).
|
|
351
|
+
if (isTemplateResult(current) || Array.isArray(current)) {
|
|
352
|
+
if (reactiveFn) {
|
|
353
|
+
hydrateReactiveStructured(reactiveFn, pair, cleanups, mountHooks, markerCursor);
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
const range = [];
|
|
357
|
+
for (let n = pair.start.nextSibling; n !== null && n !== pair.end; n = n.nextSibling) {
|
|
358
|
+
range.push(n);
|
|
359
|
+
}
|
|
360
|
+
if (isTemplateResult(current)) {
|
|
361
|
+
hydrateTemplateResult(current, range, cleanups, mountHooks, markerCursor);
|
|
362
|
+
}
|
|
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
|
+
}
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Pre-marker fallback — best-effort hydration when the SSR markup carries no
|
|
380
|
+
* per-slot boundary markers (a mismatched older SSR build). Current builds wrap
|
|
381
|
+
* every text slot, so this path is dead for matched server/client versions.
|
|
382
|
+
*/
|
|
383
|
+
function legacyHydrateTextSlot(commentMarker, value, cleanups, mountHooks, markerCursor) {
|
|
281
384
|
if (isSignal(value) || typeof value === "function") {
|
|
282
385
|
const fn = value;
|
|
283
386
|
// First, evaluate eagerly to detect a structured value (nested
|
|
@@ -332,6 +435,23 @@ function hydrateTextSlot(commentMarker, value, cleanups, mountHooks, markerCurso
|
|
|
332
435
|
return;
|
|
333
436
|
}
|
|
334
437
|
if (isTemplateResult(value)) {
|
|
438
|
+
// DIRECT nested template (component composition, `${Layout({…})}`). SSR
|
|
439
|
+
// wrapped it in a boundary-marker pair (same scheme as a reactive
|
|
440
|
+
// structured slot). Consume the pair in document order and hydrate the
|
|
441
|
+
// nested template against its captured range — wiring inner bindings to
|
|
442
|
+
// the SSR nodes and keeping the marker cursor aligned.
|
|
443
|
+
const pair = markerCursor.pairs[markerCursor.i];
|
|
444
|
+
if (pair !== undefined) {
|
|
445
|
+
markerCursor.i += 1;
|
|
446
|
+
const range = [];
|
|
447
|
+
for (let n = pair.start.nextSibling; n !== null && n !== pair.end; n = n.nextSibling) {
|
|
448
|
+
range.push(n);
|
|
449
|
+
}
|
|
450
|
+
hydrateTemplateResult(value, range, cleanups, mountHooks, markerCursor);
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
// Legacy markup without markers (older SSR build): best-effort against
|
|
454
|
+
// the single resolved node.
|
|
335
455
|
hydrateTemplateResult(value, [commentMarker], cleanups, mountHooks, markerCursor);
|
|
336
456
|
return;
|
|
337
457
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export type { CookieOptions, PersistedSignalOptions, ShareData, StorageArea, WebStorageOptions, WindowSize, } from "./browser.js";
|
|
2
2
|
export { back, clipboard, cookie, forward, hash, mediaQuery, navigate, online, persistedSignal, prefersDark, queryParam, redirect, reload, replace, session, share, storage, visibility, WebStorage, windowSize, } from "./browser.js";
|
|
3
|
+
export { type ClassValue, clsx, cn, twMerge } from "./cn.js";
|
|
3
4
|
export type { Command } from "./command.js";
|
|
4
5
|
export { command } from "./command.js";
|
|
5
6
|
export { component, onMount, onUnmount } from "./component.js";
|
|
@@ -21,3 +22,4 @@ export { type AuroraHttpContext, type AuroraResponse, type AuroraRouteConfig, au
|
|
|
21
22
|
export { createRpcClient, isRpcError, type RpcCall, type RpcClient, type RpcClientOptions, RpcError, type RpcResult, } from "./rpc.js";
|
|
22
23
|
export { renderToString } from "./ssr.js";
|
|
23
24
|
export type { TemplateResult } from "./types.js";
|
|
25
|
+
export { getRouteManifest, setRouteManifest, urlFor } from "./url.js";
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { back, clipboard, cookie, forward, hash, mediaQuery, navigate, online, persistedSignal, prefersDark, queryParam, redirect, reload, replace, session, share, storage, visibility, WebStorage, windowSize, } from "./browser.js";
|
|
2
|
+
export { clsx, cn, twMerge } from "./cn.js";
|
|
2
3
|
export { command } from "./command.js";
|
|
3
4
|
export { component, onMount, onUnmount } from "./component.js";
|
|
4
5
|
export { form } from "./form.js";
|
|
@@ -16,3 +17,4 @@ export { render } from "./render.js";
|
|
|
16
17
|
export { auroraRoute, } from "./route.js";
|
|
17
18
|
export { createRpcClient, isRpcError, RpcError, } from "./rpc.js";
|
|
18
19
|
export { renderToString } from "./ssr.js";
|
|
20
|
+
export { getRouteManifest, setRouteManifest, urlFor } from "./url.js";
|
package/dist/render.js
CHANGED
|
@@ -71,6 +71,26 @@ export function mount(result, cleanups, mounted, mountHooks) {
|
|
|
71
71
|
for (let i = 0; i < tpl.slots.length; i++) {
|
|
72
72
|
const slot = tpl.slots[i];
|
|
73
73
|
const node = resolvePath(fragment, slot.path);
|
|
74
|
+
if (node === null) {
|
|
75
|
+
// Path didn't resolve — skip this binding rather than crash (see
|
|
76
|
+
// resolvePath). Degrades to a dead binding; the surrounding render
|
|
77
|
+
// (and any command driving it) survives.
|
|
78
|
+
if (typeof console !== "undefined") {
|
|
79
|
+
console.warn(`[aurora] render: slot ${i} (${slot.kind}) path ${slot.path.join(".")} did not resolve — skipping binding`);
|
|
80
|
+
}
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
// Type guard: an attr/bool/prop/event slot needs an Element. On a
|
|
84
|
+
// structural desync the path can land on an EXISTING node of the wrong
|
|
85
|
+
// type (Text/Comment) — casting it to Element and calling setAttribute
|
|
86
|
+
// would throw "setAttribute is not a function". Degrade like the null
|
|
87
|
+
// case (skip the binding) instead of crashing the whole render.
|
|
88
|
+
if (slot.kind !== "text" && node.nodeType !== 1) {
|
|
89
|
+
if (typeof console !== "undefined") {
|
|
90
|
+
console.warn(`[aurora] render: slot ${i} (${slot.kind}) path ${slot.path.join(".")} resolved to a non-element node — skipping binding`);
|
|
91
|
+
}
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
74
94
|
if (slot.kind === "attr" && slot.staticParts !== undefined) {
|
|
75
95
|
collectMultiAttr(slot, node, result.values[i], multiGroups);
|
|
76
96
|
}
|
|
@@ -88,8 +108,19 @@ export function mount(result, cleanups, mounted, mountHooks) {
|
|
|
88
108
|
}
|
|
89
109
|
function resolvePath(root, path) {
|
|
90
110
|
let node = root;
|
|
91
|
-
for (const i of path)
|
|
92
|
-
|
|
111
|
+
for (const i of path) {
|
|
112
|
+
const next = node.childNodes[i];
|
|
113
|
+
// Fail-soft: a path step that runs off the live child list means the
|
|
114
|
+
// tree diverged from the parsed template (a hydration desync). Return
|
|
115
|
+
// null so the caller skips the binding instead of dereferencing
|
|
116
|
+
// `undefined.childNodes` and crashing the whole render — which, when the
|
|
117
|
+
// render runs inside a command's onSuccess, used to masquerade as a
|
|
118
|
+
// task failure (and on a guarded page, a logout). Mirrors
|
|
119
|
+
// `resolvePathLive` in hydrate.ts.
|
|
120
|
+
if (next === undefined)
|
|
121
|
+
return null;
|
|
122
|
+
node = next;
|
|
123
|
+
}
|
|
93
124
|
return node;
|
|
94
125
|
}
|
|
95
126
|
function applySlot(slot, node, value, cleanups, mounted, mountHooks) {
|
|
@@ -58,5 +58,13 @@ export interface RenderPageOptions {
|
|
|
58
58
|
* targets.
|
|
59
59
|
*/
|
|
60
60
|
rootId?: string;
|
|
61
|
+
/**
|
|
62
|
+
* Named-route manifest (`name → path-pattern`) for the isomorphic
|
|
63
|
+
* `urlFor()` helper — build it with Ream's `router.namedManifest()`. It is
|
|
64
|
+
* installed server-side before the page renders AND serialized into the page
|
|
65
|
+
* so the hydrate bootstrap re-installs it, making `urlFor` work identically
|
|
66
|
+
* in SSR and the browser. Omit if the app doesn't use `urlFor`.
|
|
67
|
+
*/
|
|
68
|
+
routes?: Record<string, string>;
|
|
61
69
|
}
|
|
62
70
|
export declare function renderPage<P>(ctx: RenderHttpContext, pages: Pages, name: string, props: P, options?: RenderPageOptions): Promise<void>;
|
|
@@ -23,7 +23,12 @@
|
|
|
23
23
|
* </body>
|
|
24
24
|
*/
|
|
25
25
|
import { renderToString } from "../ssr.js";
|
|
26
|
+
import { setRouteManifest } from "../url.js";
|
|
26
27
|
export async function renderPage(ctx, pages, name, props, options = {}) {
|
|
28
|
+
// Install the route manifest BEFORE rendering so a page calling `urlFor`
|
|
29
|
+
// during SSR resolves against the same map the client will get.
|
|
30
|
+
if (options.routes)
|
|
31
|
+
setRouteManifest(options.routes);
|
|
27
32
|
const factory = await pages.resolve(name);
|
|
28
33
|
// The factory must be invoked the SAME way client-side for hydrate
|
|
29
34
|
// to find matching slots — `Page(props)` is the contract.
|
|
@@ -51,11 +56,13 @@ ${options.headExtra ?? ""}
|
|
|
51
56
|
props,
|
|
52
57
|
url: pageUrl,
|
|
53
58
|
rootId,
|
|
59
|
+
routes: options.routes ?? {},
|
|
54
60
|
})}</script>
|
|
55
61
|
<script type="module">
|
|
56
|
-
import { hydrate } from '@c9up/aurora'
|
|
62
|
+
import { hydrate, setRouteManifest } from '@c9up/aurora'
|
|
57
63
|
import Page from ${JSON.stringify(pageUrl)}
|
|
58
64
|
const data = JSON.parse(document.getElementById('aurora-page-data').textContent)
|
|
65
|
+
setRouteManifest(data.routes ?? {})
|
|
59
66
|
hydrate(document.getElementById(data.rootId), () => Page(data.props))
|
|
60
67
|
</script>
|
|
61
68
|
</body>
|
package/dist/ssr.js
CHANGED
|
@@ -62,31 +62,24 @@ function stringifyTemplateResult(result) {
|
|
|
62
62
|
if (i < values.length && !skipValue) {
|
|
63
63
|
const value = values[i];
|
|
64
64
|
const inAttr = isInsideAttribute(out);
|
|
65
|
-
if (
|
|
66
|
-
// Reactive text slot whose value is a nested template /
|
|
67
|
-
// array — wrap the rendered content in boundary markers so
|
|
68
|
-
// hydration can locate the exact node range and SWAP it when
|
|
69
|
-
// the signal changes client-side. Without these markers a
|
|
70
|
-
// nested-template slot hydrates once and then goes stale
|
|
71
|
-
// (no way to find where the subtree starts/ends). Scalar
|
|
72
|
-
// reactive slots (`${signal}` → text) are NOT wrapped: their
|
|
73
|
-
// hydration updates the text node in place, no range needed.
|
|
74
|
-
out += `<!--${SLOT_START}-->`;
|
|
75
|
-
out += stringifyValue(value, false);
|
|
76
|
-
out += `<!--${SLOT_END}-->`;
|
|
77
|
-
}
|
|
78
|
-
else if (inAttr) {
|
|
65
|
+
if (inAttr) {
|
|
79
66
|
out += stringifyValue(value, true);
|
|
80
67
|
}
|
|
81
68
|
else {
|
|
82
|
-
// Text-region
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
// node
|
|
88
|
-
|
|
89
|
-
|
|
69
|
+
// Text-region slot — ALWAYS wrap in boundary markers so the SSR
|
|
70
|
+
// node structure matches the client template, which keeps exactly
|
|
71
|
+
// ONE comment node per slot. An inlined value otherwise MERGES
|
|
72
|
+
// with adjacent static text or sibling values when the browser
|
|
73
|
+
// parses the SSR HTML (`<p>Hello ${x}!</p>` → ONE text node, not
|
|
74
|
+
// three), dropping the node count and desyncing the slot AND every
|
|
75
|
+
// following sibling binding (text, attr, event). Hydration
|
|
76
|
+
// collapses each `<!--$-->…<!--/$-->` range back to one node
|
|
77
|
+
// (collapseMarkerRanges) so paths align exactly; the range also
|
|
78
|
+
// anchors scalar text updates and nested-template swaps. Same
|
|
79
|
+
// part-marker approach as lit-html / Solid.
|
|
80
|
+
out += `<!--${SLOT_START}-->`;
|
|
81
|
+
out += stringifyValue(value, false);
|
|
82
|
+
out += `<!--${SLOT_END}-->`;
|
|
90
83
|
}
|
|
91
84
|
}
|
|
92
85
|
}
|
|
@@ -95,28 +88,6 @@ function stringifyTemplateResult(result) {
|
|
|
95
88
|
/** Boundary-marker comment payloads (kept in sync with hydrate.ts). */
|
|
96
89
|
const SLOT_START = "$";
|
|
97
90
|
const SLOT_END = "/$";
|
|
98
|
-
/**
|
|
99
|
-
* True when `value` is a reactive expression (signal / function) whose
|
|
100
|
-
* current evaluation is a structured node payload (a nested
|
|
101
|
-
* TemplateResult, or an array). These are the slots that can SWAP their
|
|
102
|
-
* subtree on a client-side change and therefore need boundary markers
|
|
103
|
-
* for hydration to find the range. A reactive slot resolving to a
|
|
104
|
-
* scalar (string / number) is updated in place and needs no markers.
|
|
105
|
-
*/
|
|
106
|
-
function isReactiveStructuredSlot(value) {
|
|
107
|
-
if (!isSignal(value) && typeof value !== "function")
|
|
108
|
-
return false;
|
|
109
|
-
let evaluated;
|
|
110
|
-
try {
|
|
111
|
-
evaluated = isSignal(value)
|
|
112
|
-
? value()
|
|
113
|
-
: value();
|
|
114
|
-
}
|
|
115
|
-
catch {
|
|
116
|
-
return false;
|
|
117
|
-
}
|
|
118
|
-
return isTemplateResult(evaluated) || Array.isArray(evaluated);
|
|
119
|
-
}
|
|
120
91
|
/**
|
|
121
92
|
* Returns true if the position at the end of `htmlSoFar` lives inside
|
|
122
93
|
* the value region of an HTML tag (between `<` and `>`). The check
|
package/dist/url.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `urlFor` — isomorphic named-route URL builder (AdonisJS v7 parity), the client
|
|
3
|
+
* half of Ream's `router.urlFor()`.
|
|
4
|
+
*
|
|
5
|
+
* Routes live server-side in Ream's router; this helper builds URLs from a
|
|
6
|
+
* serialized `name → path-pattern` manifest so the SAME `urlFor(name, params)`
|
|
7
|
+
* works in a page during SSR and after hydration in the browser — no more
|
|
8
|
+
* hard-coded paths like `redirect('/login')` / `href: '/team'`.
|
|
9
|
+
*
|
|
10
|
+
* urlFor('users.show', { id: 42 }) // → '/users/42'
|
|
11
|
+
* urlFor('auth.login') // → '/login'
|
|
12
|
+
* urlFor('search', {}, { q: 'ream', p: 2 })// → '/search?q=ream&p=2'
|
|
13
|
+
*
|
|
14
|
+
* The manifest is populated by {@link setRouteManifest}: `renderPage` calls it
|
|
15
|
+
* server-side from `options.routes` (build it with `router.namedManifest()`), and
|
|
16
|
+
* injects the same map into the page so the hydrate bootstrap re-sets it client
|
|
17
|
+
* side. Node-free — part of aurora's client runtime.
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* Install the `name → path-pattern` map `urlFor` resolves against (e.g.
|
|
21
|
+
* `{ 'users.show': '/users/:id' }`, from Ream's `router.namedManifest()`).
|
|
22
|
+
* Replaces any previous manifest. Routes are static per app, so this is set once
|
|
23
|
+
* per environment (server boot / page render, and the hydrate bootstrap).
|
|
24
|
+
*/
|
|
25
|
+
export declare function setRouteManifest(routes: Record<string, string>): void;
|
|
26
|
+
/** The currently-installed route manifest (mainly for tests/introspection). */
|
|
27
|
+
export declare function getRouteManifest(): Record<string, string>;
|
|
28
|
+
/**
|
|
29
|
+
* Build a URL for a named route — fills `:param` placeholders, drops unprovided
|
|
30
|
+
* optional (`:name?`) segments, appends `query` as a query string, and throws on
|
|
31
|
+
* an unknown route or a missing required param. Mirrors Ream's `router.urlFor`.
|
|
32
|
+
*/
|
|
33
|
+
export declare function urlFor(name: string, params?: Record<string, string | number>, query?: Record<string, string | number>): string;
|
package/dist/url.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `urlFor` — isomorphic named-route URL builder (AdonisJS v7 parity), the client
|
|
3
|
+
* half of Ream's `router.urlFor()`.
|
|
4
|
+
*
|
|
5
|
+
* Routes live server-side in Ream's router; this helper builds URLs from a
|
|
6
|
+
* serialized `name → path-pattern` manifest so the SAME `urlFor(name, params)`
|
|
7
|
+
* works in a page during SSR and after hydration in the browser — no more
|
|
8
|
+
* hard-coded paths like `redirect('/login')` / `href: '/team'`.
|
|
9
|
+
*
|
|
10
|
+
* urlFor('users.show', { id: 42 }) // → '/users/42'
|
|
11
|
+
* urlFor('auth.login') // → '/login'
|
|
12
|
+
* urlFor('search', {}, { q: 'ream', p: 2 })// → '/search?q=ream&p=2'
|
|
13
|
+
*
|
|
14
|
+
* The manifest is populated by {@link setRouteManifest}: `renderPage` calls it
|
|
15
|
+
* server-side from `options.routes` (build it with `router.namedManifest()`), and
|
|
16
|
+
* injects the same map into the page so the hydrate bootstrap re-sets it client
|
|
17
|
+
* side. Node-free — part of aurora's client runtime.
|
|
18
|
+
*/
|
|
19
|
+
let manifest = {};
|
|
20
|
+
/**
|
|
21
|
+
* Install the `name → path-pattern` map `urlFor` resolves against (e.g.
|
|
22
|
+
* `{ 'users.show': '/users/:id' }`, from Ream's `router.namedManifest()`).
|
|
23
|
+
* Replaces any previous manifest. Routes are static per app, so this is set once
|
|
24
|
+
* per environment (server boot / page render, and the hydrate bootstrap).
|
|
25
|
+
*/
|
|
26
|
+
export function setRouteManifest(routes) {
|
|
27
|
+
manifest = { ...routes };
|
|
28
|
+
}
|
|
29
|
+
/** The currently-installed route manifest (mainly for tests/introspection). */
|
|
30
|
+
export function getRouteManifest() {
|
|
31
|
+
return { ...manifest };
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Build a URL for a named route — fills `:param` placeholders, drops unprovided
|
|
35
|
+
* optional (`:name?`) segments, appends `query` as a query string, and throws on
|
|
36
|
+
* an unknown route or a missing required param. Mirrors Ream's `router.urlFor`.
|
|
37
|
+
*/
|
|
38
|
+
export function urlFor(name, params, query) {
|
|
39
|
+
const pattern = manifest[name];
|
|
40
|
+
if (pattern === undefined) {
|
|
41
|
+
const known = Object.keys(manifest);
|
|
42
|
+
throw new Error(`[aurora] urlFor: unknown route '${name}'. ${known.length > 0
|
|
43
|
+
? `Known: ${known.join(", ")}`
|
|
44
|
+
: "No routes registered — was the manifest passed to render() / setRouteManifest() called?"}`);
|
|
45
|
+
}
|
|
46
|
+
let url = pattern;
|
|
47
|
+
if (params) {
|
|
48
|
+
for (const [key, value] of Object.entries(params)) {
|
|
49
|
+
// Word-boundary substitution so `:id` doesn't corrupt `:idx`.
|
|
50
|
+
const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
51
|
+
url = url.replace(new RegExp(`:${escaped}\\??(?![\\w])`, "g"), encodeURIComponent(String(value)));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// Strip remaining optional placeholders (`:name?` not provided).
|
|
55
|
+
url = url.replace(/\/:[A-Za-z_][\w]*\?/g, "");
|
|
56
|
+
const missing = url.match(/:[A-Za-z_][\w]*/g);
|
|
57
|
+
if (missing && missing.length > 0) {
|
|
58
|
+
throw new Error(`[aurora] urlFor: route '${name}' is missing params ${missing.join(", ")}`);
|
|
59
|
+
}
|
|
60
|
+
if (query) {
|
|
61
|
+
const qs = Object.entries(query)
|
|
62
|
+
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`)
|
|
63
|
+
.join("&");
|
|
64
|
+
if (qs)
|
|
65
|
+
url += `${url.includes("?") ? "&" : "?"}${qs}`;
|
|
66
|
+
}
|
|
67
|
+
return url;
|
|
68
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@c9up/aurora",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.14",
|
|
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",
|