@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.
Files changed (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +36 -0
  3. package/dist/AuroraManager.d.ts +44 -0
  4. package/dist/AuroraManager.js +47 -0
  5. package/dist/AuroraProvider.d.ts +52 -0
  6. package/dist/AuroraProvider.js +145 -0
  7. package/dist/Pages.d.ts +78 -0
  8. package/dist/Pages.js +116 -0
  9. package/dist/component.d.ts +55 -0
  10. package/dist/component.js +97 -0
  11. package/dist/html.d.ts +30 -0
  12. package/dist/html.js +246 -0
  13. package/dist/hydrate.d.ts +29 -0
  14. package/dist/hydrate.js +379 -0
  15. package/dist/index.d.ts +12 -0
  16. package/dist/index.js +12 -0
  17. package/dist/reactive.d.ts +83 -0
  18. package/dist/reactive.js +217 -0
  19. package/dist/relay.d.ts +43 -0
  20. package/dist/relay.js +144 -0
  21. package/dist/render.d.ts +25 -0
  22. package/dist/render.js +283 -0
  23. package/dist/route.d.ts +64 -0
  24. package/dist/route.js +49 -0
  25. package/dist/server/renderPage.d.ts +62 -0
  26. package/dist/server/renderPage.js +83 -0
  27. package/dist/server/serveAssets.d.ts +43 -0
  28. package/dist/server/serveAssets.js +89 -0
  29. package/dist/services/main.d.ts +18 -0
  30. package/dist/services/main.js +31 -0
  31. package/dist/ssr.d.ts +22 -0
  32. package/dist/ssr.js +179 -0
  33. package/dist/types.d.ts +78 -0
  34. package/dist/types.js +15 -0
  35. package/package.json +69 -0
  36. package/src/AuroraManager.ts +76 -0
  37. package/src/AuroraProvider.ts +187 -0
  38. package/src/Pages.ts +164 -0
  39. package/src/component.ts +138 -0
  40. package/src/html.ts +296 -0
  41. package/src/hydrate.ts +518 -0
  42. package/src/index.ts +43 -0
  43. package/src/reactive.ts +265 -0
  44. package/src/relay.ts +171 -0
  45. package/src/render.ts +378 -0
  46. package/src/route.ts +96 -0
  47. package/src/server/renderPage.ts +135 -0
  48. package/src/server/serveAssets.ts +135 -0
  49. package/src/services/main.ts +40 -0
  50. package/src/ssr.ts +179 -0
  51. package/src/types.ts +97 -0
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Component runtime.
3
+ *
4
+ * const Counter = component<{ initial?: number }>(({ initial = 0 }) => {
5
+ * const count = signal(initial)
6
+ * onMount(() => { document.title = `Count: ${count()}` })
7
+ * return html`<button @click="${() => count(count() + 1)}">${count}</button>`
8
+ * })
9
+ *
10
+ * Setup runs **once** when the component is created (mount time).
11
+ * `onMount` / `onUnmount` are bound to the per-component context active
12
+ * during setup, so they see the right cleanup queue when the component
13
+ * is unmounted later. State lives in plain `signal()` / `memo()` from
14
+ * `./reactive.js` — there's no separate hook layer, and signals work
15
+ * both inside and outside a component setup.
16
+ *
17
+ * Unlike React, there is no re-render — reactivity is push-based via the
18
+ * signals the setup function captures. The compiled template is what
19
+ * actually moves on screen.
20
+ */
21
+ const contextStack = [];
22
+ function activeContext() {
23
+ const ctx = contextStack[contextStack.length - 1];
24
+ if (!ctx) {
25
+ throw new Error("[aurora] onMount / onUnmount called outside component() — only valid inside a component setup function.");
26
+ }
27
+ return ctx;
28
+ }
29
+ /**
30
+ * Build a component factory. The returned function takes props and
31
+ * produces a `TemplateResult` that can be rendered or nested inside
32
+ * another template.
33
+ *
34
+ * `component()` does NOT itself mount anything — it composes. The
35
+ * outermost `render(Component(props), container)` is what mounts.
36
+ */
37
+ export function component(setup) {
38
+ return (props) => {
39
+ const ctx = {
40
+ cleanups: [],
41
+ mountHooks: [],
42
+ };
43
+ contextStack.push(ctx);
44
+ try {
45
+ const result = setup((props ?? {}));
46
+ return wrapWithLifecycle(result, ctx);
47
+ }
48
+ finally {
49
+ contextStack.pop();
50
+ }
51
+ };
52
+ }
53
+ /**
54
+ * Stitch the component context onto the returned TemplateResult so the
55
+ * outer renderer can flush mount hooks + register unmount cleanups
56
+ * automatically when this slot is mounted / removed.
57
+ *
58
+ * The mechanism is a `Symbol`-keyed handoff: the renderer's text-slot
59
+ * path (which handles nested TemplateResults) checks for this property
60
+ * and forwards the lifecycle.
61
+ */
62
+ const COMPONENT_LIFECYCLE = Symbol.for("aurora:component");
63
+ function wrapWithLifecycle(result, ctx) {
64
+ result[COMPONENT_LIFECYCLE] = {
65
+ mountHooks: ctx.mountHooks,
66
+ cleanups: ctx.cleanups,
67
+ };
68
+ return result;
69
+ }
70
+ /**
71
+ * Internal — extract the lifecycle attachment a `component()` left on a
72
+ * TemplateResult, if any. The renderer calls this after mounting the
73
+ * fragment so onMount fires once the DOM is live, and the returned
74
+ * cleanups bubble into the outer dispose chain.
75
+ */
76
+ export function readComponentLifecycle(result) {
77
+ return result[COMPONENT_LIFECYCLE];
78
+ }
79
+ // ─── Lifecycle ────────────────────────────────────────────────────
80
+ /**
81
+ * Schedule a callback to run after the component is mounted into the
82
+ * live document. Returning a function from `onMount` registers it as an
83
+ * unmount cleanup.
84
+ */
85
+ export function onMount(fn) {
86
+ const ctx = activeContext();
87
+ ctx.mountHooks.push(fn);
88
+ }
89
+ /**
90
+ * Schedule a callback to run when the component unmounts. Equivalent
91
+ * to the cleanup return of `onMount` but available without a paired
92
+ * mount action.
93
+ */
94
+ export function onUnmount(fn) {
95
+ const ctx = activeContext();
96
+ ctx.cleanups.push(fn);
97
+ }
package/dist/html.d.ts ADDED
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Tagged-template HTML parser.
3
+ *
4
+ * html`<button @click="${onClick}">${count}</button>`
5
+ *
6
+ * returns a `TemplateResult`. The first call with a given `strings` array
7
+ * compiles a `Template` (parsed `<template>` element + slot descriptors)
8
+ * and caches it; subsequent calls reuse the compiled artefact and only
9
+ * pair it with fresh `values`.
10
+ *
11
+ * The parser is intentionally minimal — it supports text interpolation
12
+ * (`>${x}<`), attribute interpolation (`attr="${x}"` and multi-slot
13
+ * `class="a ${x} b ${y}"`), boolean attributes (`?disabled="${x}"`), DOM
14
+ * properties (`.value="${x}"`), and event listeners (`@click="${fn}"`).
15
+ * No custom directives, no fragments-in-attribute-name, no comment-only
16
+ * placeholders.
17
+ */
18
+ import { isTemplateResult, type Template, type TemplateResult } from "./types.js";
19
+ /**
20
+ * Compile (and cache) the template for the given strings array. Exposed
21
+ * for the renderer + the SSR path; apps stick to `html`.
22
+ */
23
+ export declare function getTemplate(strings: TemplateStringsArray): Template;
24
+ /**
25
+ * Tagged-template entrypoint. The `strings` array is reference-stable per
26
+ * source location (TC39 guarantee), so caching by reference is safe and
27
+ * O(1) on the hot render path.
28
+ */
29
+ export declare function html(strings: TemplateStringsArray, ...values: unknown[]): TemplateResult;
30
+ export { isTemplateResult };
package/dist/html.js ADDED
@@ -0,0 +1,246 @@
1
+ /**
2
+ * Tagged-template HTML parser.
3
+ *
4
+ * html`<button @click="${onClick}">${count}</button>`
5
+ *
6
+ * returns a `TemplateResult`. The first call with a given `strings` array
7
+ * compiles a `Template` (parsed `<template>` element + slot descriptors)
8
+ * and caches it; subsequent calls reuse the compiled artefact and only
9
+ * pair it with fresh `values`.
10
+ *
11
+ * The parser is intentionally minimal — it supports text interpolation
12
+ * (`>${x}<`), attribute interpolation (`attr="${x}"` and multi-slot
13
+ * `class="a ${x} b ${y}"`), boolean attributes (`?disabled="${x}"`), DOM
14
+ * properties (`.value="${x}"`), and event listeners (`@click="${fn}"`).
15
+ * No custom directives, no fragments-in-attribute-name, no comment-only
16
+ * placeholders.
17
+ */
18
+ import { isTemplateResult, TEMPLATE_RESULT_BRAND, } from "./types.js";
19
+ const TEMPLATE_CACHE = new WeakMap();
20
+ /** Sentinel inserted at every `${...}` site. Read back during the walk. */
21
+ const MARKER = "__aurora_slot_";
22
+ /** Marker comment that takes a child slot inside a text region. */
23
+ const TEXT_NODE_MARKER = `<!--${MARKER}-->`;
24
+ /** Markup-friendly placeholder for slots inside attribute values. */
25
+ function attrPlaceholder(i) {
26
+ return `${MARKER}${i}__`;
27
+ }
28
+ function classifySlots(strings) {
29
+ const result = [];
30
+ let insideTag = false;
31
+ let insideComment = false;
32
+ for (let i = 0; i < strings.length - 1; i++) {
33
+ const segment = strings[i];
34
+ for (let j = 0; j < segment.length; j++) {
35
+ if (insideComment) {
36
+ // Comments swallow everything (including stray `<` / `>`) up
37
+ // to the literal `-->` terminator; without this guard a
38
+ // `<!-- > -->` literal would flip the tag scanner mid-stride.
39
+ if (segment[j] === "-" &&
40
+ segment[j + 1] === "-" &&
41
+ segment[j + 2] === ">") {
42
+ insideComment = false;
43
+ j += 2;
44
+ }
45
+ continue;
46
+ }
47
+ if (!insideTag &&
48
+ segment[j] === "<" &&
49
+ segment[j + 1] === "!" &&
50
+ segment[j + 2] === "-" &&
51
+ segment[j + 3] === "-") {
52
+ insideComment = true;
53
+ j += 3;
54
+ continue;
55
+ }
56
+ const ch = segment[j];
57
+ if (!insideTag && ch === "<")
58
+ insideTag = true;
59
+ else if (insideTag && ch === ">")
60
+ insideTag = false;
61
+ }
62
+ result.push({ region: insideTag ? "attribute" : "text" });
63
+ }
64
+ return result;
65
+ }
66
+ /**
67
+ * Build the HTML markup to feed the `<template>` element.
68
+ *
69
+ * Slots in text regions are replaced with a `<!--__aurora_slot_-->` comment
70
+ * that survives DOM parsing. Slots in attribute regions are replaced with a
71
+ * unique string token like `__aurora_slot_3__` that the post-parse walk
72
+ * detects when scanning attribute values.
73
+ */
74
+ function buildMarkup(strings, classification) {
75
+ let out = strings[0];
76
+ for (let i = 0; i < classification.length; i++) {
77
+ out +=
78
+ classification[i].region === "text"
79
+ ? TEXT_NODE_MARKER
80
+ : attrPlaceholder(i);
81
+ out += strings[i + 1];
82
+ }
83
+ return out;
84
+ }
85
+ /**
86
+ * Walk the parsed fragment and collect a slot descriptor for every
87
+ * placeholder we emitted. The walk is depth-first, child-by-child, and
88
+ * records the integer path so `render()` can re-walk a clone without any
89
+ * string parsing.
90
+ */
91
+ function collectSlots(root, classification) {
92
+ const slots = [];
93
+ let slotIndex = 0;
94
+ // Classify one marker-bearing attribute into slot(s): `@event`, `?boolean`,
95
+ // `.prop`, or a (possibly multi-slot) regular attribute. Pushes the slot(s)
96
+ // and queues the attribute for removal from the inert template.
97
+ function classifyAttribute(attr, path, toRemove) {
98
+ const value = attr.value;
99
+ if (!value.includes(MARKER))
100
+ return;
101
+ const localPath = [...path];
102
+ if (attr.name.startsWith("@")) {
103
+ const slot = {
104
+ kind: "event",
105
+ path: localPath,
106
+ event: attr.name.slice(1),
107
+ };
108
+ slots.push(slot);
109
+ slotIndex++;
110
+ toRemove.push(attr.name);
111
+ return;
112
+ }
113
+ if (attr.name.startsWith("?")) {
114
+ const slot = {
115
+ kind: "boolean-attr",
116
+ path: localPath,
117
+ name: attr.name.slice(1),
118
+ };
119
+ slots.push(slot);
120
+ slotIndex++;
121
+ toRemove.push(attr.name);
122
+ return;
123
+ }
124
+ if (attr.name.startsWith(".")) {
125
+ const slot = {
126
+ kind: "prop",
127
+ path: localPath,
128
+ name: attr.name.slice(1),
129
+ };
130
+ slots.push(slot);
131
+ slotIndex++;
132
+ toRemove.push(attr.name);
133
+ return;
134
+ }
135
+ // Regular attribute — may host one or more slots inline with static text.
136
+ // Strip the placeholder version (the renderer re-applies via setAttribute)
137
+ // so the inert template never carries the `__aurora_slot_0__` artefact.
138
+ const parts = value.split(/__aurora_slot_(\d+)__/);
139
+ // Even indices = static text, odd = slot indices. One slot with no
140
+ // surrounding static text → a "pure" attr slot; else carry static parts.
141
+ if (parts.length === 3 && parts[0] === "" && parts[2] === "") {
142
+ const slot = { kind: "attr", path: localPath, name: attr.name };
143
+ slots.push(slot);
144
+ slotIndex++;
145
+ }
146
+ else {
147
+ // Multi-slot attribute. Each slot points at the same `staticParts`
148
+ // array; consumers re-join on every update.
149
+ const staticParts = [];
150
+ const slotCountInThisAttr = (parts.length - 1) / 2;
151
+ for (let i = 0; i < parts.length; i += 2) {
152
+ staticParts.push(parts[i]);
153
+ }
154
+ for (let i = 0; i < slotCountInThisAttr; i++) {
155
+ const slot = {
156
+ kind: "attr",
157
+ path: localPath,
158
+ name: attr.name,
159
+ staticParts,
160
+ staticPartIndex: i,
161
+ };
162
+ slots.push(slot);
163
+ slotIndex++;
164
+ }
165
+ }
166
+ toRemove.push(attr.name);
167
+ }
168
+ function visit(node, path) {
169
+ // Process children FIRST in reverse so a comment-marker we're about
170
+ // to remove never invalidates the index of a later sibling. But text
171
+ // slots also bring the node into existence — handle them with a
172
+ // stable path captured up front.
173
+ if (node.nodeType === 8 /* Comment */) {
174
+ const data = node.data;
175
+ if (data === MARKER) {
176
+ if (slotIndex >= classification.length)
177
+ return;
178
+ const cls = classification[slotIndex];
179
+ if (cls.region !== "text") {
180
+ throw new Error(`[aurora] internal classification mismatch at slot ${slotIndex}`);
181
+ }
182
+ const slot = { kind: "text", path: [...path] };
183
+ slots.push(slot);
184
+ slotIndex++;
185
+ }
186
+ return;
187
+ }
188
+ if (node.nodeType === 1 /* Element */) {
189
+ const el = node;
190
+ // Scan attributes — multiple slots can share one attribute.
191
+ // Collect attrs to remove AFTER iteration (mutating during it
192
+ // shifts indices on some DOM implementations).
193
+ const toRemove = [];
194
+ for (const attr of Array.from(el.attributes)) {
195
+ classifyAttribute(attr, path, toRemove);
196
+ }
197
+ for (const name of toRemove)
198
+ el.removeAttribute(name);
199
+ }
200
+ // Recurse into children with their indices.
201
+ let childIndex = 0;
202
+ let child = node.firstChild;
203
+ while (child) {
204
+ const nextSibling = child.nextSibling;
205
+ visit(child, [...path, childIndex]);
206
+ child = nextSibling;
207
+ childIndex++;
208
+ }
209
+ }
210
+ visit(root.content, []);
211
+ return slots;
212
+ }
213
+ function compile(strings) {
214
+ const classification = classifySlots(strings);
215
+ const markup = buildMarkup(strings, classification);
216
+ const tpl = document.createElement("template");
217
+ tpl.innerHTML = markup;
218
+ const slots = collectSlots(tpl, classification);
219
+ return { element: tpl, slots };
220
+ }
221
+ /**
222
+ * Compile (and cache) the template for the given strings array. Exposed
223
+ * for the renderer + the SSR path; apps stick to `html`.
224
+ */
225
+ export function getTemplate(strings) {
226
+ let cached = TEMPLATE_CACHE.get(strings);
227
+ if (!cached) {
228
+ cached = compile(strings);
229
+ TEMPLATE_CACHE.set(strings, cached);
230
+ }
231
+ return cached;
232
+ }
233
+ /**
234
+ * Tagged-template entrypoint. The `strings` array is reference-stable per
235
+ * source location (TC39 guarantee), so caching by reference is safe and
236
+ * O(1) on the hot render path.
237
+ */
238
+ export function html(strings, ...values) {
239
+ const result = {
240
+ strings,
241
+ values,
242
+ [TEMPLATE_RESULT_BRAND]: true,
243
+ };
244
+ return result;
245
+ }
246
+ export { isTemplateResult };
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Hydration — adopt SSR-rendered HTML in the browser without rebuilding
3
+ * the DOM.
4
+ *
5
+ * `hydrate(container, factory)` runs the same component factory used
6
+ * server-side, recomputes the slot bindings, and attaches them to the
7
+ * existing nodes. Where SSR emitted plain text for `${signal}`, hydrate
8
+ * locates the same text node (via path resolution against the cloned
9
+ * template) and starts an effect that updates it on signal change.
10
+ *
11
+ * Implementation note: we still run `getTemplate(strings)` to know
12
+ * where each slot lives, then walk the LIVE container tree using the
13
+ * same path. SSR output must match the shape of the parsed template
14
+ * for hydration to find the right node — same constraint as React's
15
+ * hydration mismatch warning.
16
+ */
17
+ import { type Disposer } from "./render.js";
18
+ import { type TemplateResult } from "./types.js";
19
+ /** @internal Reset the warn-once flag (tests). */
20
+ export declare function resetHydrateWarnings(): void;
21
+ /**
22
+ * Adopt SSR markup inside `container`. `factory` is the same function
23
+ * that was rendered server-side — its output (a TemplateResult tree)
24
+ * tells hydrate which slots to wire.
25
+ *
26
+ * Returns a `Disposer` that detaches every effect and event listener,
27
+ * leaving the DOM in place.
28
+ */
29
+ export declare function hydrate(container: Element, factory: () => TemplateResult): Disposer;