@c9up/aurora 0.1.33 → 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.
@@ -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
- void http.post(path, { id, event, payload });
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
  }
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
- let pendingClosingQuote = false;
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 = segment.replace(/^"/, "");
54
- pendingClosingQuote = false;
54
+ if (pendingClosingQuote !== undefined) {
55
+ segment =
56
+ pendingClosingQuote === '"'
57
+ ? segment.replace(/^"/, "")
58
+ : segment.replace(/^'/, "");
59
+ pendingClosingQuote = undefined;
55
60
  }
56
- const directiveMatch = segment.match(/\s([@?.][\w-]+)="$/);
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
- pendingClosingQuote = true;
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
- // In attribute position: directive handlers (`@click`, `?disabled`,
151
- // `.prop`) have already been stripped by `stripDirectiveBefore`.
152
- // A function reaching this point is a reactive-expression text
153
- // slot (`${() => ...}`), which we evaluate eagerly server-side.
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.33",
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.9",
60
- "@vitest/browser-playwright": "4.1.9",
61
- "happy-dom": "^15.11.7",
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
- void http.post(path, { id, event, payload });
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/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
- let pendingClosingQuote = false;
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 = segment.replace(/^"/, "");
58
- pendingClosingQuote = false;
58
+ if (pendingClosingQuote !== undefined) {
59
+ segment =
60
+ pendingClosingQuote === '"'
61
+ ? segment.replace(/^"/, "")
62
+ : segment.replace(/^'/, "");
63
+ pendingClosingQuote = undefined;
59
64
  }
60
- const directiveMatch = segment.match(/\s([@?.][\w-]+)="$/);
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
- pendingClosingQuote = true;
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
- // In attribute position: directive handlers (`@click`, `?disabled`,
152
- // `.prop`) have already been stripped by `stripDirectiveBefore`.
153
- // A function reaching this point is a reactive-expression text
154
- // slot (`${() => ...}`), which we evaluate eagerly server-side.
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 {