@c9up/aurora 0.1.32 → 0.1.34
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/liveClient.js +8 -1
- package/dist/server/renderPage.js +10 -6
- package/dist/ssr.js +25 -10
- package/package.json +4 -4
- package/src/liveClient.ts +13 -1
- package/src/server/renderPage.ts +27 -7
- package/src/ssr.ts +25 -10
package/dist/liveClient.js
CHANGED
|
@@ -76,7 +76,14 @@ export function buildLiveTransport(relayClient, http, options = {}) {
|
|
|
76
76
|
return {
|
|
77
77
|
subscribe: (channel, handler) => relayClient.subscribe(channel, handler),
|
|
78
78
|
post: (id, event, payload) => {
|
|
79
|
-
|
|
79
|
+
// A post that fails means the server never saw this interaction, so
|
|
80
|
+
// the component's server-side state and what the user is looking at
|
|
81
|
+
// have diverged — silence is the worst possible answer. Unhandled,
|
|
82
|
+
// it was also a bare `Uncaught (in promise)` with no clue which
|
|
83
|
+
// event was lost.
|
|
84
|
+
void (async () => http.post(path, { id, event, payload }))().catch((err) => {
|
|
85
|
+
console.warn(`[aurora/live] '${event}' on ${id} did not reach the server; this component is now out of sync:`, err);
|
|
86
|
+
});
|
|
80
87
|
},
|
|
81
88
|
};
|
|
82
89
|
}
|
|
@@ -30,19 +30,23 @@ const renderScope = new AsyncLocalStorage();
|
|
|
30
30
|
setCookieStoreReader(() => renderScope.getStore()?.cookies);
|
|
31
31
|
setRouteManifestReader(() => renderScope.getStore()?.routes);
|
|
32
32
|
function isCookieReadable(request) {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
typeof request
|
|
33
|
+
if (typeof request !== "object" || request === null)
|
|
34
|
+
return false;
|
|
35
|
+
return (typeof Reflect.get(request, "plainCookie") === "function" ||
|
|
36
|
+
typeof Reflect.get(request, "cookie") === "function");
|
|
37
37
|
}
|
|
38
38
|
/** Read the allowlisted cookies off the request into a `name → value` seed. */
|
|
39
39
|
function readRequestCookies(request, names) {
|
|
40
40
|
if (!isCookieReadable(request))
|
|
41
41
|
return {};
|
|
42
42
|
const seed = {};
|
|
43
|
+
// Unsigned first; `cookie()` only when the host offers nothing else.
|
|
44
|
+
const read = request.plainCookie
|
|
45
|
+
? (name) => request.plainCookie?.(name, undefined, { encoded: false })
|
|
46
|
+
: (name) => request.cookie?.(name);
|
|
43
47
|
for (const name of names) {
|
|
44
|
-
const value =
|
|
45
|
-
if (value !== null)
|
|
48
|
+
const value = read(name);
|
|
49
|
+
if (value !== null && value !== undefined)
|
|
46
50
|
seed[name] = value;
|
|
47
51
|
}
|
|
48
52
|
return seed;
|
package/dist/ssr.js
CHANGED
|
@@ -45,19 +45,34 @@ function stringifyTemplateResult(result) {
|
|
|
45
45
|
// the matching value, and consume the closing `"` from the next
|
|
46
46
|
// segment. This three-step coordination is why the loop holds a
|
|
47
47
|
// `pendingClosingQuote` flag.
|
|
48
|
-
|
|
48
|
+
// Which quote closes the directive currently being skipped, so the closing
|
|
49
|
+
// one consumed is the one that was opened. Undefined when none is pending.
|
|
50
|
+
let pendingClosingQuote;
|
|
49
51
|
const scanner = new TagScanner();
|
|
50
52
|
for (let i = 0; i < strings.length; i++) {
|
|
51
53
|
let segment = strings[i];
|
|
52
|
-
if (pendingClosingQuote) {
|
|
53
|
-
segment =
|
|
54
|
-
|
|
54
|
+
if (pendingClosingQuote !== undefined) {
|
|
55
|
+
segment =
|
|
56
|
+
pendingClosingQuote === '"'
|
|
57
|
+
? segment.replace(/^"/, "")
|
|
58
|
+
: segment.replace(/^'/, "");
|
|
59
|
+
pendingClosingQuote = undefined;
|
|
55
60
|
}
|
|
56
|
-
|
|
61
|
+
// Both quote styles. Matching only `="` left `@click='${handler}'`
|
|
62
|
+
// unrecognised, so the handler fell through to `stringifyValue`, which
|
|
63
|
+
// CALLED it — a client event handler running on the server, its return
|
|
64
|
+
// value written into the HTML, and any exception swallowed.
|
|
65
|
+
const directiveMatch = segment.match(/\s([@?.][\w-]+)=("|'|)$/);
|
|
57
66
|
const skipValue = directiveMatch !== null;
|
|
58
67
|
if (directiveMatch) {
|
|
59
68
|
segment = segment.slice(0, segment.length - directiveMatch[0].length);
|
|
60
|
-
|
|
69
|
+
// Only a quoted directive leaves a closing quote to swallow.
|
|
70
|
+
pendingClosingQuote =
|
|
71
|
+
directiveMatch[2] === '"'
|
|
72
|
+
? '"'
|
|
73
|
+
: directiveMatch[2] === "'"
|
|
74
|
+
? "'"
|
|
75
|
+
: undefined;
|
|
61
76
|
}
|
|
62
77
|
out += segment;
|
|
63
78
|
scanner.consume(segment);
|
|
@@ -147,10 +162,10 @@ function stringifyValue(value, inAttribute) {
|
|
|
147
162
|
if (isSignal(value))
|
|
148
163
|
return stringifyValue(value(), inAttribute);
|
|
149
164
|
if (typeof value === "function") {
|
|
150
|
-
//
|
|
151
|
-
//
|
|
152
|
-
//
|
|
153
|
-
//
|
|
165
|
+
// A function here is a reactive expression — `class="${() => …}"` in an
|
|
166
|
+
// attribute, `${() => …}` in text — and is evaluated eagerly
|
|
167
|
+
// server-side. Directive values (`@click`, `?disabled`, `.prop`) never
|
|
168
|
+
// reach this point: the scanner skips them, whatever quoting they use.
|
|
154
169
|
try {
|
|
155
170
|
return stringifyValue(value(), inAttribute);
|
|
156
171
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@c9up/aurora",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.34",
|
|
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",
|
|
@@ -56,9 +56,9 @@
|
|
|
56
56
|
"@biomejs/biome": "^2.4.10",
|
|
57
57
|
"@c9up/comet": "^0.1.0",
|
|
58
58
|
"@types/node": "^22.19.15",
|
|
59
|
-
"@vitest/browser": "4.1.
|
|
60
|
-
"@vitest/browser-playwright": "4.1.
|
|
61
|
-
"happy-dom": "^
|
|
59
|
+
"@vitest/browser": "4.1.11",
|
|
60
|
+
"@vitest/browser-playwright": "4.1.11",
|
|
61
|
+
"happy-dom": "^20.12.0",
|
|
62
62
|
"playwright": "^1.61.1",
|
|
63
63
|
"typescript": "^6.0.2",
|
|
64
64
|
"vitest": "4.1.9"
|
package/src/liveClient.ts
CHANGED
|
@@ -118,7 +118,19 @@ export function buildLiveTransport(
|
|
|
118
118
|
subscribe: (channel, handler) =>
|
|
119
119
|
relayClient.subscribe<SlotPatch[]>(channel, handler),
|
|
120
120
|
post: (id, event, payload) => {
|
|
121
|
-
|
|
121
|
+
// A post that fails means the server never saw this interaction, so
|
|
122
|
+
// the component's server-side state and what the user is looking at
|
|
123
|
+
// have diverged — silence is the worst possible answer. Unhandled,
|
|
124
|
+
// it was also a bare `Uncaught (in promise)` with no clue which
|
|
125
|
+
// event was lost.
|
|
126
|
+
void (async () => http.post(path, { id, event, payload }))().catch(
|
|
127
|
+
(err: unknown) => {
|
|
128
|
+
console.warn(
|
|
129
|
+
`[aurora/live] '${event}' on ${id} did not reach the server; this component is now out of sync:`,
|
|
130
|
+
err,
|
|
131
|
+
);
|
|
132
|
+
},
|
|
133
|
+
);
|
|
122
134
|
},
|
|
123
135
|
};
|
|
124
136
|
}
|
package/src/server/renderPage.ts
CHANGED
|
@@ -142,8 +142,24 @@ export type SharedPropsResolver = (
|
|
|
142
142
|
) => SharedProps | Promise<SharedProps>;
|
|
143
143
|
|
|
144
144
|
/** A request that can read a cookie by name — the structural slice we need. */
|
|
145
|
+
/**
|
|
146
|
+
* The slice of a host request this needs: reading a cookie the browser wrote.
|
|
147
|
+
*
|
|
148
|
+
* `plainCookie` is the one to use. These cookies are written client-side
|
|
149
|
+
* through `document.cookie`, so they carry no signature; a host whose
|
|
150
|
+
* `cookie()` verifies one (as Ream's does) returns nothing for them, and the
|
|
151
|
+
* seed silently empties — which is the flash this exists to prevent.
|
|
152
|
+
*
|
|
153
|
+
* `cookie` is accepted as a fallback so a host that only offers that spelling
|
|
154
|
+
* still seeds, rather than rendering default state and saying nothing.
|
|
155
|
+
*/
|
|
145
156
|
interface CookieReadableRequest {
|
|
146
|
-
|
|
157
|
+
plainCookie?<T = string>(
|
|
158
|
+
name: string,
|
|
159
|
+
defaultValue?: T,
|
|
160
|
+
options?: { encoded?: boolean },
|
|
161
|
+
): T | null;
|
|
162
|
+
cookie?(name: string): string | null;
|
|
147
163
|
}
|
|
148
164
|
|
|
149
165
|
interface RenderScope {
|
|
@@ -157,11 +173,10 @@ setCookieStoreReader(() => renderScope.getStore()?.cookies);
|
|
|
157
173
|
setRouteManifestReader(() => renderScope.getStore()?.routes);
|
|
158
174
|
|
|
159
175
|
function isCookieReadable(request: unknown): request is CookieReadableRequest {
|
|
176
|
+
if (typeof request !== "object" || request === null) return false;
|
|
160
177
|
return (
|
|
161
|
-
typeof request === "
|
|
162
|
-
request
|
|
163
|
-
"cookie" in request &&
|
|
164
|
-
typeof request.cookie === "function"
|
|
178
|
+
typeof Reflect.get(request, "plainCookie") === "function" ||
|
|
179
|
+
typeof Reflect.get(request, "cookie") === "function"
|
|
165
180
|
);
|
|
166
181
|
}
|
|
167
182
|
|
|
@@ -172,9 +187,14 @@ function readRequestCookies(
|
|
|
172
187
|
): Record<string, string> {
|
|
173
188
|
if (!isCookieReadable(request)) return {};
|
|
174
189
|
const seed: Record<string, string> = {};
|
|
190
|
+
// Unsigned first; `cookie()` only when the host offers nothing else.
|
|
191
|
+
const read = request.plainCookie
|
|
192
|
+
? (name: string) =>
|
|
193
|
+
request.plainCookie?.<string>(name, undefined, { encoded: false })
|
|
194
|
+
: (name: string) => request.cookie?.(name);
|
|
175
195
|
for (const name of names) {
|
|
176
|
-
const value =
|
|
177
|
-
if (value !== null) seed[name] = value;
|
|
196
|
+
const value = read(name);
|
|
197
|
+
if (value !== null && value !== undefined) seed[name] = value;
|
|
178
198
|
}
|
|
179
199
|
return seed;
|
|
180
200
|
}
|
package/src/ssr.ts
CHANGED
|
@@ -49,19 +49,34 @@ function stringifyTemplateResult(result: TemplateResult): string {
|
|
|
49
49
|
// the matching value, and consume the closing `"` from the next
|
|
50
50
|
// segment. This three-step coordination is why the loop holds a
|
|
51
51
|
// `pendingClosingQuote` flag.
|
|
52
|
-
|
|
52
|
+
// Which quote closes the directive currently being skipped, so the closing
|
|
53
|
+
// one consumed is the one that was opened. Undefined when none is pending.
|
|
54
|
+
let pendingClosingQuote: '"' | "'" | undefined;
|
|
53
55
|
const scanner = new TagScanner();
|
|
54
56
|
for (let i = 0; i < strings.length; i++) {
|
|
55
57
|
let segment = strings[i];
|
|
56
|
-
if (pendingClosingQuote) {
|
|
57
|
-
segment =
|
|
58
|
-
|
|
58
|
+
if (pendingClosingQuote !== undefined) {
|
|
59
|
+
segment =
|
|
60
|
+
pendingClosingQuote === '"'
|
|
61
|
+
? segment.replace(/^"/, "")
|
|
62
|
+
: segment.replace(/^'/, "");
|
|
63
|
+
pendingClosingQuote = undefined;
|
|
59
64
|
}
|
|
60
|
-
|
|
65
|
+
// Both quote styles. Matching only `="` left `@click='${handler}'`
|
|
66
|
+
// unrecognised, so the handler fell through to `stringifyValue`, which
|
|
67
|
+
// CALLED it — a client event handler running on the server, its return
|
|
68
|
+
// value written into the HTML, and any exception swallowed.
|
|
69
|
+
const directiveMatch = segment.match(/\s([@?.][\w-]+)=("|'|)$/);
|
|
61
70
|
const skipValue = directiveMatch !== null;
|
|
62
71
|
if (directiveMatch) {
|
|
63
72
|
segment = segment.slice(0, segment.length - directiveMatch[0].length);
|
|
64
|
-
|
|
73
|
+
// Only a quoted directive leaves a closing quote to swallow.
|
|
74
|
+
pendingClosingQuote =
|
|
75
|
+
directiveMatch[2] === '"'
|
|
76
|
+
? '"'
|
|
77
|
+
: directiveMatch[2] === "'"
|
|
78
|
+
? "'"
|
|
79
|
+
: undefined;
|
|
65
80
|
}
|
|
66
81
|
out += segment;
|
|
67
82
|
scanner.consume(segment);
|
|
@@ -148,10 +163,10 @@ function stringifyValue(value: unknown, inAttribute: boolean): string {
|
|
|
148
163
|
if (value === true) return inAttribute ? "" : "true";
|
|
149
164
|
if (isSignal(value)) return stringifyValue(value(), inAttribute);
|
|
150
165
|
if (typeof value === "function") {
|
|
151
|
-
//
|
|
152
|
-
//
|
|
153
|
-
//
|
|
154
|
-
//
|
|
166
|
+
// A function here is a reactive expression — `class="${() => …}"` in an
|
|
167
|
+
// attribute, `${() => …}` in text — and is evaluated eagerly
|
|
168
|
+
// server-side. Directive values (`@click`, `?disabled`, `.prop`) never
|
|
169
|
+
// reach this point: the scanner skips them, whatever quoting they use.
|
|
155
170
|
try {
|
|
156
171
|
return stringifyValue((value as () => unknown)(), inAttribute);
|
|
157
172
|
} catch {
|