@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
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
|
+
}
|
package/dist/Pages.d.ts
ADDED
|
@@ -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 {};
|