@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
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 C9up
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,36 @@
1
+ # @c9up/aurora
2
+
3
+ > Reactive UI runtime for the Ream framework — tagged-template DOM, signal-based state, isomorphic SSR + hydration, zero build step.
4
+
5
+ Part of **[Ream](https://github.com/C9up/ream)** — a Rust-powered, AdonisJS-compatible Node.js framework. Independent, publishable package.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ pnpm add @c9up/aurora
11
+ ream configure @c9up/aurora
12
+ ```
13
+
14
+ ## Usage
15
+
16
+ Register the provider in your app, then configure it under `config/aurora.ts`:
17
+
18
+ ```ts
19
+ // reamrc.ts
20
+ providers: [
21
+ () => import('@c9up/aurora/provider'),
22
+ ]
23
+ ```
24
+
25
+ ## Entry points
26
+
27
+ - `@c9up/aurora` — main API
28
+ - `@c9up/aurora/provider` — Ream IoC provider
29
+ - `@c9up/aurora/services/main` — container service accessor
30
+ - `@c9up/aurora/relay` — realtime adapter
31
+ - `@c9up/aurora/ssr` — server-side rendering
32
+ - `@c9up/aurora/hydrate` — client hydration
33
+
34
+ ## License
35
+
36
+ MIT
@@ -0,0 +1,44 @@
1
+ /**
2
+ * AuroraManager — the singleton behind `@c9up/aurora/services/main`.
3
+ *
4
+ * import aurora from '@c9up/aurora/services/main'
5
+ *
6
+ * async show(ctx) {
7
+ * await aurora.render(ctx, 'ProjectPage', { project, tasks })
8
+ * }
9
+ *
10
+ * The manager pairs a `Pages` registry with the SSR pipeline and the
11
+ * dist-asset handler. The provider builds it from `config/aurora.ts`;
12
+ * the app can also instantiate one manually for tests.
13
+ */
14
+ import { Pages, type PagesConfig } from "./Pages.js";
15
+ import { type RenderHttpContext, type RenderPageOptions } from "./server/renderPage.js";
16
+ import { type AssetsHttpContext } from "./server/serveAssets.js";
17
+ export interface AuroraManagerConfig {
18
+ pages: PagesConfig;
19
+ /**
20
+ * Filesystem path to aurora's pre-built `dist/`. Defaults to the
21
+ * dist directory shipped with the installed `@c9up/aurora` package.
22
+ * Override only if you want to serve a custom build.
23
+ */
24
+ auroraDistRoot?: string;
25
+ }
26
+ export declare class AuroraManager {
27
+ readonly pages: Pages;
28
+ readonly auroraDistRoot: string;
29
+ constructor(config: AuroraManagerConfig);
30
+ /**
31
+ * SSR + hydrate + ship the document.
32
+ */
33
+ render(ctx: RenderHttpContext, name: string, props: unknown, options?: RenderPageOptions): Promise<void>;
34
+ /**
35
+ * Handler for aurora's pre-built ESM runtime. Mount on
36
+ * `GET /_assets/aurora/*`.
37
+ */
38
+ auroraAssetsHandler(): (ctx: AssetsHttpContext) => Promise<void>;
39
+ /**
40
+ * Handler for the app's pages directory. Mount on
41
+ * `GET /_assets/pages/*`.
42
+ */
43
+ pageAssetsHandler(): (ctx: AssetsHttpContext) => Promise<void>;
44
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * AuroraManager — the singleton behind `@c9up/aurora/services/main`.
3
+ *
4
+ * import aurora from '@c9up/aurora/services/main'
5
+ *
6
+ * async show(ctx) {
7
+ * await aurora.render(ctx, 'ProjectPage', { project, tasks })
8
+ * }
9
+ *
10
+ * The manager pairs a `Pages` registry with the SSR pipeline and the
11
+ * dist-asset handler. The provider builds it from `config/aurora.ts`;
12
+ * the app can also instantiate one manually for tests.
13
+ */
14
+ import { dirname, resolve as resolvePath } from "node:path";
15
+ import { fileURLToPath } from "node:url";
16
+ import { Pages } from "./Pages.js";
17
+ import { renderPage, } from "./server/renderPage.js";
18
+ import { serveAssets } from "./server/serveAssets.js";
19
+ const DEFAULT_AURORA_DIST = resolvePath(dirname(fileURLToPath(import.meta.url)), "../dist");
20
+ export class AuroraManager {
21
+ pages;
22
+ auroraDistRoot;
23
+ constructor(config) {
24
+ this.pages = new Pages(config.pages);
25
+ this.auroraDistRoot = config.auroraDistRoot ?? DEFAULT_AURORA_DIST;
26
+ }
27
+ /**
28
+ * SSR + hydrate + ship the document.
29
+ */
30
+ render(ctx, name, props, options) {
31
+ return renderPage(ctx, this.pages, name, props, options);
32
+ }
33
+ /**
34
+ * Handler for aurora's pre-built ESM runtime. Mount on
35
+ * `GET /_assets/aurora/*`.
36
+ */
37
+ auroraAssetsHandler() {
38
+ return serveAssets({ root: this.auroraDistRoot });
39
+ }
40
+ /**
41
+ * Handler for the app's pages directory. Mount on
42
+ * `GET /_assets/pages/*`.
43
+ */
44
+ pageAssetsHandler() {
45
+ return serveAssets({ root: this.pages.root });
46
+ }
47
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * AuroraProvider — registers the AuroraManager singleton and auto-mounts
3
+ * the two asset routes the browser needs:
4
+ *
5
+ * GET /_assets/aurora/* → packages/@c9up/aurora/dist/*
6
+ * GET /_assets/pages/* → resources/pages/*
7
+ *
8
+ * Config (in `config/aurora.ts`):
9
+ *
10
+ * export default {
11
+ * pages: { root: new URL('../resources/pages', import.meta.url).pathname },
12
+ * }
13
+ *
14
+ * The duck-typed `AuroraAppContext` keeps this provider usable in any
15
+ * framework with a container — non-Ream hosts get the singleton bindings
16
+ * and skip the route auto-registration silently.
17
+ */
18
+ interface AuroraContainer {
19
+ singleton(token: unknown, factory: () => unknown): void;
20
+ resolve<T = unknown>(token: unknown): T;
21
+ }
22
+ interface AuroraConfigStore {
23
+ get<T = unknown>(key: string): T | undefined;
24
+ }
25
+ export interface AuroraAppContext {
26
+ container: AuroraContainer;
27
+ config: AuroraConfigStore;
28
+ }
29
+ export default class AuroraProvider {
30
+ protected app: AuroraAppContext;
31
+ constructor(app: AuroraAppContext);
32
+ register(): void;
33
+ boot(): Promise<void>;
34
+ start(): Promise<void>;
35
+ ready(): Promise<void>;
36
+ shutdown(): Promise<void>;
37
+ /**
38
+ * Resolve the user-supplied config:
39
+ * - relative `pages.root` (e.g. `./resources/pages`) is joined to
40
+ * the project's `appRoot` URL — same convention `modules.path`
41
+ * uses;
42
+ * - absolute paths are passed through;
43
+ * - missing config falls back to `<appRoot>/resources/pages`.
44
+ *
45
+ * `appRoot` is fetched from the container if the host registered
46
+ * one (Ream does, since v0.x — see Ignitor); other hosts get the
47
+ * `process.cwd()` fallback.
48
+ */
49
+ private resolveConfig;
50
+ private readAppRoot;
51
+ }
52
+ export {};
@@ -0,0 +1,145 @@
1
+ /**
2
+ * AuroraProvider — registers the AuroraManager singleton and auto-mounts
3
+ * the two asset routes the browser needs:
4
+ *
5
+ * GET /_assets/aurora/* → packages/@c9up/aurora/dist/*
6
+ * GET /_assets/pages/* → resources/pages/*
7
+ *
8
+ * Config (in `config/aurora.ts`):
9
+ *
10
+ * export default {
11
+ * pages: { root: new URL('../resources/pages', import.meta.url).pathname },
12
+ * }
13
+ *
14
+ * The duck-typed `AuroraAppContext` keeps this provider usable in any
15
+ * framework with a container — non-Ream hosts get the singleton bindings
16
+ * and skip the route auto-registration silently.
17
+ */
18
+ import { isAbsolute, resolve as resolvePath } from "node:path";
19
+ import { fileURLToPath } from "node:url";
20
+ import { AuroraManager } from "./AuroraManager.js";
21
+ import { auroraRoute } from "./route.js";
22
+ import { setAurora } from "./services/main.js";
23
+ import { renderToString } from "./ssr.js";
24
+ export default class AuroraProvider {
25
+ app;
26
+ constructor(app) {
27
+ this.app = app;
28
+ }
29
+ register() {
30
+ this.app.container.singleton(AuroraManager, () => {
31
+ const raw = this.app.config.get("aurora");
32
+ const config = this.resolveConfig(raw);
33
+ const manager = new AuroraManager(config);
34
+ setAurora(manager);
35
+ return manager;
36
+ });
37
+ this.app.container.singleton("aurora", () => this.app.container.resolve(AuroraManager));
38
+ // Legacy stateless bindings — kept so existing apps that
39
+ // `container.resolve('aurora.render')` still get a working
40
+ // function. New code should use the singleton.
41
+ this.app.container.singleton("aurora.renderToString", () => renderToString);
42
+ this.app.container.singleton("aurora.route", () => auroraRoute);
43
+ }
44
+ async boot() {
45
+ // Force-resolve so `setAurora` runs even if the app never
46
+ // touches the singleton from a preload.
47
+ const manager = this.app.container.resolve(AuroraManager);
48
+ setAurora(manager);
49
+ }
50
+ async start() {
51
+ // Asset routes are registered in `start()` — after preloads —
52
+ // so apps can swap aurora's pages root in a preload if they
53
+ // wanted to. Non-Ream hosts (no `@c9up/ream/services/router`)
54
+ // AND pre-`setRouter` boots (router proxy uninit) both
55
+ // silent-return; ANY other error (slug collision, AuroraManager
56
+ // crash, factory bug) propagates so real regressions surface
57
+ // with a stack instead of "the asset routes just stopped
58
+ // mounting".
59
+ // Variable specifier so tsc does not statically resolve the optional
60
+ // `@c9up/ream` peer at build time (keeps aurora agnostic /
61
+ // standalone-buildable). Resolved to the host router only at runtime
62
+ // when aurora actually runs inside Ream.
63
+ const routerSpecifier = "@c9up/ream/services/router";
64
+ let routerMod;
65
+ try {
66
+ routerMod = await import(routerSpecifier);
67
+ }
68
+ catch (err) {
69
+ if (isModuleNotFound(err))
70
+ return;
71
+ throw err;
72
+ }
73
+ const manager = this.app.container.resolve(AuroraManager);
74
+ try {
75
+ routerMod.default.get("/_assets/aurora/*", adaptHandler(manager.auroraAssetsHandler()));
76
+ routerMod.default.get("/_assets/pages/*", adaptHandler(manager.pageAssetsHandler()));
77
+ }
78
+ catch (err) {
79
+ if (isRouterProxyUninit(err))
80
+ return;
81
+ throw err;
82
+ }
83
+ }
84
+ async ready() { }
85
+ async shutdown() { }
86
+ /**
87
+ * Resolve the user-supplied config:
88
+ * - relative `pages.root` (e.g. `./resources/pages`) is joined to
89
+ * the project's `appRoot` URL — same convention `modules.path`
90
+ * uses;
91
+ * - absolute paths are passed through;
92
+ * - missing config falls back to `<appRoot>/resources/pages`.
93
+ *
94
+ * `appRoot` is fetched from the container if the host registered
95
+ * one (Ream does, since v0.x — see Ignitor); other hosts get the
96
+ * `process.cwd()` fallback.
97
+ */
98
+ resolveConfig(raw) {
99
+ const appRoot = this.readAppRoot();
100
+ const userRoot = raw?.pages?.root;
101
+ const root = typeof userRoot === "string" && userRoot.length > 0
102
+ ? isAbsolute(userRoot)
103
+ ? userRoot
104
+ : resolvePath(appRoot, userRoot)
105
+ : resolvePath(appRoot, "resources/pages");
106
+ return {
107
+ ...(raw ?? {}),
108
+ pages: { ...(raw?.pages ?? {}), root },
109
+ };
110
+ }
111
+ readAppRoot() {
112
+ try {
113
+ const raw = this.app.container.resolve("appRoot");
114
+ if (raw instanceof URL)
115
+ return fileURLToPath(raw);
116
+ if (typeof raw === "string")
117
+ return raw;
118
+ }
119
+ catch {
120
+ // Host doesn't expose appRoot — fall through.
121
+ }
122
+ return process.cwd();
123
+ }
124
+ }
125
+ /**
126
+ * Adapter: serveAssets() returns a handler that takes our duck-typed
127
+ * AssetsHttpContext. Ream's router passes its own HttpContext. The
128
+ * two are structurally compatible (request.param + response.{status,
129
+ * header, send}) but TypeScript needs the bridge made explicit.
130
+ */
131
+ function adaptHandler(handler) {
132
+ return (ctx) => handler(ctx);
133
+ }
134
+ /** Node's ERR_MODULE_NOT_FOUND surfaces on an Error subclass with `code`. */
135
+ function isModuleNotFound(err) {
136
+ if (err === null || typeof err !== "object" || !("code" in err))
137
+ return false;
138
+ const { code } = err;
139
+ return code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND";
140
+ }
141
+ /** Ream's router proxy throws this exact string before Ignitor wires it. */
142
+ function isRouterProxyUninit(err) {
143
+ return (err instanceof Error &&
144
+ err.message.includes("Router accessed before initialization"));
145
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Page registry — resolves a page NAME (e.g. `"ProjectPage"`) to its
3
+ * factory function, both server-side (dynamic import from disk) and
4
+ * client-side (URL on the asset mount).
5
+ *
6
+ * Convention: pages live in a configurable root directory, one file
7
+ * per page, default-exporting a function `(props) => TemplateResult`.
8
+ *
9
+ * resources/pages/
10
+ * ProjectPage.js → name `"ProjectPage"`
11
+ * dashboard/Home.js → name `"dashboard/Home"`
12
+ *
13
+ * Sub-paths are allowed; the last `/`-separated segment is the file
14
+ * stem (with or without the `.js` extension).
15
+ */
16
+ import type { TemplateResult } from "./types.js";
17
+ /** A page module's default export. Receives props, returns a template. */
18
+ export type PageFactory<P = unknown> = (props: P) => TemplateResult | Promise<TemplateResult>;
19
+ export interface PagesConfig {
20
+ /**
21
+ * Absolute filesystem path to the pages directory. The server
22
+ * imports `${root}/${name}.js` (or `${name}.ts` when transpiled at
23
+ * runtime by `@swc-node/register`).
24
+ */
25
+ root: string;
26
+ /**
27
+ * URL prefix the browser uses to fetch a page's compiled JS.
28
+ * Defaults to `/_assets/pages`. A name `"Foo"` maps to
29
+ * `${urlPrefix}/Foo.js`.
30
+ */
31
+ urlPrefix?: string;
32
+ /**
33
+ * File extension to append when neither the source nor the
34
+ * compiled module ships with one. Defaults to `.js` — Node ESM
35
+ * resolution requires the explicit extension, and `@swc-node`
36
+ * transparently handles `.ts` aliases that resolve back to `.js`.
37
+ */
38
+ extension?: string;
39
+ }
40
+ /**
41
+ * `Pages` is a tiny resolver — no caching, no glob, no magic. The
42
+ * server imports the module dynamically on every render so editors +
43
+ * `--watch` reloads pick up changes immediately. Apps that want a
44
+ * pre-registered map (e.g. when pages are bundled into one entry) can
45
+ * call `register()` to short-circuit the disk lookup.
46
+ */
47
+ export declare class Pages {
48
+ readonly root: string;
49
+ readonly urlPrefix: string;
50
+ readonly extension: string;
51
+ private readonly registry;
52
+ constructor(config: PagesConfig);
53
+ /**
54
+ * Pre-register a page factory under `name`, bypassing the disk
55
+ * lookup. Useful for bundled apps and tests.
56
+ *
57
+ * Generic on the props shape so callers can pass a tightly-typed
58
+ * factory (e.g. `PageFactory<{ name: string }>`) without TS rejecting
59
+ * the call due to function-parameter contravariance. The factory is
60
+ * stored as `PageFactory<unknown>` because the registry hands props
61
+ * back as `unknown` — the renderer JSON.stringifies them either way.
62
+ */
63
+ register<P>(name: string, factory: PageFactory<P>): void;
64
+ /**
65
+ * Resolve a page name to its factory function. Throws when the
66
+ * page is neither registered nor importable from disk.
67
+ *
68
+ * Path safety: `name` is rejected if it contains `..` segments or
69
+ * absolute-path markers. The joined path is also checked to live
70
+ * under `root` — defense in depth against URL-decoding tricks.
71
+ */
72
+ resolve(name: string): Promise<PageFactory>;
73
+ /**
74
+ * Browser-side URL the importmap (or a `<script src="…">`) should
75
+ * point at to fetch the same page's compiled JS.
76
+ */
77
+ urlFor(name: string): string;
78
+ }
package/dist/Pages.js ADDED
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Page registry — resolves a page NAME (e.g. `"ProjectPage"`) to its
3
+ * factory function, both server-side (dynamic import from disk) and
4
+ * client-side (URL on the asset mount).
5
+ *
6
+ * Convention: pages live in a configurable root directory, one file
7
+ * per page, default-exporting a function `(props) => TemplateResult`.
8
+ *
9
+ * resources/pages/
10
+ * ProjectPage.js → name `"ProjectPage"`
11
+ * dashboard/Home.js → name `"dashboard/Home"`
12
+ *
13
+ * Sub-paths are allowed; the last `/`-separated segment is the file
14
+ * stem (with or without the `.js` extension).
15
+ */
16
+ import { resolve as resolvePath, sep } from "node:path";
17
+ import { pathToFileURL } from "node:url";
18
+ /**
19
+ * `Pages` is a tiny resolver — no caching, no glob, no magic. The
20
+ * server imports the module dynamically on every render so editors +
21
+ * `--watch` reloads pick up changes immediately. Apps that want a
22
+ * pre-registered map (e.g. when pages are bundled into one entry) can
23
+ * call `register()` to short-circuit the disk lookup.
24
+ */
25
+ export class Pages {
26
+ root;
27
+ urlPrefix;
28
+ extension;
29
+ registry = new Map();
30
+ constructor(config) {
31
+ this.root = config.root;
32
+ this.urlPrefix = (config.urlPrefix ?? "/_assets/pages").replace(/\/$/, "");
33
+ this.extension = config.extension ?? ".js";
34
+ }
35
+ /**
36
+ * Pre-register a page factory under `name`, bypassing the disk
37
+ * lookup. Useful for bundled apps and tests.
38
+ *
39
+ * Generic on the props shape so callers can pass a tightly-typed
40
+ * factory (e.g. `PageFactory<{ name: string }>`) without TS rejecting
41
+ * the call due to function-parameter contravariance. The factory is
42
+ * stored as `PageFactory<unknown>` because the registry hands props
43
+ * back as `unknown` — the renderer JSON.stringifies them either way.
44
+ */
45
+ register(name, factory) {
46
+ this.registry.set(name, factory);
47
+ }
48
+ /**
49
+ * Resolve a page name to its factory function. Throws when the
50
+ * page is neither registered nor importable from disk.
51
+ *
52
+ * Path safety: `name` is rejected if it contains `..` segments or
53
+ * absolute-path markers. The joined path is also checked to live
54
+ * under `root` — defense in depth against URL-decoding tricks.
55
+ */
56
+ async resolve(name) {
57
+ const preset = this.registry.get(name);
58
+ if (preset)
59
+ return preset;
60
+ assertSafeName(name);
61
+ const absolute = resolvePath(this.root, `${name}${this.extension}`);
62
+ if (!absolute.startsWith(this.root + sep) && absolute !== this.root) {
63
+ throw new Error(`[aurora] page path "${name}" resolves outside the pages root`);
64
+ }
65
+ // `pathToFileURL` so Windows + ESM stay happy. Node's ESM
66
+ // loader caches modules by URL, so a stable URL would freeze
67
+ // the first-imported version of the page for the whole process
68
+ // lifetime — pages edited on disk would NOT be picked up even
69
+ // when the app runs under a file watcher. In dev mode we bust
70
+ // the URL with the file's mtime so a real change yields a new
71
+ // cache key and triggers a re-import. In production we keep
72
+ // the stable URL — page sources don't change post-deploy and
73
+ // busting per-request would leak memory (each unique URL stays
74
+ // resident in the ESM loader for the process lifetime).
75
+ const isDev = process.env.NODE_ENV !== "production";
76
+ let urlHref = pathToFileURL(absolute).href;
77
+ if (isDev) {
78
+ try {
79
+ const { statSync } = await import("node:fs");
80
+ urlHref = `${urlHref}?v=${statSync(absolute).mtimeMs}`;
81
+ }
82
+ catch {
83
+ // stat failed → fall back to stable URL; the import below
84
+ // will surface the underlying ENOENT.
85
+ }
86
+ }
87
+ let mod;
88
+ try {
89
+ mod = (await import(urlHref));
90
+ }
91
+ catch (err) {
92
+ throw new Error(`[aurora] page "${name}" not found at ${absolute} — ${err.message}`);
93
+ }
94
+ if (typeof mod.default !== "function") {
95
+ throw new Error(`[aurora] page "${name}" must default-export a factory function`);
96
+ }
97
+ return mod.default;
98
+ }
99
+ /**
100
+ * Browser-side URL the importmap (or a `<script src="…">`) should
101
+ * point at to fetch the same page's compiled JS.
102
+ */
103
+ urlFor(name) {
104
+ assertSafeName(name);
105
+ return `${this.urlPrefix}/${name}${this.extension}`;
106
+ }
107
+ }
108
+ function assertSafeName(name) {
109
+ if (name.length === 0 ||
110
+ name.startsWith("/") ||
111
+ name.startsWith("\\") ||
112
+ name.includes("..") ||
113
+ name.includes("\0")) {
114
+ throw new Error(`[aurora] illegal page name: ${JSON.stringify(name)}`);
115
+ }
116
+ }
@@ -0,0 +1,55 @@
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
+ import type { Disposer } from "./render.js";
22
+ import type { EffectCallback, TemplateResult } from "./types.js";
23
+ /**
24
+ * Build a component factory. The returned function takes props and
25
+ * produces a `TemplateResult` that can be rendered or nested inside
26
+ * another template.
27
+ *
28
+ * `component()` does NOT itself mount anything — it composes. The
29
+ * outermost `render(Component(props), container)` is what mounts.
30
+ */
31
+ export declare function component<P = Record<string, never>>(setup: (props: P) => TemplateResult): (props?: P) => TemplateResult;
32
+ interface ComponentLifecycle {
33
+ mountHooks: ReadonlyArray<EffectCallback>;
34
+ cleanups: Disposer[];
35
+ }
36
+ /**
37
+ * Internal — extract the lifecycle attachment a `component()` left on a
38
+ * TemplateResult, if any. The renderer calls this after mounting the
39
+ * fragment so onMount fires once the DOM is live, and the returned
40
+ * cleanups bubble into the outer dispose chain.
41
+ */
42
+ export declare function readComponentLifecycle(result: TemplateResult): ComponentLifecycle | undefined;
43
+ /**
44
+ * Schedule a callback to run after the component is mounted into the
45
+ * live document. Returning a function from `onMount` registers it as an
46
+ * unmount cleanup.
47
+ */
48
+ export declare function onMount(fn: EffectCallback): void;
49
+ /**
50
+ * Schedule a callback to run when the component unmounts. Equivalent
51
+ * to the cleanup return of `onMount` but available without a paired
52
+ * mount action.
53
+ */
54
+ export declare function onUnmount(fn: () => void): void;
55
+ export {};