@c9up/aurora 0.1.3
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/LICENSE +21 -0
- package/README.md +36 -0
- package/dist/AuroraManager.d.ts +44 -0
- package/dist/AuroraManager.js +47 -0
- package/dist/AuroraProvider.d.ts +52 -0
- package/dist/AuroraProvider.js +145 -0
- package/dist/Pages.d.ts +78 -0
- package/dist/Pages.js +116 -0
- package/dist/component.d.ts +55 -0
- package/dist/component.js +97 -0
- package/dist/html.d.ts +30 -0
- package/dist/html.js +246 -0
- package/dist/hydrate.d.ts +29 -0
- package/dist/hydrate.js +379 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +12 -0
- package/dist/reactive.d.ts +83 -0
- package/dist/reactive.js +217 -0
- package/dist/relay.d.ts +43 -0
- package/dist/relay.js +144 -0
- package/dist/render.d.ts +25 -0
- package/dist/render.js +283 -0
- package/dist/route.d.ts +64 -0
- package/dist/route.js +49 -0
- package/dist/server/renderPage.d.ts +62 -0
- package/dist/server/renderPage.js +83 -0
- package/dist/server/serveAssets.d.ts +43 -0
- package/dist/server/serveAssets.js +89 -0
- package/dist/services/main.d.ts +18 -0
- package/dist/services/main.js +31 -0
- package/dist/ssr.d.ts +22 -0
- package/dist/ssr.js +179 -0
- package/dist/types.d.ts +78 -0
- package/dist/types.js +15 -0
- package/package.json +69 -0
- package/src/AuroraManager.ts +76 -0
- package/src/AuroraProvider.ts +187 -0
- package/src/Pages.ts +164 -0
- package/src/component.ts +138 -0
- package/src/html.ts +296 -0
- package/src/hydrate.ts +518 -0
- package/src/index.ts +43 -0
- package/src/reactive.ts +265 -0
- package/src/relay.ts +171 -0
- package/src/render.ts +378 -0
- package/src/route.ts +96 -0
- package/src/server/renderPage.ts +135 -0
- package/src/server/serveAssets.ts +135 -0
- package/src/services/main.ts +40 -0
- package/src/ssr.ts +179 -0
- package/src/types.ts +97 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `serveAssets` — generic static-file handler exposed by aurora so an
|
|
3
|
+
* app can mount the runtime + the pages dist with a couple of routes:
|
|
4
|
+
*
|
|
5
|
+
* router.get('/_assets/aurora/*', serveAssets({ root: auroraDistPath }))
|
|
6
|
+
* router.get('/_assets/pages/*', serveAssets({ root: pagesPath }))
|
|
7
|
+
*
|
|
8
|
+
* The handler is framework-agnostic: it reads `ctx.request.param('*')`
|
|
9
|
+
* and writes to `ctx.response`. Any context that satisfies
|
|
10
|
+
* `AssetsHttpContext` (Ream, AdonisJS, anything duck-typed) works.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { readFile, realpath } from "node:fs/promises";
|
|
14
|
+
import { extname, join, resolve as resolvePath, sep } from "node:path";
|
|
15
|
+
|
|
16
|
+
const CONTENT_TYPES: Record<string, string> = {
|
|
17
|
+
".js": "text/javascript; charset=utf-8",
|
|
18
|
+
".mjs": "text/javascript; charset=utf-8",
|
|
19
|
+
".map": "application/json; charset=utf-8",
|
|
20
|
+
".css": "text/css; charset=utf-8",
|
|
21
|
+
".json": "application/json; charset=utf-8",
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export interface AssetsRequest {
|
|
25
|
+
/**
|
|
26
|
+
* Read the wildcard `*` segment of the matched route. Most routers
|
|
27
|
+
* (Ream, AdonisJS, fastify with params) expose this as
|
|
28
|
+
* `params['*']` — the duck-typed helper below accepts either
|
|
29
|
+
* convention.
|
|
30
|
+
*/
|
|
31
|
+
param(name: string): unknown;
|
|
32
|
+
}
|
|
33
|
+
export interface AssetsResponse {
|
|
34
|
+
status(code: number): AssetsResponse;
|
|
35
|
+
header(name: string, value: string): AssetsResponse;
|
|
36
|
+
send(body: string | Buffer): void;
|
|
37
|
+
}
|
|
38
|
+
export interface AssetsHttpContext {
|
|
39
|
+
request: AssetsRequest;
|
|
40
|
+
response: AssetsResponse;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface ServeAssetsOptions {
|
|
44
|
+
/**
|
|
45
|
+
* Absolute filesystem root the handler is allowed to serve from.
|
|
46
|
+
* Requests resolving outside this root return 403.
|
|
47
|
+
*/
|
|
48
|
+
root: string;
|
|
49
|
+
/**
|
|
50
|
+
* `Cache-Control` value to emit. Defaults to a dev-friendly
|
|
51
|
+
* 60-second TTL. Production deployments should hash the asset
|
|
52
|
+
* name and switch to `public, max-age=31536000, immutable`.
|
|
53
|
+
*/
|
|
54
|
+
cacheControl?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function serveAssets(
|
|
58
|
+
options: ServeAssetsOptions,
|
|
59
|
+
): (ctx: AssetsHttpContext) => Promise<void> {
|
|
60
|
+
const root = options.root;
|
|
61
|
+
const cacheControl = options.cacheControl ?? "public, max-age=60";
|
|
62
|
+
// Canonicalize the root ONCE at handler creation. The realpath check
|
|
63
|
+
// below compares against this canonical form so a symlinked root
|
|
64
|
+
// (e.g. `/var/www/current → /var/www/release-42`) still resolves
|
|
65
|
+
// requests correctly. `realpath` failure at construction means the
|
|
66
|
+
// configured root doesn't exist yet — we fall back to the lexical
|
|
67
|
+
// resolve so the first request emits a clean 404 instead of a boot
|
|
68
|
+
// crash. The realpath re-check at request time handles that case.
|
|
69
|
+
let canonicalRoot: string | undefined;
|
|
70
|
+
realpath(root).then(
|
|
71
|
+
(p) => {
|
|
72
|
+
canonicalRoot = p;
|
|
73
|
+
},
|
|
74
|
+
() => {
|
|
75
|
+
/* root not yet on disk — request-time realpath will surface it */
|
|
76
|
+
},
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
return async (ctx) => {
|
|
80
|
+
const rest = ctx.request.param("*");
|
|
81
|
+
if (typeof rest !== "string" || rest.length === 0) {
|
|
82
|
+
ctx.response.status(400).send("missing asset path");
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
// First gate: lexical containment check. `resolve()` collapses
|
|
86
|
+
// `../` segments; we assert the resolved path still starts with
|
|
87
|
+
// `root + sep`. This blocks the "../../../etc/passwd" class of
|
|
88
|
+
// requests before we ever touch the filesystem.
|
|
89
|
+
const absolute = resolvePath(join(root, rest));
|
|
90
|
+
if (!absolute.startsWith(root + sep) && absolute !== root) {
|
|
91
|
+
ctx.response.status(403).send("forbidden");
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Second gate: dereference any symlinks under the root and
|
|
96
|
+
// re-check containment against the canonical root. Without this
|
|
97
|
+
// step a symlink planted at `<root>/legit → /etc/secrets` would
|
|
98
|
+
// pass the lexical check above and be served. We re-canonicalize
|
|
99
|
+
// the root each request when the constructor-time realpath
|
|
100
|
+
// hadn't resolved yet (root mounted after boot).
|
|
101
|
+
let canonicalAbsolute: string;
|
|
102
|
+
let canonicalRootNow: string;
|
|
103
|
+
try {
|
|
104
|
+
canonicalRootNow = canonicalRoot ?? (await realpath(root));
|
|
105
|
+
canonicalAbsolute = await realpath(absolute);
|
|
106
|
+
} catch {
|
|
107
|
+
// realpath fails if the target doesn't exist — emit a normal
|
|
108
|
+
// 404 here so symlink-escape probes can't be distinguished
|
|
109
|
+
// from genuine misses via response timing or status.
|
|
110
|
+
ctx.response.status(404).send("asset not found");
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (
|
|
114
|
+
!canonicalAbsolute.startsWith(canonicalRootNow + sep) &&
|
|
115
|
+
canonicalAbsolute !== canonicalRootNow
|
|
116
|
+
) {
|
|
117
|
+
ctx.response.status(403).send("forbidden");
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
let body: Buffer;
|
|
122
|
+
try {
|
|
123
|
+
body = await readFile(canonicalAbsolute);
|
|
124
|
+
} catch {
|
|
125
|
+
ctx.response.status(404).send("asset not found");
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const type =
|
|
130
|
+
CONTENT_TYPES[extname(canonicalAbsolute)] ?? "application/octet-stream";
|
|
131
|
+
ctx.response.header("content-type", type);
|
|
132
|
+
ctx.response.header("cache-control", cacheControl);
|
|
133
|
+
ctx.response.send(body);
|
|
134
|
+
};
|
|
135
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default `AuroraManager` singleton — Adonis-style:
|
|
3
|
+
*
|
|
4
|
+
* import aurora from '@c9up/aurora/services/main'
|
|
5
|
+
*
|
|
6
|
+
* await aurora.render(ctx, 'ProjectPage', { project, tasks })
|
|
7
|
+
*
|
|
8
|
+
* Populated either by `AuroraProvider.boot()` (when the app uses
|
|
9
|
+
* `() => import('@c9up/aurora/provider')`) or by the app itself via
|
|
10
|
+
* `setAurora(myManager)`.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { AuroraManager } from "../AuroraManager.js";
|
|
14
|
+
|
|
15
|
+
let instance: AuroraManager | undefined;
|
|
16
|
+
|
|
17
|
+
/** @internal Bind the singleton (called by AuroraProvider or by the app). */
|
|
18
|
+
export function setAurora(value: AuroraManager): void {
|
|
19
|
+
instance = value;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** @internal Read the singleton (or `undefined` pre-boot). */
|
|
23
|
+
export function getAurora(): AuroraManager | undefined {
|
|
24
|
+
return instance;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const aurora: AuroraManager = new Proxy({} as AuroraManager, {
|
|
28
|
+
get(_target, prop) {
|
|
29
|
+
if (!instance) {
|
|
30
|
+
throw new Error(
|
|
31
|
+
"[aurora] AuroraManager singleton accessed before AuroraProvider.boot() ran " +
|
|
32
|
+
"or `setAurora(myManager)` was called. Wire one of them first.",
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
const value = Reflect.get(instance, prop, instance);
|
|
36
|
+
return typeof value === "function" ? value.bind(instance) : value;
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
export default aurora;
|
package/src/ssr.ts
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-side rendering — produces an HTML string from a `TemplateResult`
|
|
3
|
+
* without ever touching the DOM.
|
|
4
|
+
*
|
|
5
|
+
* Reads each slot's value eagerly (signals get a one-shot snapshot,
|
|
6
|
+
* functions are invoked, nested TemplateResults recurse). Event handlers
|
|
7
|
+
* are dropped server-side; hydration re-binds them once the markup
|
|
8
|
+
* lands in the browser.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { isSignal } from "./reactive.js";
|
|
12
|
+
import { isTemplateResult, type TemplateResult } from "./types.js";
|
|
13
|
+
|
|
14
|
+
const VOID_ELEMENTS = new Set([
|
|
15
|
+
"area",
|
|
16
|
+
"base",
|
|
17
|
+
"br",
|
|
18
|
+
"col",
|
|
19
|
+
"embed",
|
|
20
|
+
"hr",
|
|
21
|
+
"img",
|
|
22
|
+
"input",
|
|
23
|
+
"keygen",
|
|
24
|
+
"link",
|
|
25
|
+
"meta",
|
|
26
|
+
"source",
|
|
27
|
+
"track",
|
|
28
|
+
"wbr",
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Stringify a TemplateResult into HTML. Returns the markup ready to be
|
|
33
|
+
* shipped over the wire — no surrounding `<html>`/`<head>`/`<body>`
|
|
34
|
+
* unless the template includes them.
|
|
35
|
+
*
|
|
36
|
+
* The function walks the `strings` array directly; it does NOT depend
|
|
37
|
+
* on the DOM-side template cache, so it works in any JS runtime (Node,
|
|
38
|
+
* Cloudflare Workers, Bun, Deno).
|
|
39
|
+
*/
|
|
40
|
+
export function renderToString(result: TemplateResult): string {
|
|
41
|
+
return stringifyTemplateResult(result);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function stringifyTemplateResult(result: TemplateResult): string {
|
|
45
|
+
const { strings, values } = result;
|
|
46
|
+
let out = "";
|
|
47
|
+
// When a segment ends with a directive (` @click="`, ` ?disabled="`,
|
|
48
|
+
// ` .value="`), we drop the directive prefix from that segment, skip
|
|
49
|
+
// the matching value, and consume the closing `"` from the next
|
|
50
|
+
// segment. This three-step coordination is why the loop holds a
|
|
51
|
+
// `pendingClosingQuote` flag.
|
|
52
|
+
let pendingClosingQuote = false;
|
|
53
|
+
for (let i = 0; i < strings.length; i++) {
|
|
54
|
+
let segment = strings[i];
|
|
55
|
+
if (pendingClosingQuote) {
|
|
56
|
+
segment = segment.replace(/^"/, "");
|
|
57
|
+
pendingClosingQuote = false;
|
|
58
|
+
}
|
|
59
|
+
const directiveMatch = segment.match(/\s([@?.][\w-]+)="$/);
|
|
60
|
+
const skipValue = directiveMatch !== null;
|
|
61
|
+
if (directiveMatch) {
|
|
62
|
+
segment = segment.slice(0, segment.length - directiveMatch[0].length);
|
|
63
|
+
pendingClosingQuote = true;
|
|
64
|
+
}
|
|
65
|
+
out += segment;
|
|
66
|
+
if (i < values.length && !skipValue) {
|
|
67
|
+
const value = values[i];
|
|
68
|
+
const inAttr = isInsideAttribute(out);
|
|
69
|
+
if (!inAttr && isReactiveStructuredSlot(value)) {
|
|
70
|
+
// Reactive text slot whose value is a nested template /
|
|
71
|
+
// array — wrap the rendered content in boundary markers so
|
|
72
|
+
// hydration can locate the exact node range and SWAP it when
|
|
73
|
+
// the signal changes client-side. Without these markers a
|
|
74
|
+
// nested-template slot hydrates once and then goes stale
|
|
75
|
+
// (no way to find where the subtree starts/ends). Scalar
|
|
76
|
+
// reactive slots (`${signal}` → text) are NOT wrapped: their
|
|
77
|
+
// hydration updates the text node in place, no range needed.
|
|
78
|
+
out += `<!--${SLOT_START}-->`;
|
|
79
|
+
out += stringifyValue(value, false);
|
|
80
|
+
out += `<!--${SLOT_END}-->`;
|
|
81
|
+
} else {
|
|
82
|
+
out += stringifyValue(value, inAttr);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Boundary-marker comment payloads (kept in sync with hydrate.ts). */
|
|
90
|
+
const SLOT_START = "$";
|
|
91
|
+
const SLOT_END = "/$";
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* True when `value` is a reactive expression (signal / function) whose
|
|
95
|
+
* current evaluation is a structured node payload (a nested
|
|
96
|
+
* TemplateResult, or an array). These are the slots that can SWAP their
|
|
97
|
+
* subtree on a client-side change and therefore need boundary markers
|
|
98
|
+
* for hydration to find the range. A reactive slot resolving to a
|
|
99
|
+
* scalar (string / number) is updated in place and needs no markers.
|
|
100
|
+
*/
|
|
101
|
+
function isReactiveStructuredSlot(value: unknown): boolean {
|
|
102
|
+
if (!isSignal(value) && typeof value !== "function") return false;
|
|
103
|
+
let evaluated: unknown;
|
|
104
|
+
try {
|
|
105
|
+
evaluated = isSignal(value)
|
|
106
|
+
? (value as () => unknown)()
|
|
107
|
+
: (value as () => unknown)();
|
|
108
|
+
} catch {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
return isTemplateResult(evaluated) || Array.isArray(evaluated);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Returns true if the position at the end of `htmlSoFar` lives inside
|
|
116
|
+
* the value region of an HTML tag (between `<` and `>`). The check
|
|
117
|
+
* walks backwards from the end, which is the smallest hint we need to
|
|
118
|
+
* decide between text-region and attribute-region escaping.
|
|
119
|
+
*/
|
|
120
|
+
function isInsideAttribute(htmlSoFar: string): boolean {
|
|
121
|
+
for (let i = htmlSoFar.length - 1; i >= 0; i--) {
|
|
122
|
+
const c = htmlSoFar.charCodeAt(i);
|
|
123
|
+
if (c === 60 /* '<' */) return true;
|
|
124
|
+
if (c === 62 /* '>' */) return false;
|
|
125
|
+
}
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function stringifyValue(value: unknown, inAttribute: boolean): string {
|
|
130
|
+
if (value === null || value === undefined || value === false) return "";
|
|
131
|
+
if (value === true) return inAttribute ? "" : "true";
|
|
132
|
+
if (isSignal(value)) return stringifyValue(value(), inAttribute);
|
|
133
|
+
if (typeof value === "function") {
|
|
134
|
+
// In attribute position: directive handlers (`@click`, `?disabled`,
|
|
135
|
+
// `.prop`) have already been stripped by `stripDirectiveBefore`.
|
|
136
|
+
// A function reaching this point is a reactive-expression text
|
|
137
|
+
// slot (`${() => ...}`), which we evaluate eagerly server-side.
|
|
138
|
+
try {
|
|
139
|
+
return stringifyValue((value as () => unknown)(), inAttribute);
|
|
140
|
+
} catch {
|
|
141
|
+
return "";
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (Array.isArray(value)) {
|
|
145
|
+
let out = "";
|
|
146
|
+
for (const item of value) out += stringifyValue(item, inAttribute);
|
|
147
|
+
return out;
|
|
148
|
+
}
|
|
149
|
+
if (isTemplateResult(value)) return stringifyTemplateResult(value);
|
|
150
|
+
// Plain value — escape HTML entities (text) or attribute special
|
|
151
|
+
// characters (attribute value).
|
|
152
|
+
return inAttribute ? escapeAttr(String(value)) : escapeText(String(value));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function escapeText(s: string): string {
|
|
156
|
+
return s
|
|
157
|
+
.replaceAll("&", "&")
|
|
158
|
+
.replaceAll("<", "<")
|
|
159
|
+
.replaceAll(">", ">");
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function escapeAttr(s: string): string {
|
|
163
|
+
// Escape BOTH quote styles: the engine doesn't force double-quoted
|
|
164
|
+
// attributes (the classifier only tracks `<`/`>`), so a template author
|
|
165
|
+
// writing `id='${x}'` must still be safe — without escaping `'` a value
|
|
166
|
+
// like `' onmouseover='alert(1)` would break out of a single-quoted
|
|
167
|
+
// attribute. `>` isn't strictly required inside a quoted value but is
|
|
168
|
+
// escaped to stay safe under stray scanners that hunt tag boundaries
|
|
169
|
+
// before resolving the quote context.
|
|
170
|
+
return s
|
|
171
|
+
.replaceAll("&", "&")
|
|
172
|
+
.replaceAll('"', """)
|
|
173
|
+
.replaceAll("'", "'")
|
|
174
|
+
.replaceAll("<", "<")
|
|
175
|
+
.replaceAll(">", ">");
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// VOID_ELEMENTS exported for downstream tooling (hydration heuristics).
|
|
179
|
+
export { VOID_ELEMENTS };
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared types for aurora templates.
|
|
3
|
+
*
|
|
4
|
+
* A `TemplateResult` is what `html\`...\`` returns. It carries the raw
|
|
5
|
+
* pieces (strings, values) and a stable reference to the parsed
|
|
6
|
+
* `Template` (memoised by `strings`). Templates are framework-internal —
|
|
7
|
+
* apps only ever see `TemplateResult`.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A lifecycle/effect callback that runs and OPTIONALLY returns a cleanup
|
|
12
|
+
* function — exactly React's `EffectCallback = () => void | (() => void)`.
|
|
13
|
+
*
|
|
14
|
+
* The `void` member of the return union is load-bearing: it is what makes a
|
|
15
|
+
* plain `() => {}` (whose return type is `void`) assignable here. Replacing it
|
|
16
|
+
* with `(() => void) | undefined` makes every void-returning callback a type
|
|
17
|
+
* error (a `() => void` is NOT assignable to `() => (() => void) | undefined`).
|
|
18
|
+
* So `noConfusingVoidType` is suppressed once, at this single definition,
|
|
19
|
+
* instead of being worked around at ~15 call sites with broken types.
|
|
20
|
+
*/
|
|
21
|
+
// biome-ignore lint/suspicious/noConfusingVoidType: `void` is required so plain void-returning callbacks stay assignable (React EffectCallback pattern; dropping it breaks ~10 call sites)
|
|
22
|
+
export type EffectCallback = () => void | (() => void);
|
|
23
|
+
|
|
24
|
+
/** Slot descriptor — where a `${value}` lives inside a parsed template. */
|
|
25
|
+
export type SlotKind = "text" | "attr" | "event" | "boolean-attr" | "prop";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Path to the binding point inside the cloned template. Each step is a
|
|
29
|
+
* child index. We never walk by query selectors because attribute and text
|
|
30
|
+
* placements would need synthetic markers in markup an app provided.
|
|
31
|
+
*/
|
|
32
|
+
export type NodePath = readonly number[];
|
|
33
|
+
|
|
34
|
+
export interface TextSlot {
|
|
35
|
+
kind: "text";
|
|
36
|
+
path: NodePath;
|
|
37
|
+
}
|
|
38
|
+
export interface AttrSlot {
|
|
39
|
+
kind: "attr";
|
|
40
|
+
path: NodePath;
|
|
41
|
+
name: string;
|
|
42
|
+
/**
|
|
43
|
+
* When the attribute interpolates more than one `${...}` slot, every
|
|
44
|
+
* slot shares the same `name` and the static segments are stored under
|
|
45
|
+
* `staticParts`. The render step joins them back together each tick.
|
|
46
|
+
*/
|
|
47
|
+
staticParts?: readonly string[];
|
|
48
|
+
staticPartIndex?: number;
|
|
49
|
+
}
|
|
50
|
+
export interface EventSlot {
|
|
51
|
+
kind: "event";
|
|
52
|
+
path: NodePath;
|
|
53
|
+
event: string;
|
|
54
|
+
}
|
|
55
|
+
export interface BooleanAttrSlot {
|
|
56
|
+
kind: "boolean-attr";
|
|
57
|
+
path: NodePath;
|
|
58
|
+
name: string;
|
|
59
|
+
}
|
|
60
|
+
export interface PropSlot {
|
|
61
|
+
kind: "prop";
|
|
62
|
+
path: NodePath;
|
|
63
|
+
name: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export type Slot = TextSlot | AttrSlot | EventSlot | BooleanAttrSlot | PropSlot;
|
|
67
|
+
|
|
68
|
+
/** Compiled artefact — produced once per unique `strings` array. */
|
|
69
|
+
export interface Template {
|
|
70
|
+
/**
|
|
71
|
+
* A `<template>` element whose `content` fragment is cloned on every
|
|
72
|
+
* render. Cloning is much cheaper than re-parsing the HTML string.
|
|
73
|
+
*/
|
|
74
|
+
readonly element: HTMLTemplateElement;
|
|
75
|
+
/** Slot descriptors in source order (same order as the `${...}` values). */
|
|
76
|
+
readonly slots: readonly Slot[];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Tagged-template return value — what `html\`\`` produces. */
|
|
80
|
+
export interface TemplateResult {
|
|
81
|
+
readonly strings: TemplateStringsArray;
|
|
82
|
+
readonly values: readonly unknown[];
|
|
83
|
+
readonly [TEMPLATE_RESULT_BRAND]: true;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export const TEMPLATE_RESULT_BRAND: unique symbol = Symbol.for(
|
|
87
|
+
"aurora:template-result",
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
export function isTemplateResult(value: unknown): value is TemplateResult {
|
|
91
|
+
return (
|
|
92
|
+
typeof value === "object" &&
|
|
93
|
+
value !== null &&
|
|
94
|
+
(value as { [TEMPLATE_RESULT_BRAND]?: boolean })[TEMPLATE_RESULT_BRAND] ===
|
|
95
|
+
true
|
|
96
|
+
);
|
|
97
|
+
}
|