@c9up/aurora 0.1.27 → 0.1.29

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/browser.js CHANGED
@@ -1,3 +1,8 @@
1
+ /// <reference lib="dom" />
2
+ // This file uses browser globals. The reference pulls the DOM lib in for
3
+ // THIS file whatever `lib` the consumer configured, so a Node app
4
+ // typechecking against our sources does not trip over `window` —
5
+ // `types: "./src/index.ts"` means every consumer reads them.
1
6
  /**
2
7
  * Browser DX helpers — navigation + a typed `localStorage` wrapper.
3
8
  *
package/dist/html.js CHANGED
@@ -1,3 +1,8 @@
1
+ /// <reference lib="dom" />
2
+ // This file uses browser globals. The reference pulls the DOM lib in for
3
+ // THIS file whatever `lib` the consumer configured, so a Node app
4
+ // typechecking against our sources does not trip over `window` —
5
+ // `types: "./src/index.ts"` means every consumer reads them.
1
6
  /**
2
7
  * Tagged-template HTML parser.
3
8
  *
package/dist/http.js CHANGED
@@ -1,3 +1,8 @@
1
+ /// <reference lib="dom" />
2
+ // This file uses browser globals. The reference pulls the DOM lib in for
3
+ // THIS file whatever `lib` the consumer configured, so a Node app
4
+ // typechecking against our sources does not trip over `window` —
5
+ // `types: "./src/index.ts"` means every consumer reads them.
1
6
  /**
2
7
  * `HttpClient` — a small typed wrapper over `fetch` so call sites read
3
8
  * `await http.get<User>("/auth/me")` instead of hand-rolling headers,
package/dist/hydrate.js CHANGED
@@ -1,3 +1,8 @@
1
+ /// <reference lib="dom" />
2
+ // This file uses browser globals. The reference pulls the DOM lib in for
3
+ // THIS file whatever `lib` the consumer configured, so a Node app
4
+ // typechecking against our sources does not trip over `window` —
5
+ // `types: "./src/index.ts"` means every consumer reads them.
1
6
  /**
2
7
  * Hydration — adopt SSR-rendered HTML in the browser without rebuilding
3
8
  * the DOM.
@@ -1,3 +1,8 @@
1
+ /// <reference lib="dom" />
2
+ // This file uses browser globals. The reference pulls the DOM lib in for
3
+ // THIS file whatever `lib` the consumer configured, so a Node app
4
+ // typechecking against our sources does not trip over `window` —
5
+ // `types: "./src/index.ts"` means every consumer reads them.
1
6
  /**
2
7
  * Live client runtime (Stage 5) — the thin browser side of live components.
3
8
  *
@@ -12,7 +12,12 @@
12
12
  import type { LiveRouter } from "./liveRouter.js";
13
13
  /** The slice of the host HTTP router this needs. */
14
14
  export interface LiveHttpRouter {
15
- post(path: string, handler: (ctx: LiveHttpContext) => unknown): unknown;
15
+ /**
16
+ * The handler returns `void | Promise<void>`, not `unknown`: that is what an
17
+ * HTTP handler returns in ream and in AdonisJS, and the wider shape made
18
+ * ream's own Router fail to satisfy this interface.
19
+ */
20
+ post(path: string, handler: (ctx: LiveHttpContext) => void | Promise<void>): unknown;
16
21
  }
17
22
  /** The slice of the host HTTP context this needs (Ream's HttpContext satisfies it). */
