@c9up/aurora 0.1.36 → 0.1.38
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/AuroraProvider.js +13 -2
- package/dist/augmentations.d.ts +20 -0
- package/dist/html.js +80 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.js +9 -1
- package/dist/server.d.ts +1 -0
- package/dist/server.js +6 -0
- package/dist/services/main.d.ts +9 -0
- package/dist/services/main.js +11 -0
- package/dist/ssr.js +43 -1
- package/package.json +3 -2
- package/src/AuroraProvider.ts +13 -2
- package/src/augmentations.ts +23 -1
- package/src/html.ts +84 -1
- package/src/index.ts +9 -1
- package/src/server.ts +6 -0
- package/src/services/main.ts +12 -0
- package/src/ssr.ts +42 -1
package/dist/AuroraProvider.js
CHANGED
|
@@ -20,10 +20,12 @@ import { isAbsolute, resolve as resolvePath } from "node:path";
|
|
|
20
20
|
import { fileURLToPath } from "node:url";
|
|
21
21
|
import { AuroraManager } from "./AuroraManager.js";
|
|
22
22
|
import { auroraRoute } from "./route.js";
|
|
23
|
-
import { setAurora } from "./services/main.js";
|
|
23
|
+
import { clearAurora, getAurora, setAurora } from "./services/main.js";
|
|
24
24
|
import { renderToString } from "./ssr.js";
|
|
25
25
|
export default class AuroraProvider {
|
|
26
26
|
app;
|
|
27
|
+
/** What this provider bound, so shutdown only clears its own. */
|
|
28
|
+
#owned;
|
|
27
29
|
constructor(app) {
|
|
28
30
|
this.app = app;
|
|
29
31
|
}
|
|
@@ -46,6 +48,7 @@ export default class AuroraProvider {
|
|
|
46
48
|
// Force-resolve so `setAurora` runs even if the app never
|
|
47
49
|
// touches the singleton from a preload.
|
|
48
50
|
const manager = await this.app.container.resolve(AuroraManager);
|
|
51
|
+
this.#owned = manager;
|
|
49
52
|
setAurora(manager);
|
|
50
53
|
}
|
|
51
54
|
async start() {
|
|
@@ -77,7 +80,15 @@ export default class AuroraProvider {
|
|
|
77
80
|
}
|
|
78
81
|
}
|
|
79
82
|
async ready() { }
|
|
80
|
-
async shutdown() {
|
|
83
|
+
async shutdown() {
|
|
84
|
+
// Release the module-level singleton, while it is still ours. A stopped
|
|
85
|
+
// application left a dead Aurora manager reachable through `services/main`, and
|
|
86
|
+
// with two applications in one process the survivor's binding must not
|
|
87
|
+
// be the one cleared.
|
|
88
|
+
if (this.#owned !== undefined && getAurora() === this.#owned)
|
|
89
|
+
clearAurora();
|
|
90
|
+
this.#owned = undefined;
|
|
91
|
+
}
|
|
81
92
|
/**
|
|
82
93
|
* Resolve the user-supplied config:
|
|
83
94
|
* - relative `pages.root` (e.g. `./resources/pages`) is joined to
|
package/dist/augmentations.d.ts
CHANGED
|
@@ -15,9 +15,29 @@
|
|
|
15
15
|
* simply inert.
|
|
16
16
|
*/
|
|
17
17
|
import type { AuroraManager } from "./AuroraManager.js";
|
|
18
|
+
import type { AuroraRequestRenderer } from "./middleware.js";
|
|
18
19
|
declare module "@c9up/ream/types" {
|
|
19
20
|
interface ContainerBindings {
|
|
20
21
|
/** The Aurora manager, bound by `AuroraProvider`. */
|
|
21
22
|
aurora: AuroraManager;
|
|
22
23
|
}
|
|
23
24
|
}
|
|
25
|
+
declare module "@c9up/ream" {
|
|
26
|
+
interface HttpContext {
|
|
27
|
+
/**
|
|
28
|
+
* Render a page for THIS request — `ctx.aurora.render(name, props)`.
|
|
29
|
+
*
|
|
30
|
+
* Attached by the `auroraContext` middleware, which is what the docs tell
|
|
31
|
+
* an application to register. Without this declaration the property the
|
|
32
|
+
* middleware sets did not exist as far as the compiler was concerned, so
|
|
33
|
+
* the shorthand the documentation teaches did not typecheck, and a
|
|
34
|
+
* controller had to reach for the module-level `aurora.render(ctx, ...)`
|
|
35
|
+
* or assert its way past it.
|
|
36
|
+
*
|
|
37
|
+
* Optional, because the middleware is: an application that never
|
|
38
|
+
* registers it has no `ctx.aurora`, and saying otherwise would let a
|
|
39
|
+
* controller call something that is not there.
|
|
40
|
+
*/
|
|
41
|
+
aurora?: AuroraRequestRenderer;
|
|
42
|
+
}
|
|
43
|
+
}
|
package/dist/html.js
CHANGED
|
@@ -216,11 +216,90 @@ function collectSlots(root, classification) {
|
|
|
216
216
|
visit(root.content, []);
|
|
217
217
|
return slots;
|
|
218
218
|
}
|
|
219
|
+
/**
|
|
220
|
+
* Elements that only ever exist inside `<svg>`.
|
|
221
|
+
*
|
|
222
|
+
* Names shared with HTML — `a`, `title`, `style`, `script`, `text` in some
|
|
223
|
+
* dialects — are deliberately absent: seeing one says nothing about which
|
|
224
|
+
* namespace was meant, and guessing wrong would move an ordinary anchor into
|
|
225
|
+
* SVG.
|
|
226
|
+
*/
|
|
227
|
+
const SVG_ONLY = new Set([
|
|
228
|
+
"animate",
|
|
229
|
+
"animatemotion",
|
|
230
|
+
"animatetransform",
|
|
231
|
+
"circle",
|
|
232
|
+
"clippath",
|
|
233
|
+
"defs",
|
|
234
|
+
"desc",
|
|
235
|
+
"ellipse",
|
|
236
|
+
"feblend",
|
|
237
|
+
"fecolormatrix",
|
|
238
|
+
"fegaussianblur",
|
|
239
|
+
"femerge",
|
|
240
|
+
"feoffset",
|
|
241
|
+
"filter",
|
|
242
|
+
"foreignobject",
|
|
243
|
+
"g",
|
|
244
|
+
"image",
|
|
245
|
+
"line",
|
|
246
|
+
"lineargradient",
|
|
247
|
+
"marker",
|
|
248
|
+
"mask",
|
|
249
|
+
"path",
|
|
250
|
+
"pattern",
|
|
251
|
+
"polygon",
|
|
252
|
+
"polyline",
|
|
253
|
+
"radialgradient",
|
|
254
|
+
"rect",
|
|
255
|
+
"stop",
|
|
256
|
+
"svg",
|
|
257
|
+
"symbol",
|
|
258
|
+
"tspan",
|
|
259
|
+
"use",
|
|
260
|
+
]);
|
|
261
|
+
/**
|
|
262
|
+
* Whether this markup is SVG content that lost its `<svg>` ancestor.
|
|
263
|
+
*
|
|
264
|
+
* A template compiled on its own — `html\`<path/><path/>\``, the body of an
|
|
265
|
+
* icon helper — is parsed with no parent, and the HTML parser has no
|
|
266
|
+
* self-closing tag for an unknown element: the second `<path>` becomes a CHILD
|
|
267
|
+
* of the first, in the XHTML namespace. Nothing throws and nothing is logged;
|
|
268
|
+
* the icon is simply invisible, because `<path>` in the wrong namespace paints
|
|
269
|
+
* nothing. Parsing the same markup inside an `<svg>` makes the parser apply
|
|
270
|
+
* foreign-content rules and produce the two siblings that were written.
|
|
271
|
+
*
|
|
272
|
+
* `<svg>` itself is excluded: the parser already handles it when it is the root
|
|
273
|
+
* of the markup, and wrapping one in another would nest them.
|
|
274
|
+
*/
|
|
275
|
+
function isOrphanedSvgContent(root) {
|
|
276
|
+
const elements = Array.from(root.content.childNodes).filter((node) => node.nodeType === 1);
|
|
277
|
+
if (elements.length === 0)
|
|
278
|
+
return false;
|
|
279
|
+
return elements.every((el) => {
|
|
280
|
+
const name = el.localName.toLowerCase();
|
|
281
|
+
return name !== "svg" && SVG_ONLY.has(name);
|
|
282
|
+
});
|
|
283
|
+
}
|
|
219
284
|
function compile(strings) {
|
|
220
285
|
const classification = classifySlots(strings);
|
|
221
286
|
const markup = buildMarkup(strings, classification);
|
|
222
|
-
|
|
287
|
+
let tpl = document.createElement("template");
|
|
223
288
|
tpl.innerHTML = markup;
|
|
289
|
+
if (isOrphanedSvgContent(tpl)) {
|
|
290
|
+
// Re-parsed with the ancestor the markup was written for, then lifted
|
|
291
|
+
// back out: the nodes keep the SVG namespace they were given, and the
|
|
292
|
+
// slot paths below are collected against the shape that will actually
|
|
293
|
+
// be cloned.
|
|
294
|
+
const wrapper = document.createElement("template");
|
|
295
|
+
wrapper.innerHTML = `<svg>${markup}</svg>`;
|
|
296
|
+
const svg = wrapper.content.firstElementChild;
|
|
297
|
+
if (svg !== null) {
|
|
298
|
+
tpl = document.createElement("template");
|
|
299
|
+
while (svg.firstChild)
|
|
300
|
+
tpl.content.appendChild(svg.firstChild);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
224
303
|
const slots = collectSlots(tpl, classification);
|
|
225
304
|
return { element: tpl, slots };
|
|
226
305
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import "./augmentations.js";
|
|
2
1
|
export type { CookieCodec, CookieOptions, PersistedSignalOptions, ShareData, StorageArea, WebStorageOptions, WindowSize, } from "./browser.js";
|
|
3
2
|
export { back, booleanCookie, clipboard, cookie, cookieSignal, cookieState, forward, getCookieStore, hash, jsonCookie, mediaQuery, navigate, online, persistedSignal, prefersDark, queryParam, redirect, reload, replace, session, setCookieStore, share, storage, visibility, WebStorage, windowSize, } from "./browser.js";
|
|
4
3
|
export { type ClassValue, clsx, cn, twMerge } from "./cn.js";
|
package/dist/index.js
CHANGED
|
@@ -4,7 +4,15 @@
|
|
|
4
4
|
// node:fs / node:path / node:url live in `@c9up/aurora/server`. Keeping them off
|
|
5
5
|
// this barrel is what lets a browser bundle import the client primitives without
|
|
6
6
|
// the bundler dragging Node built-ins through the import graph.
|
|
7
|
-
|
|
7
|
+
//
|
|
8
|
+
// `./augmentations.js` is NOT loaded here either, and for a related reason: it
|
|
9
|
+
// augments ream's HttpContext and ContainerBindings, so it names `@c9up/ream`
|
|
10
|
+
// and drags ream's whole source into any program that imports this barrel. A
|
|
11
|
+
// component library that only wants `component`/`html` was compiling ream's
|
|
12
|
+
// decorated console commands under its own tsconfig, which does not enable
|
|
13
|
+
// decorators — a build failure, in a package that never mentions ream. The
|
|
14
|
+
// augmentations load from `@c9up/aurora/server` and `@c9up/aurora/provider`,
|
|
15
|
+
// which is where every type they describe is reachable from anyway.
|
|
8
16
|
export { back, booleanCookie, clipboard, cookie, cookieSignal, cookieState, forward, getCookieStore, hash, jsonCookie, mediaQuery, navigate, online, persistedSignal, prefersDark, queryParam, redirect, reload, replace, session, setCookieStore, share, storage, visibility, WebStorage, windowSize, } from "./browser.js";
|
|
9
17
|
export { clsx, cn, twMerge } from "./cn.js";
|
|
10
18
|
export { command } from "./command.js";
|
package/dist/server.d.ts
CHANGED
package/dist/server.js
CHANGED
|
@@ -4,6 +4,12 @@
|
|
|
4
4
|
// importing client primitives (component/html/hydrate/render) never pulls the
|
|
5
5
|
// Node built-ins through the import graph. Server code imports from
|
|
6
6
|
// `@c9up/aurora/server`; the client `.` entry stays node-free.
|
|
7
|
+
//
|
|
8
|
+
// The ream augmentations (`ctx.aurora`, the `aurora` container binding) load
|
|
9
|
+
// here rather than from the client barrel: everything they describe is
|
|
10
|
+
// server-side, and naming `@c9up/ream` from the browser entry pulled ream's
|
|
11
|
+
// source into every consumer's program — including ones that never use it.
|
|
12
|
+
import "./augmentations.js";
|
|
7
13
|
export { AuroraManager } from "./AuroraManager.js";
|
|
8
14
|
export { auroraContext, } from "./middleware.js";
|
|
9
15
|
export { Pages } from "./Pages.js";
|
package/dist/services/main.d.ts
CHANGED
|
@@ -14,5 +14,14 @@ import type { AuroraManager } from "../AuroraManager.js";
|
|
|
14
14
|
export declare function setAurora(value: AuroraManager): void;
|
|
15
15
|
/** @internal Read the singleton (or `undefined` pre-boot). */
|
|
16
16
|
export declare function getAurora(): AuroraManager | undefined;
|
|
17
|
+
/**
|
|
18
|
+
* @internal Release the singleton, so a shut-down application does not leave a
|
|
19
|
+
* dead Aurora manager reachable through `services/main`.
|
|
20
|
+
*
|
|
21
|
+
* The caller checks ownership first (`getAurora() === mine`): two applications
|
|
22
|
+
* share this module in one process, and the one shutting down must not clear
|
|
23
|
+
* what the other has since bound.
|
|
24
|
+
*/
|
|
25
|
+
export declare function clearAurora(): void;
|
|
17
26
|
declare const aurora: AuroraManager;
|
|
18
27
|
export default aurora;
|
package/dist/services/main.js
CHANGED
|
@@ -18,6 +18,17 @@ export function setAurora(value) {
|
|
|
18
18
|
export function getAurora() {
|
|
19
19
|
return instance;
|
|
20
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* @internal Release the singleton, so a shut-down application does not leave a
|
|
23
|
+
* dead Aurora manager reachable through `services/main`.
|
|
24
|
+
*
|
|
25
|
+
* The caller checks ownership first (`getAurora() === mine`): two applications
|
|
26
|
+
* share this module in one process, and the one shutting down must not clear
|
|
27
|
+
* what the other has since bound.
|
|
28
|
+
*/
|
|
29
|
+
export function clearAurora() {
|
|
30
|
+
instance = undefined;
|
|
31
|
+
}
|
|
21
32
|
const aurora = new Proxy({}, {
|
|
22
33
|
get(_target, prop) {
|
|
23
34
|
// A module loader inspects what it imports before anyone uses it: it reads
|
package/dist/ssr.js
CHANGED
|
@@ -64,15 +64,37 @@ function stringifyTemplateResult(result) {
|
|
|
64
64
|
// value written into the HTML, and any exception swallowed.
|
|
65
65
|
const directiveMatch = segment.match(/\s([@?.][\w-]+)=("|'|)$/);
|
|
66
66
|
const skipValue = directiveMatch !== null;
|
|
67
|
+
// Set when the skipped directive is a boolean attribute, which — unlike
|
|
68
|
+
// the other two — still has markup to emit. See below.
|
|
69
|
+
let booleanAttrName;
|
|
67
70
|
if (directiveMatch) {
|
|
68
|
-
const [whole = "", , quote] = directiveMatch;
|
|
71
|
+
const [whole = "", directive = "", quote] = directiveMatch;
|
|
69
72
|
segment = segment.slice(0, segment.length - whole.length);
|
|
73
|
+
// `?disabled=${x}` is HTML STATE, not a client-only binding.
|
|
74
|
+
// `@click` is a listener and `.value` a DOM property: neither
|
|
75
|
+
// exists until the runtime binds it, so dropping them is right.
|
|
76
|
+
// A boolean attribute is different — the browser acts on it while
|
|
77
|
+
// parsing. Skipping it too made the server contradict the very
|
|
78
|
+
// first client render: a `?hidden` panel arrived visible and
|
|
79
|
+
// blinked away once hydration caught up.
|
|
80
|
+
if (directive.startsWith("?"))
|
|
81
|
+
booleanAttrName = directive.slice(1);
|
|
70
82
|
// Only a quoted directive leaves a closing quote to swallow.
|
|
71
83
|
pendingClosingQuote =
|
|
72
84
|
quote === '"' ? '"' : quote === "'" ? "'" : undefined;
|
|
73
85
|
}
|
|
74
86
|
out += segment;
|
|
75
87
|
scanner.consume(segment);
|
|
88
|
+
if (booleanAttrName !== undefined && i < values.length) {
|
|
89
|
+
// Present-and-empty when truthy, absent otherwise — byte-for-byte
|
|
90
|
+
// what applyBooleanAttrSlot writes on the client, so hydration
|
|
91
|
+
// re-applying the effect is a no-op instead of a correction.
|
|
92
|
+
if (resolveBooleanValue(values[i])) {
|
|
93
|
+
const rendered = ` ${booleanAttrName}=""`;
|
|
94
|
+
out += rendered;
|
|
95
|
+
scanner.consume(rendered);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
76
98
|
if (i < values.length && !skipValue) {
|
|
77
99
|
const value = values[i];
|
|
78
100
|
const inAttr = scanner.insideTag;
|
|
@@ -151,6 +173,26 @@ class TagScanner {
|
|
|
151
173
|
return this.#inTag;
|
|
152
174
|
}
|
|
153
175
|
}
|
|
176
|
+
/**
|
|
177
|
+
* Read a boolean attribute's value the way the client reads it: a signal or a
|
|
178
|
+
* reactive expression is called ONCE, then coerced. One level is not an
|
|
179
|
+
* approximation — `applyBooleanAttrSlot` does exactly the same, so a signal
|
|
180
|
+
* that returns a signal is truthy on both sides.
|
|
181
|
+
*/
|
|
182
|
+
function resolveBooleanValue(value) {
|
|
183
|
+
if (isSignal(value) || typeof value === "function") {
|
|
184
|
+
try {
|
|
185
|
+
return Boolean(value());
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
// Same fail-soft as stringifyValue: an expression that throws
|
|
189
|
+
// server-side leaves the attribute off and lets the client effect
|
|
190
|
+
// decide once it has a real DOM to read.
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return Boolean(value);
|
|
195
|
+
}
|
|
154
196
|
function stringifyValue(value, inAttribute) {
|
|
155
197
|
if (value === null || value === undefined || value === false)
|
|
156
198
|
return "";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@c9up/aurora",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.38",
|
|
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",
|
|
@@ -58,6 +58,7 @@
|
|
|
58
58
|
"@types/node": "^22.19.15",
|
|
59
59
|
"@vitest/browser": "4.1.11",
|
|
60
60
|
"@vitest/browser-playwright": "4.1.11",
|
|
61
|
+
"@vitest/coverage-v8": "4.1.9",
|
|
61
62
|
"jsdom": "^30.0.1",
|
|
62
63
|
"playwright": "^1.61.1",
|
|
63
64
|
"typescript": "^6.0.2",
|
|
@@ -81,7 +82,7 @@
|
|
|
81
82
|
"build": "tsc -p tsconfig.build.json",
|
|
82
83
|
"typecheck": "tsc --noEmit",
|
|
83
84
|
"test": "vitest run",
|
|
84
|
-
"lint": "biome check src/",
|
|
85
|
+
"lint": "biome check src/ tests/",
|
|
85
86
|
"test:coverage": "vitest run --coverage",
|
|
86
87
|
"test:browser": "vitest run -c vitest.browser.config.ts"
|
|
87
88
|
}
|
package/src/AuroraProvider.ts
CHANGED
|
@@ -26,7 +26,7 @@ import type {
|
|
|
26
26
|
AssetsRequest,
|
|
27
27
|
AssetsResponse,
|
|
28
28
|
} from "./server/serveAssets.js";
|
|
29
|
-
import { setAurora } from "./services/main.js";
|
|
29
|
+
import { clearAurora, getAurora, setAurora } from "./services/main.js";
|
|
30
30
|
import { renderToString } from "./ssr.js";
|
|
31
31
|
|
|
32
32
|
interface AuroraContainer {
|
|
@@ -50,6 +50,9 @@ interface ReamRouter {
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
export default class AuroraProvider {
|
|
53
|
+
/** What this provider bound, so shutdown only clears its own. */
|
|
54
|
+
#owned: AuroraManager | undefined;
|
|
55
|
+
|
|
53
56
|
constructor(protected app: AuroraAppContext) {}
|
|
54
57
|
|
|
55
58
|
register(): void {
|
|
@@ -75,6 +78,7 @@ export default class AuroraProvider {
|
|
|
75
78
|
// touches the singleton from a preload.
|
|
76
79
|
const manager =
|
|
77
80
|
await this.app.container.resolve<AuroraManager>(AuroraManager);
|
|
81
|
+
this.#owned = manager;
|
|
78
82
|
setAurora(manager);
|
|
79
83
|
}
|
|
80
84
|
|
|
@@ -114,7 +118,14 @@ export default class AuroraProvider {
|
|
|
114
118
|
}
|
|
115
119
|
|
|
116
120
|
async ready(): Promise<void> {}
|
|
117
|
-
async shutdown(): Promise<void> {
|
|
121
|
+
async shutdown(): Promise<void> {
|
|
122
|
+
// Release the module-level singleton, while it is still ours. A stopped
|
|
123
|
+
// application left a dead Aurora manager reachable through `services/main`, and
|
|
124
|
+
// with two applications in one process the survivor's binding must not
|
|
125
|
+
// be the one cleared.
|
|
126
|
+
if (this.#owned !== undefined && getAurora() === this.#owned) clearAurora();
|
|
127
|
+
this.#owned = undefined;
|
|
128
|
+
}
|
|
118
129
|
|
|
119
130
|
/**
|
|
120
131
|
* Resolve the user-supplied config:
|
package/src/augmentations.ts
CHANGED
|
@@ -15,10 +15,12 @@
|
|
|
15
15
|
* simply inert.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
// Referenced so the
|
|
18
|
+
// Referenced so the augmentations below resolve the modules they augment.
|
|
19
|
+
import type {} from "@c9up/ream";
|
|
19
20
|
import type {} from "@c9up/ream/types";
|
|
20
21
|
|
|
21
22
|
import type { AuroraManager } from "./AuroraManager.js";
|
|
23
|
+
import type { AuroraRequestRenderer } from "./middleware.js";
|
|
22
24
|
|
|
23
25
|
declare module "@c9up/ream/types" {
|
|
24
26
|
interface ContainerBindings {
|
|
@@ -26,3 +28,23 @@ declare module "@c9up/ream/types" {
|
|
|
26
28
|
aurora: AuroraManager;
|
|
27
29
|
}
|
|
28
30
|
}
|
|
31
|
+
|
|
32
|
+
declare module "@c9up/ream" {
|
|
33
|
+
interface HttpContext {
|
|
34
|
+
/**
|
|
35
|
+
* Render a page for THIS request — `ctx.aurora.render(name, props)`.
|
|
36
|
+
*
|
|
37
|
+
* Attached by the `auroraContext` middleware, which is what the docs tell
|
|
38
|
+
* an application to register. Without this declaration the property the
|
|
39
|
+
* middleware sets did not exist as far as the compiler was concerned, so
|
|
40
|
+
* the shorthand the documentation teaches did not typecheck, and a
|
|
41
|
+
* controller had to reach for the module-level `aurora.render(ctx, ...)`
|
|
42
|
+
* or assert its way past it.
|
|
43
|
+
*
|
|
44
|
+
* Optional, because the middleware is: an application that never
|
|
45
|
+
* registers it has no `ctx.aurora`, and saying otherwise would let a
|
|
46
|
+
* controller call something that is not there.
|
|
47
|
+
*/
|
|
48
|
+
aurora?: AuroraRequestRenderer;
|
|
49
|
+
}
|
|
50
|
+
}
|
package/src/html.ts
CHANGED
|
@@ -259,11 +259,94 @@ function collectSlots(
|
|
|
259
259
|
return slots;
|
|
260
260
|
}
|
|
261
261
|
|
|
262
|
+
/**
|
|
263
|
+
* Elements that only ever exist inside `<svg>`.
|
|
264
|
+
*
|
|
265
|
+
* Names shared with HTML — `a`, `title`, `style`, `script`, `text` in some
|
|
266
|
+
* dialects — are deliberately absent: seeing one says nothing about which
|
|
267
|
+
* namespace was meant, and guessing wrong would move an ordinary anchor into
|
|
268
|
+
* SVG.
|
|
269
|
+
*/
|
|
270
|
+
const SVG_ONLY = new Set([
|
|
271
|
+
"animate",
|
|
272
|
+
"animatemotion",
|
|
273
|
+
"animatetransform",
|
|
274
|
+
"circle",
|
|
275
|
+
"clippath",
|
|
276
|
+
"defs",
|
|
277
|
+
"desc",
|
|
278
|
+
"ellipse",
|
|
279
|
+
"feblend",
|
|
280
|
+
"fecolormatrix",
|
|
281
|
+
"fegaussianblur",
|
|
282
|
+
"femerge",
|
|
283
|
+
"feoffset",
|
|
284
|
+
"filter",
|
|
285
|
+
"foreignobject",
|
|
286
|
+
"g",
|
|
287
|
+
"image",
|
|
288
|
+
"line",
|
|
289
|
+
"lineargradient",
|
|
290
|
+
"marker",
|
|
291
|
+
"mask",
|
|
292
|
+
"path",
|
|
293
|
+
"pattern",
|
|
294
|
+
"polygon",
|
|
295
|
+
"polyline",
|
|
296
|
+
"radialgradient",
|
|
297
|
+
"rect",
|
|
298
|
+
"stop",
|
|
299
|
+
"svg",
|
|
300
|
+
"symbol",
|
|
301
|
+
"tspan",
|
|
302
|
+
"use",
|
|
303
|
+
]);
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Whether this markup is SVG content that lost its `<svg>` ancestor.
|
|
307
|
+
*
|
|
308
|
+
* A template compiled on its own — `html\`<path/><path/>\``, the body of an
|
|
309
|
+
* icon helper — is parsed with no parent, and the HTML parser has no
|
|
310
|
+
* self-closing tag for an unknown element: the second `<path>` becomes a CHILD
|
|
311
|
+
* of the first, in the XHTML namespace. Nothing throws and nothing is logged;
|
|
312
|
+
* the icon is simply invisible, because `<path>` in the wrong namespace paints
|
|
313
|
+
* nothing. Parsing the same markup inside an `<svg>` makes the parser apply
|
|
314
|
+
* foreign-content rules and produce the two siblings that were written.
|
|
315
|
+
*
|
|
316
|
+
* `<svg>` itself is excluded: the parser already handles it when it is the root
|
|
317
|
+
* of the markup, and wrapping one in another would nest them.
|
|
318
|
+
*/
|
|
319
|
+
function isOrphanedSvgContent(root: HTMLTemplateElement): boolean {
|
|
320
|
+
const elements = Array.from(root.content.childNodes).filter(
|
|
321
|
+
(node): node is Element => node.nodeType === 1,
|
|
322
|
+
);
|
|
323
|
+
if (elements.length === 0) return false;
|
|
324
|
+
return elements.every((el) => {
|
|
325
|
+
const name = el.localName.toLowerCase();
|
|
326
|
+
return name !== "svg" && SVG_ONLY.has(name);
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
262
330
|
function compile(strings: TemplateStringsArray): Template {
|
|
263
331
|
const classification = classifySlots(strings);
|
|
264
332
|
const markup = buildMarkup(strings, classification);
|
|
265
|
-
|
|
333
|
+
let tpl = document.createElement("template");
|
|
266
334
|
tpl.innerHTML = markup;
|
|
335
|
+
|
|
336
|
+
if (isOrphanedSvgContent(tpl)) {
|
|
337
|
+
// Re-parsed with the ancestor the markup was written for, then lifted
|
|
338
|
+
// back out: the nodes keep the SVG namespace they were given, and the
|
|
339
|
+
// slot paths below are collected against the shape that will actually
|
|
340
|
+
// be cloned.
|
|
341
|
+
const wrapper = document.createElement("template");
|
|
342
|
+
wrapper.innerHTML = `<svg>${markup}</svg>`;
|
|
343
|
+
const svg = wrapper.content.firstElementChild;
|
|
344
|
+
if (svg !== null) {
|
|
345
|
+
tpl = document.createElement("template");
|
|
346
|
+
while (svg.firstChild) tpl.content.appendChild(svg.firstChild);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
267
350
|
const slots = collectSlots(tpl, classification);
|
|
268
351
|
return { element: tpl, slots };
|
|
269
352
|
}
|
package/src/index.ts
CHANGED
|
@@ -4,7 +4,15 @@
|
|
|
4
4
|
// node:fs / node:path / node:url live in `@c9up/aurora/server`. Keeping them off
|
|
5
5
|
// this barrel is what lets a browser bundle import the client primitives without
|
|
6
6
|
// the bundler dragging Node built-ins through the import graph.
|
|
7
|
-
|
|
7
|
+
//
|
|
8
|
+
// `./augmentations.js` is NOT loaded here either, and for a related reason: it
|
|
9
|
+
// augments ream's HttpContext and ContainerBindings, so it names `@c9up/ream`
|
|
10
|
+
// and drags ream's whole source into any program that imports this barrel. A
|
|
11
|
+
// component library that only wants `component`/`html` was compiling ream's
|
|
12
|
+
// decorated console commands under its own tsconfig, which does not enable
|
|
13
|
+
// decorators — a build failure, in a package that never mentions ream. The
|
|
14
|
+
// augmentations load from `@c9up/aurora/server` and `@c9up/aurora/provider`,
|
|
15
|
+
// which is where every type they describe is reachable from anyway.
|
|
8
16
|
|
|
9
17
|
export type {
|
|
10
18
|
CookieCodec,
|
package/src/server.ts
CHANGED
|
@@ -4,6 +4,12 @@
|
|
|
4
4
|
// importing client primitives (component/html/hydrate/render) never pulls the
|
|
5
5
|
// Node built-ins through the import graph. Server code imports from
|
|
6
6
|
// `@c9up/aurora/server`; the client `.` entry stays node-free.
|
|
7
|
+
//
|
|
8
|
+
// The ream augmentations (`ctx.aurora`, the `aurora` container binding) load
|
|
9
|
+
// here rather than from the client barrel: everything they describe is
|
|
10
|
+
// server-side, and naming `@c9up/ream` from the browser entry pulled ream's
|
|
11
|
+
// source into every consumer's program — including ones that never use it.
|
|
12
|
+
import "./augmentations.js";
|
|
7
13
|
|
|
8
14
|
export { AuroraManager, type AuroraManagerConfig } from "./AuroraManager.js";
|
|
9
15
|
export {
|
package/src/services/main.ts
CHANGED
|
@@ -24,6 +24,18 @@ export function getAurora(): AuroraManager | undefined {
|
|
|
24
24
|
return instance;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* @internal Release the singleton, so a shut-down application does not leave a
|
|
29
|
+
* dead Aurora manager reachable through `services/main`.
|
|
30
|
+
*
|
|
31
|
+
* The caller checks ownership first (`getAurora() === mine`): two applications
|
|
32
|
+
* share this module in one process, and the one shutting down must not clear
|
|
33
|
+
* what the other has since bound.
|
|
34
|
+
*/
|
|
35
|
+
export function clearAurora(): void {
|
|
36
|
+
instance = undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
27
39
|
const aurora: AuroraManager = new Proxy({} as AuroraManager, {
|
|
28
40
|
get(_target, prop) {
|
|
29
41
|
// A module loader inspects what it imports before anyone uses it: it reads
|
package/src/ssr.ts
CHANGED
|
@@ -68,15 +68,36 @@ function stringifyTemplateResult(result: TemplateResult): string {
|
|
|
68
68
|
// value written into the HTML, and any exception swallowed.
|
|
69
69
|
const directiveMatch = segment.match(/\s([@?.][\w-]+)=("|'|)$/);
|
|
70
70
|
const skipValue = directiveMatch !== null;
|
|
71
|
+
// Set when the skipped directive is a boolean attribute, which — unlike
|
|
72
|
+
// the other two — still has markup to emit. See below.
|
|
73
|
+
let booleanAttrName: string | undefined;
|
|
71
74
|
if (directiveMatch) {
|
|
72
|
-
const [whole = "", , quote] = directiveMatch;
|
|
75
|
+
const [whole = "", directive = "", quote] = directiveMatch;
|
|
73
76
|
segment = segment.slice(0, segment.length - whole.length);
|
|
77
|
+
// `?disabled=${x}` is HTML STATE, not a client-only binding.
|
|
78
|
+
// `@click` is a listener and `.value` a DOM property: neither
|
|
79
|
+
// exists until the runtime binds it, so dropping them is right.
|
|
80
|
+
// A boolean attribute is different — the browser acts on it while
|
|
81
|
+
// parsing. Skipping it too made the server contradict the very
|
|
82
|
+
// first client render: a `?hidden` panel arrived visible and
|
|
83
|
+
// blinked away once hydration caught up.
|
|
84
|
+
if (directive.startsWith("?")) booleanAttrName = directive.slice(1);
|
|
74
85
|
// Only a quoted directive leaves a closing quote to swallow.
|
|
75
86
|
pendingClosingQuote =
|
|
76
87
|
quote === '"' ? '"' : quote === "'" ? "'" : undefined;
|
|
77
88
|
}
|
|
78
89
|
out += segment;
|
|
79
90
|
scanner.consume(segment);
|
|
91
|
+
if (booleanAttrName !== undefined && i < values.length) {
|
|
92
|
+
// Present-and-empty when truthy, absent otherwise — byte-for-byte
|
|
93
|
+
// what applyBooleanAttrSlot writes on the client, so hydration
|
|
94
|
+
// re-applying the effect is a no-op instead of a correction.
|
|
95
|
+
if (resolveBooleanValue(values[i])) {
|
|
96
|
+
const rendered = ` ${booleanAttrName}=""`;
|
|
97
|
+
out += rendered;
|
|
98
|
+
scanner.consume(rendered);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
80
101
|
if (i < values.length && !skipValue) {
|
|
81
102
|
const value = values[i];
|
|
82
103
|
const inAttr = scanner.insideTag;
|
|
@@ -155,6 +176,26 @@ class TagScanner {
|
|
|
155
176
|
}
|
|
156
177
|
}
|
|
157
178
|
|
|
179
|
+
/**
|
|
180
|
+
* Read a boolean attribute's value the way the client reads it: a signal or a
|
|
181
|
+
* reactive expression is called ONCE, then coerced. One level is not an
|
|
182
|
+
* approximation — `applyBooleanAttrSlot` does exactly the same, so a signal
|
|
183
|
+
* that returns a signal is truthy on both sides.
|
|
184
|
+
*/
|
|
185
|
+
function resolveBooleanValue(value: unknown): boolean {
|
|
186
|
+
if (isSignal(value) || typeof value === "function") {
|
|
187
|
+
try {
|
|
188
|
+
return Boolean((value as () => unknown)());
|
|
189
|
+
} catch {
|
|
190
|
+
// Same fail-soft as stringifyValue: an expression that throws
|
|
191
|
+
// server-side leaves the attribute off and lets the client effect
|
|
192
|
+
// decide once it has a real DOM to read.
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return Boolean(value);
|
|
197
|
+
}
|
|
198
|
+
|
|
158
199
|
function stringifyValue(value: unknown, inAttribute: boolean): string {
|
|
159
200
|
if (value === null || value === undefined || value === false) return "";
|
|
160
201
|
if (value === true) return inAttribute ? "" : "true";
|