@c9up/aurora 0.1.12 → 0.1.13
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/command.js +26 -10
- package/dist/hydrate.js +56 -2
- package/dist/render.js +22 -2
- package/dist/ssr.js +13 -0
- package/package.json +1 -1
- package/src/command.ts +24 -6
- package/src/hydrate.ts +58 -2
- package/src/render.ts +24 -2
- package/src/ssr.ts +12 -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,12 +231,49 @@ 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
|
}
|
|
@@ -332,6 +369,23 @@ function hydrateTextSlot(commentMarker, value, cleanups, mountHooks, markerCurso
|
|
|
332
369
|
return;
|
|
333
370
|
}
|
|
334
371
|
if (isTemplateResult(value)) {
|
|
372
|
+
// DIRECT nested template (component composition, `${Layout({…})}`). SSR
|
|
373
|
+
// wrapped it in a boundary-marker pair (same scheme as a reactive
|
|
374
|
+
// structured slot). Consume the pair in document order and hydrate the
|
|
375
|
+
// nested template against its captured range — wiring inner bindings to
|
|
376
|
+
// the SSR nodes and keeping the marker cursor aligned.
|
|
377
|
+
const pair = markerCursor.pairs[markerCursor.i];
|
|
378
|
+
if (pair !== undefined) {
|
|
379
|
+
markerCursor.i += 1;
|
|
380
|
+
const range = [];
|
|
381
|
+
for (let n = pair.start.nextSibling; n !== null && n !== pair.end; n = n.nextSibling) {
|
|
382
|
+
range.push(n);
|
|
383
|
+
}
|
|
384
|
+
hydrateTemplateResult(value, range, cleanups, mountHooks, markerCursor);
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
// Legacy markup without markers (older SSR build): best-effort against
|
|
388
|
+
// the single resolved node.
|
|
335
389
|
hydrateTemplateResult(value, [commentMarker], cleanups, mountHooks, markerCursor);
|
|
336
390
|
return;
|
|
337
391
|
}
|
package/dist/render.js
CHANGED
|
@@ -71,6 +71,15 @@ 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
|
+
}
|
|
74
83
|
if (slot.kind === "attr" && slot.staticParts !== undefined) {
|
|
75
84
|
collectMultiAttr(slot, node, result.values[i], multiGroups);
|
|
76
85
|
}
|
|
@@ -88,8 +97,19 @@ export function mount(result, cleanups, mounted, mountHooks) {
|
|
|
88
97
|
}
|
|
89
98
|
function resolvePath(root, path) {
|
|
90
99
|
let node = root;
|
|
91
|
-
for (const i of path)
|
|
92
|
-
|
|
100
|
+
for (const i of path) {
|
|
101
|
+
const next = node.childNodes[i];
|
|
102
|
+
// Fail-soft: a path step that runs off the live child list means the
|
|
103
|
+
// tree diverged from the parsed template (a hydration desync). Return
|
|
104
|
+
// null so the caller skips the binding instead of dereferencing
|
|
105
|
+
// `undefined.childNodes` and crashing the whole render — which, when the
|
|
106
|
+
// render runs inside a command's onSuccess, used to masquerade as a
|
|
107
|
+
// task failure (and on a guarded page, a logout). Mirrors
|
|
108
|
+
// `resolvePathLive` in hydrate.ts.
|
|
109
|
+
if (next === undefined)
|
|
110
|
+
return null;
|
|
111
|
+
node = next;
|
|
112
|
+
}
|
|
93
113
|
return node;
|
|
94
114
|
}
|
|
95
115
|
function applySlot(slot, node, value, cleanups, mounted, mountHooks) {
|
package/dist/ssr.js
CHANGED
|
@@ -75,6 +75,19 @@ function stringifyTemplateResult(result) {
|
|
|
75
75
|
out += stringifyValue(value, false);
|
|
76
76
|
out += `<!--${SLOT_END}-->`;
|
|
77
77
|
}
|
|
78
|
+
else if (!inAttr && isTemplateResult(value)) {
|
|
79
|
+
// DIRECT (non-reactive) nested template — component composition,
|
|
80
|
+
// e.g. `${Layout({ children })}` or `${table}`. It renders to a
|
|
81
|
+
// node RANGE just like a reactive structured slot, so it needs the
|
|
82
|
+
// SAME boundary markers: the client template counts every slot as
|
|
83
|
+
// ONE comment node, so without a markable range a multi-node child
|
|
84
|
+
// shifts the childNode indices of every FOLLOWING sibling slot →
|
|
85
|
+
// dead bindings / "slot path not found". Hydration collapses the
|
|
86
|
+
// marked range back to one node so sibling paths stay aligned.
|
|
87
|
+
out += `<!--${SLOT_START}-->`;
|
|
88
|
+
out += stringifyValue(value, false);
|
|
89
|
+
out += `<!--${SLOT_END}-->`;
|
|
90
|
+
}
|
|
78
91
|
else if (inAttr) {
|
|
79
92
|
out += stringifyValue(value, true);
|
|
80
93
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@c9up/aurora",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.13",
|
|
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",
|
package/src/command.ts
CHANGED
|
@@ -91,14 +91,32 @@ class CommandRunner<TArgs extends unknown[], TData>
|
|
|
91
91
|
this.#loading(true);
|
|
92
92
|
this.#error(null);
|
|
93
93
|
try {
|
|
94
|
-
|
|
94
|
+
let result: TData;
|
|
95
|
+
try {
|
|
96
|
+
result = await this.#task(...args);
|
|
97
|
+
} catch (error) {
|
|
98
|
+
if (id !== this.#runId) return; // superseded — drop
|
|
99
|
+
this.#error(error);
|
|
100
|
+
for (const handler of this.#onFail) handler(error);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
95
103
|
if (id !== this.#runId) return; // superseded by a newer run — drop
|
|
96
104
|
this.#data(result);
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
105
|
+
// onSuccess runs OUTSIDE the task's failure boundary: success is
|
|
106
|
+
// decided by the task, never by the callback. A throw here (e.g. a
|
|
107
|
+
// render error after the data lands) is a handler bug — surfacing it
|
|
108
|
+
// as a task failure would route to onFail, which on a guarded page
|
|
109
|
+
// masquerades as a logout. Report it, but never reclassify it.
|
|
110
|
+
try {
|
|
111
|
+
for (const handler of this.#onSuccess) handler(result);
|
|
112
|
+
} catch (handlerError) {
|
|
113
|
+
if (typeof console !== "undefined") {
|
|
114
|
+
console.error(
|
|
115
|
+
"[aurora] a command onSuccess handler threw — not treated as a task failure:",
|
|
116
|
+
handlerError,
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
102
120
|
} finally {
|
|
103
121
|
if (id === this.#runId) {
|
|
104
122
|
this.#loading(false);
|
package/src/hydrate.ts
CHANGED
|
@@ -320,15 +320,50 @@ function hydrateTemplateResult(
|
|
|
320
320
|
* Text-slot paths point to a comment marker that doesn't exist in
|
|
321
321
|
* hydration markup — we tolerate the miss and return null.
|
|
322
322
|
*/
|
|
323
|
+
/**
|
|
324
|
+
* Collapse each top-level `<!--$-->…<!--/$-->` range in `nodes` to a SINGLE
|
|
325
|
+
* entry (its start marker), dropping the in-range content + end marker from the
|
|
326
|
+
* count. SSR expands a structured slot (reactive OR a direct nested template)
|
|
327
|
+
* to a node RANGE, but the parsed client template counts every slot as exactly
|
|
328
|
+
* ONE comment node — so without this collapse the extra range nodes shift the
|
|
329
|
+
* childNode index of every FOLLOWING sibling slot (dead bindings / "slot path
|
|
330
|
+
* not found"). Nested ranges (depth > 0) are skipped wholesale: they belong to
|
|
331
|
+
* the outer slot's content and are hydrated when we recurse into it.
|
|
332
|
+
*/
|
|
333
|
+
function collapseMarkerRanges(nodes: ChildNode[]): ChildNode[] {
|
|
334
|
+
const out: ChildNode[] = [];
|
|
335
|
+
let depth = 0;
|
|
336
|
+
for (const n of nodes) {
|
|
337
|
+
if (n.nodeType === 8 /* Comment */) {
|
|
338
|
+
const data = (n as Comment).data;
|
|
339
|
+
if (data === SLOT_START) {
|
|
340
|
+
if (depth === 0) out.push(n); // the whole range counts as one node
|
|
341
|
+
depth += 1;
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
if (data === SLOT_END) {
|
|
345
|
+
if (depth > 0) depth -= 1;
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
if (depth === 0) out.push(n);
|
|
350
|
+
}
|
|
351
|
+
return out;
|
|
352
|
+
}
|
|
353
|
+
|
|
323
354
|
function resolvePathLive(
|
|
324
355
|
_root: ParentNode,
|
|
325
356
|
path: NodePath,
|
|
326
357
|
rootNodes: ChildNode[],
|
|
327
358
|
): Node | null {
|
|
328
359
|
if (path.length === 0) return null;
|
|
329
|
-
|
|
360
|
+
// Collapse marker ranges at EVERY level so the live child list matches the
|
|
361
|
+
// parsed template's one-node-per-slot shape (see collapseMarkerRanges).
|
|
362
|
+
let children = collapseMarkerRanges(rootNodes);
|
|
363
|
+
let node: Node | null = children[path[0]] ?? null;
|
|
330
364
|
for (let i = 1; node && i < path.length; i++) {
|
|
331
|
-
|
|
365
|
+
children = collapseMarkerRanges(Array.from(node.childNodes));
|
|
366
|
+
node = children[path[i]] ?? null;
|
|
332
367
|
}
|
|
333
368
|
return node;
|
|
334
369
|
}
|
|
@@ -451,6 +486,27 @@ function hydrateTextSlot(
|
|
|
451
486
|
return;
|
|
452
487
|
}
|
|
453
488
|
if (isTemplateResult(value)) {
|
|
489
|
+
// DIRECT nested template (component composition, `${Layout({…})}`). SSR
|
|
490
|
+
// wrapped it in a boundary-marker pair (same scheme as a reactive
|
|
491
|
+
// structured slot). Consume the pair in document order and hydrate the
|
|
492
|
+
// nested template against its captured range — wiring inner bindings to
|
|
493
|
+
// the SSR nodes and keeping the marker cursor aligned.
|
|
494
|
+
const pair = markerCursor.pairs[markerCursor.i];
|
|
495
|
+
if (pair !== undefined) {
|
|
496
|
+
markerCursor.i += 1;
|
|
497
|
+
const range: ChildNode[] = [];
|
|
498
|
+
for (
|
|
499
|
+
let n = pair.start.nextSibling;
|
|
500
|
+
n !== null && n !== pair.end;
|
|
501
|
+
n = n.nextSibling
|
|
502
|
+
) {
|
|
503
|
+
range.push(n as ChildNode);
|
|
504
|
+
}
|
|
505
|
+
hydrateTemplateResult(value, range, cleanups, mountHooks, markerCursor);
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
// Legacy markup without markers (older SSR build): best-effort against
|
|
509
|
+
// the single resolved node.
|
|
454
510
|
hydrateTemplateResult(
|
|
455
511
|
value,
|
|
456
512
|
[commentMarker as ChildNode],
|
package/src/render.ts
CHANGED
|
@@ -107,6 +107,17 @@ export function mount(
|
|
|
107
107
|
for (let i = 0; i < tpl.slots.length; i++) {
|
|
108
108
|
const slot = tpl.slots[i];
|
|
109
109
|
const node = resolvePath(fragment, slot.path);
|
|
110
|
+
if (node === null) {
|
|
111
|
+
// Path didn't resolve — skip this binding rather than crash (see
|
|
112
|
+
// resolvePath). Degrades to a dead binding; the surrounding render
|
|
113
|
+
// (and any command driving it) survives.
|
|
114
|
+
if (typeof console !== "undefined") {
|
|
115
|
+
console.warn(
|
|
116
|
+
`[aurora] render: slot ${i} (${slot.kind}) path ${slot.path.join(".")} did not resolve — skipping binding`,
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
110
121
|
if (slot.kind === "attr" && slot.staticParts !== undefined) {
|
|
111
122
|
collectMultiAttr(slot, node as Element, result.values[i], multiGroups);
|
|
112
123
|
} else {
|
|
@@ -124,9 +135,20 @@ export function mount(
|
|
|
124
135
|
return fragment;
|
|
125
136
|
}
|
|
126
137
|
|
|
127
|
-
function resolvePath(root: ParentNode, path: NodePath): Node {
|
|
138
|
+
function resolvePath(root: ParentNode, path: NodePath): Node | null {
|
|
128
139
|
let node: Node = root;
|
|
129
|
-
for (const i of path)
|
|
140
|
+
for (const i of path) {
|
|
141
|
+
const next = node.childNodes[i];
|
|
142
|
+
// Fail-soft: a path step that runs off the live child list means the
|
|
143
|
+
// tree diverged from the parsed template (a hydration desync). Return
|
|
144
|
+
// null so the caller skips the binding instead of dereferencing
|
|
145
|
+
// `undefined.childNodes` and crashing the whole render — which, when the
|
|
146
|
+
// render runs inside a command's onSuccess, used to masquerade as a
|
|
147
|
+
// task failure (and on a guarded page, a logout). Mirrors
|
|
148
|
+
// `resolvePathLive` in hydrate.ts.
|
|
149
|
+
if (next === undefined) return null;
|
|
150
|
+
node = next;
|
|
151
|
+
}
|
|
130
152
|
return node;
|
|
131
153
|
}
|
|
132
154
|
|
package/src/ssr.ts
CHANGED
|
@@ -78,6 +78,18 @@ function stringifyTemplateResult(result: TemplateResult): string {
|
|
|
78
78
|
out += `<!--${SLOT_START}-->`;
|
|
79
79
|
out += stringifyValue(value, false);
|
|
80
80
|
out += `<!--${SLOT_END}-->`;
|
|
81
|
+
} else if (!inAttr && isTemplateResult(value)) {
|
|
82
|
+
// DIRECT (non-reactive) nested template — component composition,
|
|
83
|
+
// e.g. `${Layout({ children })}` or `${table}`. It renders to a
|
|
84
|
+
// node RANGE just like a reactive structured slot, so it needs the
|
|
85
|
+
// SAME boundary markers: the client template counts every slot as
|
|
86
|
+
// ONE comment node, so without a markable range a multi-node child
|
|
87
|
+
// shifts the childNode indices of every FOLLOWING sibling slot →
|
|
88
|
+
// dead bindings / "slot path not found". Hydration collapses the
|
|
89
|
+
// marked range back to one node so sibling paths stay aligned.
|
|
90
|
+
out += `<!--${SLOT_START}-->`;
|
|
91
|
+
out += stringifyValue(value, false);
|
|
92
|
+
out += `<!--${SLOT_END}-->`;
|
|
81
93
|
} else if (inAttr) {
|
|
82
94
|
out += stringifyValue(value, true);
|
|
83
95
|
} else {
|