18
23
  export interface LiveHttpContext {
package/dist/render.js CHANGED
@@ -1,3 +1,8 @@
1
+ /// <reference lib="dom" />
2
+ // This file uses browser globals. The reference pulls the DOM lib in for
3
+ // THIS file whatever `lib` the consumer configured, so a Node app
4
+ // typechecking against our sources does not trip over `window` —
5
+ // `types: "./src/index.ts"` means every consumer reads them.
1
6
  /**
2
7
  * Render a `TemplateResult` to the DOM and keep it reactive.
3
8
  *
package/dist/rpc.js CHANGED
@@ -1,3 +1,8 @@
1
+ /// <reference lib="dom" />
2
+ // This file uses browser globals. The reference pulls the DOM lib in for
3
+ // THIS file whatever `lib` the consumer configured, so a Node app
4
+ // typechecking against our sources does not trip over `window` —
5
+ // `types: "./src/index.ts"` means every consumer reads them.
1
6
  /**
2
7
  * Browser JSON-RPC 2.0 client for Ream's RPC endpoint — aurora's thin binding
3
8
  * over the agnostic {@link https://github.com/C9up/comet | @c9up/comet} client.
@@ -140,13 +140,29 @@ function escapeAttr(value) {
140
140
  }
141
141
  /**
142
142
  * Escape a JSON payload for safe embedding inside a `<script>` block.
143
- * The HTML parser closes the script on `</script>` regardless of JSON
144
- * quoting, so we slash-escape the `/`. We also escape `<!--` and `-->`
145
- * to dodge HTML-comment interpretation inside the script body.
143
+ *
144
+ * The HTML parser ends the script on `</script>` and reinterprets `<!--` /
145
+ * `-->` as comment markers, whatever the JSON quoting says — so those
146
+ * characters must not survive literally. They are escaped as \uXXXX, which is
147
+ * valid JSON: a backslash escape like `\!` or `\>` is NOT, and made
148
+ * `JSON.parse` throw on the client the moment a prop contained a comment
149
+ * marker, taking the whole page's hydration with it.
146
150
  */
147
151
  function escapeJsonForScript(value) {
148
- return JSON.stringify(value)
149
- .replace(/<\/(script)/gi, "<\\/$1")
150
- .replace(/<!--/g, "<\\!--")
151
- .replace(/-->/g, "--\\>");
152
+ return JSON.stringify(value).replace(/[<>&\u2028\u2029]/g, (c) => {
153
+ switch (c) {
154
+ case "<":
155
+ return "\\u003c";
156
+ case ">":
157
+ return "\\u003e";
158
+ case "&":
159
+ return "\\u0026";
160
+ // Line separators are valid in JSON strings but terminate a JS line,
161
+ // so a script block carrying them raw is a syntax error.
162
+ case "\u2028":
163
+ return "\\u2028";
164
+ default:
165
+ return "\\u2029";
166
+ }
167
+ });
152
168
  }
package/dist/ssr.js CHANGED
@@ -46,6 +46,7 @@ function stringifyTemplateResult(result) {
46
46
  // segment. This three-step coordination is why the loop holds a
47
47
  // `pendingClosingQuote` flag.
48
48
  let pendingClosingQuote = false;
49
+ const scanner = new TagScanner();
49
50
  for (let i = 0; i < strings.length; i++) {
50
51
  let segment = strings[i];
51
52
  if (pendingClosingQuote) {
@@ -59,11 +60,14 @@ function stringifyTemplateResult(result) {
59
60
  pendingClosingQuote = true;
60
61
  }
61
62
  out += segment;
63
+ scanner.consume(segment);
62
64
  if (i < values.length && !skipValue) {
63
65
  const value = values[i];
64
- const inAttr = isInsideAttribute(out);
66
+ const inAttr = scanner.insideTag;
65
67
  if (inAttr) {
66
- out += stringifyValue(value, true);
68
+ const rendered = stringifyValue(value, true);
69
+ out += rendered;
70
+ scanner.consume(rendered);
67
71
  }
68
72
  else {
69
73
  // Text-region slot — ALWAYS wrap in boundary markers so the SSR
@@ -77,9 +81,13 @@ function stringifyTemplateResult(result) {
77
81
  // (collapseMarkerRanges) so paths align exactly; the range also
78
82
  // anchors scalar text updates and nested-template swaps. Same
79
83
  // part-marker approach as lit-html / Solid.
84
+ const rendered = stringifyValue(value, false);
80
85
  out += `<!--${SLOT_START}-->`;
81
- out += stringifyValue(value, false);
86
+ out += rendered;
82
87
  out += `<!--${SLOT_END}-->`;
88
+ // A text-region value may itself carry markup (a nested template
89
+ // or a SafeString), so it has to move the scanner too.
90
+ scanner.consume(rendered);
83
91
  }
84
92
  }
85
93
  }
@@ -89,20 +97,47 @@ function stringifyTemplateResult(result) {
89
97
  const SLOT_START = "$";
90
98
  const SLOT_END = "/$";
91
99
  /**
92
- * Returns true if the position at the end of `htmlSoFar` lives inside
93
- * the value region of an HTML tag (between `<` and `>`). The check
94
- * walks backwards from the end, which is the smallest hint we need to
95
- * decide between text-region and attribute-region escaping.
100
+ * Tracks whether the cursor sits inside a tag, scanning FORWARD as the output
101
+ * grows.
102
+ *
103
+ * The obvious version walked backwards looking for the nearest `<` or `>`, but
104
+ * a `>` inside a quoted attribute value — `title="a > b"` — reads as the end of
105
+ * the tag, so the next interpolation is treated as a text slot and gets wrapped
106
+ * in `<!--$-->` markers INSIDE an attribute. That corrupts the markup and
107
+ * desyncs every following slot path at hydration. Quotes are what disambiguate,
108
+ * and they can only be resolved by reading forward.
109
+ *
110
+ * State is carried across appends instead of re-derived, so the whole render
111
+ * stays linear.
96
112
  */
97
- function isInsideAttribute(htmlSoFar) {
98
- for (let i = htmlSoFar.length - 1; i >= 0; i--) {
99
- const c = htmlSoFar.charCodeAt(i);
100
- if (c === 60 /* '<' */)
101
- return true;
102
- if (c === 62 /* '>' */)
103
- return false;
113
+ class TagScanner {
114
+ #inTag = false;
115
+ /** The quote character currently open inside a tag, or empty. */
116
+ #quote = "";
117
+ /** Feed everything appended since the last call. */
118
+ consume(chunk) {
119
+ for (let i = 0; i < chunk.length; i++) {
120
+ const c = chunk[i];
121
+ if (this.#quote !== "") {
122
+ if (c === this.#quote)
123
+ this.#quote = "";
124
+ continue;
125
+ }
126
+ if (this.#inTag) {
127
+ if (c === '"' || c === "'")
128
+ this.#quote = c;
129
+ else if (c === ">")
130
+ this.#inTag = false;
131
+ continue;
132
+ }
133
+ if (c === "<")
134
+ this.#inTag = true;
135
+ }
136
+ }
137
+ /** True when the cursor is inside a tag — an attribute region. */
138
+ get insideTag() {
139
+ return this.#inTag;
104
140
  }
105
- return false;
106
141
  }
107
142
  function stringifyValue(value, inAttribute) {
108
143
  if (value === null || value === undefined || value === false)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@c9up/aurora",
3
- "version": "0.1.27",
3
+ "version": "0.1.29",
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",
@@ -42,7 +42,7 @@
42
42
  },
43
43
  "peerDependencies": {
44
44
  "@c9up/comet": "^0.1.0",
45
- "@c9up/ream": "^0.1.0"
45
+ "@c9up/ream": "^0.2.0"
46
46
  },
47
47
  "peerDependenciesMeta": {
48
48
  "@c9up/comet": {
package/src/browser.ts CHANGED
@@ -1,3 +1,8 @@
1
+ /// <reference lib="dom" />
2
+ // This file uses browser globals. The reference pulls the DOM lib in for
3
+ // THIS file whatever `lib` the consumer configured, so a Node app
4
+ // typechecking against our sources does not trip over `window` —
5
+ // `types: "./src/index.ts"` means every consumer reads them.
1
6
  /**
2
7
  * Browser DX helpers — navigation + a typed `localStorage` wrapper.
3
8
  *
package/src/html.ts CHANGED
@@ -1,3 +1,8 @@
1
+ /// <reference lib="dom" />
2
+ // This file uses browser globals. The reference pulls the DOM lib in for
3
+ // THIS file whatever `lib` the consumer configured, so a Node app
4
+ // typechecking against our sources does not trip over `window` —
5
+ // `types: "./src/index.ts"` means every consumer reads them.
1
6
  /**
2
7
  * Tagged-template HTML parser.
3
8
  *
package/src/http.ts CHANGED
@@ -1,3 +1,8 @@
1
+ /// <reference lib="dom" />
2
+ // This file uses browser globals. The reference pulls the DOM lib in for
3
+ // THIS file whatever `lib` the consumer configured, so a Node app
4
+ // typechecking against our sources does not trip over `window` —
5
+ // `types: "./src/index.ts"` means every consumer reads them.
1
6
  /**
2
7
  * `HttpClient` — a small typed wrapper over `fetch` so call sites read
3
8
  * `await http.get<User>("/auth/me")` instead of hand-rolling headers,
package/src/hydrate.ts CHANGED
@@ -1,3 +1,8 @@
1
+ /// <reference lib="dom" />
2
+ // This file uses browser globals. The reference pulls the DOM lib in for
3
+ // THIS file whatever `lib` the consumer configured, so a Node app
4
+ // typechecking against our sources does not trip over `window` —
5
+ // `types: "./src/index.ts"` means every consumer reads them.
1
6
  /**
2
7
  * Hydration — adopt SSR-rendered HTML in the browser without rebuilding
3
8
  * the DOM.
package/src/liveClient.ts CHANGED
@@ -1,3 +1,8 @@
1
+ /// <reference lib="dom" />
2
+ // This file uses browser globals. The reference pulls the DOM lib in for
3
+ // THIS file whatever `lib` the consumer configured, so a Node app
4
+ // typechecking against our sources does not trip over `window` —
5
+ // `types: "./src/index.ts"` means every consumer reads them.
1
6
  /**
2
7
  * Live client runtime (Stage 5) — the thin browser side of live components.
3
8
  *
package/src/liveServer.ts CHANGED
@@ -14,7 +14,15 @@ import type { LiveRouter } from "./liveRouter.js";
14
14
 
15
15
  /** The slice of the host HTTP router this needs. */
16
16
  export interface LiveHttpRouter {
17
- post(path: string, handler: (ctx: LiveHttpContext) => unknown): unknown;
17
+ /**
18
+ * The handler returns `void | Promise<void>`, not `unknown`: that is what an
19
+ * HTTP handler returns in ream and in AdonisJS, and the wider shape made
20
+ * ream's own Router fail to satisfy this interface.
21
+ */
22
+ post(
23
+ path: string,
24
+ handler: (ctx: LiveHttpContext) => void | Promise<void>,
25
+ ): unknown;
18
26
  }
19
27
 
20
28
  /** The slice of the host HTTP context this needs (Ream's HttpContext satisfies it). */
package/src/render.ts CHANGED
@@ -1,3 +1,8 @@
1
+ /// <reference lib="dom" />
2
+ // This file uses browser globals. The reference pulls the DOM lib in for
3
+ // THIS file whatever `lib` the consumer configured, so a Node app
4
+ // typechecking against our sources does not trip over `window` —
5
+ // `types: "./src/index.ts"` means every consumer reads them.
1
6
  /**
2
7
  * Render a `TemplateResult` to the DOM and keep it reactive.
3
8
  *
package/src/rpc.ts CHANGED
@@ -1,3 +1,8 @@
1
+ /// <reference lib="dom" />
2
+ // This file uses browser globals. The reference pulls the DOM lib in for
3
+ // THIS file whatever `lib` the consumer configured, so a Node app
4
+ // typechecking against our sources does not trip over `window` —
5
+ // `types: "./src/index.ts"` means every consumer reads them.
1
6
  /**
2
7
  * Browser JSON-RPC 2.0 client for Ream's RPC endpoint — aurora's thin binding
3
8
  * over the agnostic {@link https://github.com/C9up/comet | @c9up/comet} client.
@@ -278,13 +278,29 @@ function escapeAttr(value: string): string {
278
278
 
279
279
  /**
280
280
  * Escape a JSON payload for safe embedding inside a `<script>` block.
281
- * The HTML parser closes the script on `</script>` regardless of JSON
282
- * quoting, so we slash-escape the `/`. We also escape `<!--` and `-->`
283
- * to dodge HTML-comment interpretation inside the script body.
281
+ *
282
+ * The HTML parser ends the script on `</script>` and reinterprets `<!--` /
283
+ * `-->` as comment markers, whatever the JSON quoting says — so those
284
+ * characters must not survive literally. They are escaped as \uXXXX, which is
285
+ * valid JSON: a backslash escape like `\!` or `\>` is NOT, and made
286
+ * `JSON.parse` throw on the client the moment a prop contained a comment
287
+ * marker, taking the whole page's hydration with it.
284
288
  */
285
289
  function escapeJsonForScript(value: unknown): string {
286
- return JSON.stringify(value)
287
- .replace(/<\/(script)/gi, "<\\/$1")
288
- .replace(/<!--/g, "<\\!--")
289
- .replace(/-->/g, "--\\>");
290
+ return JSON.stringify(value).replace(/[<>&\u2028\u2029]/g, (c) => {
291
+ switch (c) {
292
+ case "<":
293
+ return "\\u003c";
294
+ case ">":
295
+ return "\\u003e";
296
+ case "&":
297
+ return "\\u0026";
298
+ // Line separators are valid in JSON strings but terminate a JS line,
299
+ // so a script block carrying them raw is a syntax error.
300
+ case "\u2028":
301
+ return "\\u2028";
302
+ default:
303
+ return "\\u2029";
304
+ }
305
+ });
290
306
  }
package/src/ssr.ts CHANGED
@@ -50,6 +50,7 @@ function stringifyTemplateResult(result: TemplateResult): string {
50
50
  // segment. This three-step coordination is why the loop holds a
51
51
  // `pendingClosingQuote` flag.
52
52
  let pendingClosingQuote = false;
53
+ const scanner = new TagScanner();
53
54
  for (let i = 0; i < strings.length; i++) {
54
55
  let segment = strings[i];
55
56
  if (pendingClosingQuote) {
@@ -63,11 +64,14 @@ function stringifyTemplateResult(result: TemplateResult): string {
63
64
  pendingClosingQuote = true;
64
65
  }
65
66
  out += segment;
67
+ scanner.consume(segment);
66
68
  if (i < values.length && !skipValue) {
67
69
  const value = values[i];
68
- const inAttr = isInsideAttribute(out);
70
+ const inAttr = scanner.insideTag;
69
71
  if (inAttr) {
70
- out += stringifyValue(value, true);
72
+ const rendered = stringifyValue(value, true);
73
+ out += rendered;
74
+ scanner.consume(rendered);
71
75
  } else {
72
76
  // Text-region slot — ALWAYS wrap in boundary markers so the SSR
73
77
  // node structure matches the client template, which keeps exactly
@@ -80,9 +84,13 @@ function stringifyTemplateResult(result: TemplateResult): string {
80
84
  // (collapseMarkerRanges) so paths align exactly; the range also
81
85
  // anchors scalar text updates and nested-template swaps. Same
82
86
  // part-marker approach as lit-html / Solid.
87
+ const rendered = stringifyValue(value, false);
83
88
  out += `<!--${SLOT_START}-->`;
84
- out += stringifyValue(value, false);
89
+ out += rendered;
85
90
  out += `<!--${SLOT_END}-->`;
91
+ // A text-region value may itself carry markup (a nested template
92
+ // or a SafeString), so it has to move the scanner too.
93
+ scanner.consume(rendered);
86
94
  }
87
95
  }
88
96
  }
@@ -94,18 +102,45 @@ const SLOT_START = "$";
94
102
  const SLOT_END = "/$";
95
103
 
96
104
  /**
97
- * Returns true if the position at the end of `htmlSoFar` lives inside
98
- * the value region of an HTML tag (between `<` and `>`). The check
99
- * walks backwards from the end, which is the smallest hint we need to
100
- * decide between text-region and attribute-region escaping.
105
+ * Tracks whether the cursor sits inside a tag, scanning FORWARD as the output
106
+ * grows.
107
+ *
108
+ * The obvious version walked backwards looking for the nearest `<` or `>`, but
109
+ * a `>` inside a quoted attribute value — `title="a > b"` — reads as the end of
110
+ * the tag, so the next interpolation is treated as a text slot and gets wrapped
111
+ * in `<!--$-->` markers INSIDE an attribute. That corrupts the markup and
112
+ * desyncs every following slot path at hydration. Quotes are what disambiguate,
113
+ * and they can only be resolved by reading forward.
114
+ *
115
+ * State is carried across appends instead of re-derived, so the whole render
116
+ * stays linear.
101
117
  */
102
- function isInsideAttribute(htmlSoFar: string): boolean {
103
- for (let i = htmlSoFar.length - 1; i >= 0; i--) {
104
- const c = htmlSoFar.charCodeAt(i);
105
- if (c === 60 /* '<' */) return true;
106
- if (c === 62 /* '>' */) return false;
118
+ class TagScanner {
119
+ #inTag = false;
120
+ /** The quote character currently open inside a tag, or empty. */
121
+ #quote = "";
122
+
123
+ /** Feed everything appended since the last call. */
124
+ consume(chunk: string): void {
125
+ for (let i = 0; i < chunk.length; i++) {
126
+ const c = chunk[i];
127
+ if (this.#quote !== "") {
128
+ if (c === this.#quote) this.#quote = "";
129
+ continue;
130
+ }
131
+ if (this.#inTag) {
132
+ if (c === '"' || c === "'") this.#quote = c;
133
+ else if (c === ">") this.#inTag = false;
134
+ continue;
135
+ }
136
+ if (c === "<") this.#inTag = true;
137
+ }
138
+ }
139
+
140
+ /** True when the cursor is inside a tag — an attribute region. */
141
+ get insideTag(): boolean {
142
+ return this.#inTag;
107
143
  }
108
- return false;
109
144
  }
110
145
 
111
146
  function stringifyValue(value: unknown, inAttribute: boolean): string {