@janit/fu 0.0.2
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 +300 -0
- package/dist/app.d.ts +25 -0
- package/dist/app.js +80 -0
- package/dist/app.js.map +1 -0
- package/dist/build.d.ts +4 -0
- package/dist/build.js +108 -0
- package/dist/build.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +25 -0
- package/dist/cli.js.map +1 -0
- package/dist/client.d.ts +7 -0
- package/dist/client.js +59 -0
- package/dist/client.js.map +1 -0
- package/dist/dev.d.ts +2 -0
- package/dist/dev.js +167 -0
- package/dist/dev.js.map +1 -0
- package/dist/errors.d.ts +20 -0
- package/dist/errors.js +54 -0
- package/dist/errors.js.map +1 -0
- package/dist/hmr-runtime.js +112 -0
- package/dist/mod.d.ts +14 -0
- package/dist/mod.js +12 -0
- package/dist/mod.js.map +1 -0
- package/dist/plugins.d.ts +68 -0
- package/dist/plugins.js +246 -0
- package/dist/plugins.js.map +1 -0
- package/dist/render.d.ts +38 -0
- package/dist/render.js +189 -0
- package/dist/render.js.map +1 -0
- package/dist/router.d.ts +21 -0
- package/dist/router.js +48 -0
- package/dist/router.js.map +1 -0
- package/dist/types.d.ts +87 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +98 -0
- package/src/cli.ts +25 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jani Tarvainen
|
|
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,300 @@
|
|
|
1
|
+
# Fresh Urquell
|
|
2
|
+
|
|
3
|
+
A minimal islands framework: file-system routing, SSR and hydration on
|
|
4
|
+
Preact 11, built by [rolldown](https://rolldown.rs) and served by
|
|
5
|
+
[Nitro](https://nitro.build). Runs on **Deno, Node and Bun**.
|
|
6
|
+
|
|
7
|
+
No Vite, no Babel, no esbuild. The bundler, JSX transform and CSS engine are
|
|
8
|
+
all Rust.
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
core ~300 lines router + render + client + hmr runtime
|
|
12
|
+
drivers ~400 lines build + dev + shared rolldown plugins
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## What this is
|
|
16
|
+
|
|
17
|
+
Fresh Urquell is inspired by [Deno Fresh](https://usefresh.dev/), and aims to be
|
|
18
|
+
just a single pint of glue code — enough to form a functional, simple framework
|
|
19
|
+
on top of known good libraries, and no more. The interesting work already lives
|
|
20
|
+
in Preact, rolldown, Nitro and lightningcss; this only binds them together.
|
|
21
|
+
|
|
22
|
+
It is experimental and primarily just for myself. Use it accordingly.
|
|
23
|
+
|
|
24
|
+
## Installing it
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
deno add npm:@janit/fu # or: npm i @janit/fu
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Install from **npm**, not JSR. The framework hands its own runtime modules to
|
|
31
|
+
rolldown, and a bundler cannot resolve a remote module — Deno keeps JSR packages
|
|
32
|
+
as `https:` URLs, so `jsr:@janit/fu` builds nothing. npm gives a real directory
|
|
33
|
+
on disk. (The JSR copy exists for reading the API and for runtime-only use.)
|
|
34
|
+
|
|
35
|
+
The npm package ships compiled JavaScript, because Deno refuses to type-strip
|
|
36
|
+
TypeScript inside `node_modules`.
|
|
37
|
+
|
|
38
|
+
## Quick start
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
npm install # once: installs the toolchain (rolldown, nitro, lightningcss)
|
|
42
|
+
|
|
43
|
+
deno task dev # example app, dev server with HMR on 0.0.0.0:1337
|
|
44
|
+
deno task build # example app, production build
|
|
45
|
+
|
|
46
|
+
deno task todo:dev # the fu-todo example app
|
|
47
|
+
deno task todo:build
|
|
48
|
+
deno task todo:start # serve its build on :1337
|
|
49
|
+
|
|
50
|
+
deno task test # 41 unit tests
|
|
51
|
+
deno task check # types + tests + jsr publish dry-run
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Both example apps build against **this checkout's `src/`**, never a published
|
|
55
|
+
package, so the framework and the apps can be changed and tested together
|
|
56
|
+
before anything is released. `npm install` at the root is the only setup step;
|
|
57
|
+
it serves the whole repo.
|
|
58
|
+
|
|
59
|
+
Then run the build on whichever runtime you like — one artifact, three runtimes:
|
|
60
|
+
|
|
61
|
+
```sh
|
|
62
|
+
node example/.output/server/index.mjs
|
|
63
|
+
bun example/.output/server/index.mjs
|
|
64
|
+
deno run -A example/.output/server/index.mjs
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Project shape
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
app.ts middleware chain (optional)
|
|
71
|
+
state.ts your State type
|
|
72
|
+
middleware/*.ts one concern per file
|
|
73
|
+
routes/_app.tsx shell wrapping every page (optional)
|
|
74
|
+
routes/index.tsx -> /
|
|
75
|
+
routes/about.tsx -> /about
|
|
76
|
+
routes/blog/[slug].tsx -> /blog/:slug
|
|
77
|
+
routes/files/[...rest].tsx -> /files/:rest*
|
|
78
|
+
islands/Counter.tsx interactive, hydrated on the client
|
|
79
|
+
islands/counter.module.css CSS Modules, scoped
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Files under `routes/` starting with `_` are framework files, not routes.
|
|
83
|
+
|
|
84
|
+
A route exports a page component, and optionally `handlers`:
|
|
85
|
+
|
|
86
|
+
```tsx
|
|
87
|
+
export const handlers = {
|
|
88
|
+
GET: () => ({ renderedAt: new Date().toISOString() }),
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export default function About(ctx: PageContext) {
|
|
92
|
+
return <p>{(ctx.data as { renderedAt: string }).renderedAt}</p>;
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Return a `Response` from a handler to short-circuit; return anything else and
|
|
97
|
+
it lands on `ctx.data`.
|
|
98
|
+
|
|
99
|
+
## Middleware
|
|
100
|
+
|
|
101
|
+
`app.ts` composes an ordered chain. Registration order is outermost first, so
|
|
102
|
+
the first registered runs first on the way in and **unwinds last** — it has the
|
|
103
|
+
final say on headers, including on short-circuit responses from further in.
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
import { App } from "@janit/fu";
|
|
107
|
+
|
|
108
|
+
const app = new App<State>();
|
|
109
|
+
|
|
110
|
+
app.use(async (ctx) => { // outermost: unwinds last
|
|
111
|
+
const res = await ctx.next();
|
|
112
|
+
res.headers.set("X-Frame-Options", "SAMEORIGIN");
|
|
113
|
+
return res;
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
app.use((ctx) => // short-circuit: never calls next()
|
|
117
|
+
ctx.url.pathname === "/healthz"
|
|
118
|
+
? new Response("ok")
|
|
119
|
+
: ctx.next()
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
app.use((ctx) => { // populate state for everything below
|
|
123
|
+
ctx.state.tenant = resolveTenant(ctx.req);
|
|
124
|
+
return ctx.next();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
export default app;
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
A middleware either returns a `Response` (short-circuiting) or returns
|
|
131
|
+
`ctx.next()`. Await `next()` first to inspect or mutate the response. Throwing
|
|
132
|
+
propagates outward past any `await ctx.next()`.
|
|
133
|
+
|
|
134
|
+
Middleware runs **before** routing, so `ctx.params` is empty inside it — match
|
|
135
|
+
on `ctx.url.pathname`. That is what lets a middleware answer a request no route
|
|
136
|
+
exists for: redirects, `/healthz`, `/robots.txt`.
|
|
137
|
+
|
|
138
|
+
Redirects need nothing special — read state, read the URL, return a response:
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
export const redirects: Middleware<State> = (ctx) => {
|
|
142
|
+
const hit = table[ctx.url.pathname];
|
|
143
|
+
if (!hit) return ctx.next();
|
|
144
|
+
return new Response(null, { status: hit.status, headers: { location: hit.to } });
|
|
145
|
+
};
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Errors
|
|
149
|
+
|
|
150
|
+
Anything thrown by a handler, a page or a middleware is turned into a response
|
|
151
|
+
rather than reaching the server as an unhandled crash. So are 404 and 405.
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
import { HttpError } from "@janit/fu";
|
|
155
|
+
|
|
156
|
+
export const handlers = {
|
|
157
|
+
GET(ctx) {
|
|
158
|
+
if (!ctx.state.session) throw new HttpError(403, "Not yours");
|
|
159
|
+
...
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
An optional `routes/_error.tsx` renders them, receiving `ctx` like any page with
|
|
165
|
+
`ctx.error` set to `{ status, message }`. It is wrapped by `routes/_app.tsx`, so
|
|
166
|
+
error pages inherit the app's markup. Without one you get plain text.
|
|
167
|
+
|
|
168
|
+
Two things the framework does for you:
|
|
169
|
+
|
|
170
|
+
- **A 500 never renders its underlying message.** `ctx.error.message` is generic
|
|
171
|
+
for 5xx; the real error goes to `ctx.error.cause` and is logged, because a
|
|
172
|
+
thrown error's message routinely carries connection strings and file paths.
|
|
173
|
+
A `4xx` keeps whatever message you gave it.
|
|
174
|
+
- **No failure is cacheable.** Error responses carry `no-store` and `noindex`.
|
|
175
|
+
During a deploy a valid URL can 404 for a few seconds, and a shared cache
|
|
176
|
+
would pin that.
|
|
177
|
+
|
|
178
|
+
Errors are caught at the route boundary and returned *through* the middleware
|
|
179
|
+
chain, so security headers and logging middleware still see them.
|
|
180
|
+
|
|
181
|
+
## Page metadata
|
|
182
|
+
|
|
183
|
+
`ctx.head` is the channel from a route to the document `<head>`. Middleware can
|
|
184
|
+
write to it too.
|
|
185
|
+
|
|
186
|
+
```ts
|
|
187
|
+
export const handlers = {
|
|
188
|
+
GET: (ctx: PageContext<State>) => {
|
|
189
|
+
ctx.head.title = `${ctx.params.slug} — Fresh Urquell`;
|
|
190
|
+
ctx.head.canonical = `${ctx.url.origin}/blog/${ctx.params.slug}`;
|
|
191
|
+
ctx.head.jsonLd = { "@context": "https://schema.org", "@type": "BlogPosting" };
|
|
192
|
+
return null;
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
The framework renders `title`, `description`, `canonical`, `robots`, `image`,
|
|
198
|
+
`lang`, arbitrary `links` and a JSON-LD block. An optional `routes/_app.tsx`
|
|
199
|
+
wraps the body markup and can read `ctx.state`.
|
|
200
|
+
|
|
201
|
+
An island is any component under `islands/`. It renders on the server and
|
|
202
|
+
hydrates on the client:
|
|
203
|
+
|
|
204
|
+
```tsx
|
|
205
|
+
import { useSignal } from "@preact/signals";
|
|
206
|
+
import styles from "./counter.module.css";
|
|
207
|
+
|
|
208
|
+
export default function Counter({ start = 0 }) {
|
|
209
|
+
const n = useSignal(start);
|
|
210
|
+
return <button class={styles.badge} onClick={() => n.value++}>{n}</button>;
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## What works
|
|
215
|
+
|
|
216
|
+
Routing (static, dynamic and wildcard, via the web-standard `URLPattern`),
|
|
217
|
+
middleware with typed shared state, page metadata, a root shell, SSR, island
|
|
218
|
+
hydration, signals, CSS and CSS Modules with `composes`, native CSS nesting and
|
|
219
|
+
`@layer`, and HMR that preserves component state.
|
|
220
|
+
|
|
221
|
+
Editing an island hot-swaps its markup **without losing hook state**. Editing
|
|
222
|
+
CSS swaps the stylesheet in place. Editing a route rebuilds the server.
|
|
223
|
+
|
|
224
|
+
## Architecture
|
|
225
|
+
|
|
226
|
+
Two layers with a hard boundary. The core is bundler-agnostic — it consumes
|
|
227
|
+
only a route manifest and an asset list, both plain data:
|
|
228
|
+
|
|
229
|
+
```ts
|
|
230
|
+
type RouteManifest = Record<string, () => Promise<RouteModule>>;
|
|
231
|
+
type Assets = { js: { href: string }[]; css: { href: string }[] };
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Everything bundler-specific lives in the drivers (`src/build.ts`, `src/dev.ts`)
|
|
235
|
+
and the shared plugins (`src/plugins.ts`). The same core ran unchanged under a
|
|
236
|
+
Vite driver during prototyping, so swapping the build layer stays cheap.
|
|
237
|
+
|
|
238
|
+
See [the design doc](docs/design.md) for
|
|
239
|
+
the full rationale and the nine undocumented traps this implementation encodes.
|
|
240
|
+
|
|
241
|
+
## Tests
|
|
242
|
+
|
|
243
|
+
```sh
|
|
244
|
+
deno task test
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
`deno test` over `src/`, no browser needed. The suite is written against the
|
|
248
|
+
failure modes this framework actually hit, so each one guards a real regression:
|
|
249
|
+
|
|
250
|
+
| area | what it pins down |
|
|
251
|
+
|---|---|
|
|
252
|
+
| router | static beats dynamic beats wildcard; wildcards span segments; params decode |
|
|
253
|
+
| middleware | outer unwinds last and decorates inner short-circuits; `next()` twice rejects; throws propagate |
|
|
254
|
+
| render | head escaping; JSON-LD cannot close its own `<script>`; 404 vs 405; islands get a marker |
|
|
255
|
+
| errors | a 500 never leaks its message; error responses are uncacheable; a broken error page falls back |
|
|
256
|
+
| plugins | `composes` keeps every class name; CSS output changes with content; the JSX transform never touches rolldown's runtime; the H3Event unwrap |
|
|
257
|
+
|
|
258
|
+
Two of these were found by writing the suite, not before it: `ctx.next()` called
|
|
259
|
+
twice resumed at the wrong depth, and a middleware throwing synchronously
|
|
260
|
+
escaped the chain entirely instead of rejecting.
|
|
261
|
+
|
|
262
|
+
## Publishing
|
|
263
|
+
|
|
264
|
+
Two public repos ship from this private one, each as a squashed snapshot so no
|
|
265
|
+
private history leaks:
|
|
266
|
+
|
|
267
|
+
```sh
|
|
268
|
+
./scripts/publish.sh --dry-run --tag v0.0.1 "First public release" # -> janit/fu
|
|
269
|
+
./scripts/publish-todo.sh --dry-run --tag v0.0.1 "First public release" # -> janit/fu-todo
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
`scripts/publish-lib.sh` holds the mechanics: a baseline exclude list that is
|
|
273
|
+
the privacy boundary, a staged-tree backstop that refuses the push if anything
|
|
274
|
+
private reaches the index anyway, semver tagging derived from the public repo's
|
|
275
|
+
existing tags, and version stamping so the git tag and `deno.json` cannot drift.
|
|
276
|
+
`fu-todo/` is stripped from `janit/fu`; the framework specifier is rewritten
|
|
277
|
+
from `../src/mod.ts` on the way into `janit/fu-todo`.
|
|
278
|
+
|
|
279
|
+
## Known gaps
|
|
280
|
+
|
|
281
|
+
- **Adding or removing a hook** in an island breaks hook order during HMR. It
|
|
282
|
+
does not crash, but needs a manual refresh.
|
|
283
|
+
- Naming an anonymous `export default () => {}` island shifts source positions,
|
|
284
|
+
so that one module's sourcemap is dropped rather than left wrong.
|
|
285
|
+
- `urlpattern-polyfill` is bundled even on Deno and Bun, which have
|
|
286
|
+
`URLPattern` natively — ~24 kB of dead weight there (never executed).
|
|
287
|
+
- No sourcemaps in dev, no error overlay, no prerendering, no per-route CSS
|
|
288
|
+
splitting.
|
|
289
|
+
- No per-directory `_middleware.ts`. Middleware is registered programmatically
|
|
290
|
+
in `app.ts`; scope it with a path check. (Measured against a real app: nested
|
|
291
|
+
middleware had zero uses across 43 middleware.)
|
|
292
|
+
- Deliberately absent: partials, streaming SSR, nested layouts.
|
|
293
|
+
|
|
294
|
+
## Status
|
|
295
|
+
|
|
296
|
+
Alpha, and built on deliberately unstable ground: Preact 11 is a release
|
|
297
|
+
candidate, Nitro 3 is beta, and rolldown's `devMode` is marked *"not ready for
|
|
298
|
+
public usage"*. That instability is an accepted trade.
|
|
299
|
+
|
|
300
|
+
MIT.
|
package/dist/app.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Ctx, Middleware } from "./types.ts";
|
|
2
|
+
export declare function compose<S>(middleware: readonly Middleware<S>[], terminal: (ctx: Ctx<S>) => Response | Promise<Response>): (ctx: Ctx<S>) => Promise<Response>;
|
|
3
|
+
/**
|
|
4
|
+
* The application: an ordered middleware chain.
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* const app = new App<State>();
|
|
8
|
+
* app.use(securityHeaders);
|
|
9
|
+
* app.use(resolveTenant);
|
|
10
|
+
* export default app;
|
|
11
|
+
* ```
|
|
12
|
+
*
|
|
13
|
+
* Middleware runs before routing, so `ctx.params` is empty inside it — match on
|
|
14
|
+
* `ctx.url.pathname` instead. That is deliberate: it lets a middleware answer a
|
|
15
|
+
* request (a redirect, `/healthz`, `/robots.txt`) without a route existing.
|
|
16
|
+
*/
|
|
17
|
+
export declare class App<S = Record<string, unknown>> {
|
|
18
|
+
#private;
|
|
19
|
+
/** Register a middleware. Returns `this` so calls can chain. */
|
|
20
|
+
use(middleware: Middleware<S>): this;
|
|
21
|
+
/** The registered chain, in registration order. */
|
|
22
|
+
get middleware(): readonly Middleware<S>[];
|
|
23
|
+
/** Wrap a terminal handler in this app's chain. */
|
|
24
|
+
compose(terminal: (ctx: Ctx<S>) => Response | Promise<Response>): (ctx: Ctx<S>) => Promise<Response>;
|
|
25
|
+
}
|
package/dist/app.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Composes a middleware chain around a terminal handler.
|
|
3
|
+
*
|
|
4
|
+
* The chain is an onion: the first middleware registered is outermost, so it
|
|
5
|
+
* runs first on the way in and unwinds last on the way out — which is what lets
|
|
6
|
+
* it overwrite headers any inner middleware set. A middleware that returns
|
|
7
|
+
* without calling `ctx.next()` short-circuits everything beneath it.
|
|
8
|
+
*
|
|
9
|
+
* `ctx.state` and `ctx.head` are shared by reference across the whole chain, so
|
|
10
|
+
* mutations are visible both further in and further out. Each level otherwise
|
|
11
|
+
* gets its own shallow copy, which is what makes `next` per-level.
|
|
12
|
+
*/
|
|
13
|
+
function settle(fn) {
|
|
14
|
+
try {
|
|
15
|
+
return Promise.resolve(fn());
|
|
16
|
+
}
|
|
17
|
+
catch (err) {
|
|
18
|
+
return Promise.reject(err);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export function compose(middleware, terminal) {
|
|
22
|
+
return (root) => {
|
|
23
|
+
const dispatch = (i, prev) => {
|
|
24
|
+
const mw = middleware[i];
|
|
25
|
+
// A middleware or handler that throws SYNCHRONOUSLY would otherwise
|
|
26
|
+
// escape the composed function entirely rather than rejecting, and no
|
|
27
|
+
// downstream .catch would ever see it.
|
|
28
|
+
if (!mw)
|
|
29
|
+
return settle(() => terminal(prev));
|
|
30
|
+
// Each level gets its own ctx so that `next` is bound to THAT level.
|
|
31
|
+
// A single shared object cannot work: the inner dispatch would overwrite
|
|
32
|
+
// `ctx.next`, and an outer middleware calling it a second time would
|
|
33
|
+
// silently resume at the wrong depth instead of failing.
|
|
34
|
+
let called = false;
|
|
35
|
+
const ctx = {
|
|
36
|
+
...prev,
|
|
37
|
+
next: () => {
|
|
38
|
+
if (called) {
|
|
39
|
+
return Promise.reject(new Error("fu: ctx.next() called more than once in one middleware"));
|
|
40
|
+
}
|
|
41
|
+
called = true;
|
|
42
|
+
return dispatch(i + 1, ctx);
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
return settle(() => mw(ctx));
|
|
46
|
+
};
|
|
47
|
+
return dispatch(0, root);
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The application: an ordered middleware chain.
|
|
52
|
+
*
|
|
53
|
+
* ```ts
|
|
54
|
+
* const app = new App<State>();
|
|
55
|
+
* app.use(securityHeaders);
|
|
56
|
+
* app.use(resolveTenant);
|
|
57
|
+
* export default app;
|
|
58
|
+
* ```
|
|
59
|
+
*
|
|
60
|
+
* Middleware runs before routing, so `ctx.params` is empty inside it — match on
|
|
61
|
+
* `ctx.url.pathname` instead. That is deliberate: it lets a middleware answer a
|
|
62
|
+
* request (a redirect, `/healthz`, `/robots.txt`) without a route existing.
|
|
63
|
+
*/
|
|
64
|
+
export class App {
|
|
65
|
+
#middleware = [];
|
|
66
|
+
/** Register a middleware. Returns `this` so calls can chain. */
|
|
67
|
+
use(middleware) {
|
|
68
|
+
this.#middleware.push(middleware);
|
|
69
|
+
return this;
|
|
70
|
+
}
|
|
71
|
+
/** The registered chain, in registration order. */
|
|
72
|
+
get middleware() {
|
|
73
|
+
return this.#middleware;
|
|
74
|
+
}
|
|
75
|
+
/** Wrap a terminal handler in this app's chain. */
|
|
76
|
+
compose(terminal) {
|
|
77
|
+
return compose(this.#middleware, terminal);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=app.js.map
|
package/dist/app.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"app.js","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;GAWG;AACH,SAAS,MAAM,CAAC,EAAsC;IACpD,IAAI,CAAC;QACH,OAAO,OAAO,CAAC,OAAO,CAAC,EAAE,EAAE,CAAC,CAAC;IAC/B,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IAC7B,CAAC;AACH,CAAC;AAED,MAAM,UAAU,OAAO,CACrB,UAAoC,EACpC,QAAuD;IAEvD,OAAO,CAAC,IAAY,EAAqB,EAAE;QACzC,MAAM,QAAQ,GAAG,CAAC,CAAS,EAAE,IAAY,EAAqB,EAAE;YAC9D,MAAM,EAAE,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC;YACzB,oEAAoE;YACpE,sEAAsE;YACtE,uCAAuC;YACvC,IAAI,CAAC,EAAE;gBAAE,OAAO,MAAM,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC;YAE7C,qEAAqE;YACrE,yEAAyE;YACzE,qEAAqE;YACrE,yDAAyD;YACzD,IAAI,MAAM,GAAG,KAAK,CAAC;YACnB,MAAM,GAAG,GAAW;gBAClB,GAAG,IAAI;gBACP,IAAI,EAAE,GAAG,EAAE;oBACT,IAAI,MAAM,EAAE,CAAC;wBACX,OAAO,OAAO,CAAC,MAAM,CACnB,IAAI,KAAK,CAAC,wDAAwD,CAAC,CACpE,CAAC;oBACJ,CAAC;oBACD,MAAM,GAAG,IAAI,CAAC;oBACd,OAAO,QAAQ,CAAC,CAAC,GAAG,CAAC,EAAE,GAAG,CAAC,CAAC;gBAC9B,CAAC;aACF,CAAC;YACF,OAAO,MAAM,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;QAC/B,CAAC,CAAC;QACF,OAAO,QAAQ,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;IAC3B,CAAC,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,OAAO,GAAG;IACL,WAAW,GAAoB,EAAE,CAAC;IAE3C,gEAAgE;IAChE,GAAG,CAAC,UAAyB;QAC3B,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QAClC,OAAO,IAAI,CAAC;IACd,CAAC;IAED,mDAAmD;IACnD,IAAI,UAAU;QACZ,OAAO,IAAI,CAAC,WAAW,CAAC;IAC1B,CAAC;IAED,mDAAmD;IACnD,OAAO,CACL,QAAuD;QAEvD,OAAO,OAAO,CAAC,IAAI,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC;IAC7C,CAAC;CACF"}
|
package/dist/build.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { FuOptions } from "./types.ts";
|
|
2
|
+
export declare function build(opts: FuOptions): Promise<void>;
|
|
3
|
+
/** Concatenate collected stylesheets into one hashed file. Returns its name. */
|
|
4
|
+
export declare function writeSheets(dir: string, sheets: Map<string, string>): string | null;
|
package/dist/build.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// Production driver: rolldown builds the client, nitro builds the server.
|
|
2
|
+
import { rolldown } from "rolldown";
|
|
3
|
+
import { build as nitroBuild, copyPublicAssets, createNitro } from "nitro/builder";
|
|
4
|
+
import * as fs from "node:fs";
|
|
5
|
+
import * as path from "node:path";
|
|
6
|
+
import { bootModule, css, jsx, optional, ssrModule, virtual, walk } from "./plugins.js";
|
|
7
|
+
/**
|
|
8
|
+
* Directory holding the framework's own runtime modules, whose paths are handed
|
|
9
|
+
* to rolldown. `import.meta.dirname` is undefined when this module is loaded
|
|
10
|
+
* from a remote URL, and a bundler cannot fetch `https:` modules anyway — so
|
|
11
|
+
* fail with the reason rather than a TypeError three frames later.
|
|
12
|
+
*/
|
|
13
|
+
const HERE = import.meta.dirname ?? remoteFrameworkError();
|
|
14
|
+
/**
|
|
15
|
+
* Extension of the framework's own runtime modules: `.ts` when running from
|
|
16
|
+
* source (this repo, or JSR), `.js` when running from the compiled npm build.
|
|
17
|
+
*/
|
|
18
|
+
const EXT = import.meta.url.endsWith(".js") ? ".js" : ".ts";
|
|
19
|
+
function remoteFrameworkError() {
|
|
20
|
+
throw new Error("fu: the framework is loaded from a remote URL (" + import.meta.url + "), " +
|
|
21
|
+
"so its runtime modules cannot be handed to the bundler. Install it " +
|
|
22
|
+
"instead — `npm:@janit/fu` in a Deno import map, or `npm i @janit/fu` — " +
|
|
23
|
+
"so it resolves to a real directory.");
|
|
24
|
+
}
|
|
25
|
+
export async function build(opts) {
|
|
26
|
+
const root = path.resolve(opts.root);
|
|
27
|
+
const outDir = opts.outDir ? path.resolve(opts.outDir) : path.join(root, ".output");
|
|
28
|
+
const clientDir = path.join(root, "dist/client");
|
|
29
|
+
const genDir = path.join(root, ".fu");
|
|
30
|
+
const routeFiles = walk(root, path.join(root, "routes"));
|
|
31
|
+
const islandFiles = walk(root, path.join(root, "islands"));
|
|
32
|
+
// ---- client ----
|
|
33
|
+
fs.rmSync(clientDir, { recursive: true, force: true });
|
|
34
|
+
fs.mkdirSync(clientDir, { recursive: true });
|
|
35
|
+
const sheets = new Map();
|
|
36
|
+
const bundle = await rolldown({
|
|
37
|
+
input: { boot: "fu:boot" },
|
|
38
|
+
plugins: [
|
|
39
|
+
virtual({ "fu:boot": bootModule(path.join(HERE, `client${EXT}`), islandFiles, root) }),
|
|
40
|
+
css(sheets),
|
|
41
|
+
jsx(),
|
|
42
|
+
],
|
|
43
|
+
platform: "browser",
|
|
44
|
+
// rolldown types modules by extension and refuses to bundle CSS; css()
|
|
45
|
+
// has already replaced their contents with JS.
|
|
46
|
+
moduleTypes: { ".css": "js" },
|
|
47
|
+
});
|
|
48
|
+
const result = await bundle.write({
|
|
49
|
+
dir: clientDir,
|
|
50
|
+
format: "esm",
|
|
51
|
+
entryFileNames: "[name]-[hash].js",
|
|
52
|
+
chunkFileNames: "[name]-[hash].js",
|
|
53
|
+
});
|
|
54
|
+
await bundle.close();
|
|
55
|
+
const entry = result.output.find((o) => o.type === "chunk" && o.isEntry);
|
|
56
|
+
if (!entry)
|
|
57
|
+
throw new Error("fu: client build produced no entry chunk");
|
|
58
|
+
const cssHref = writeSheets(clientDir, sheets);
|
|
59
|
+
// ---- server ----
|
|
60
|
+
fs.rmSync(genDir, { recursive: true, force: true });
|
|
61
|
+
fs.mkdirSync(genDir, { recursive: true });
|
|
62
|
+
const assets = {
|
|
63
|
+
js: [{ href: "/" + entry.fileName }],
|
|
64
|
+
css: cssHref ? [{ href: "/" + cssHref }] : [],
|
|
65
|
+
};
|
|
66
|
+
// nitro path-resolves `handlers[].handler`, so a virtual id would not survive
|
|
67
|
+
// its routing codegen — generate a real file.
|
|
68
|
+
const ssrEntry = path.join(genDir, "ssr.ts");
|
|
69
|
+
fs.writeFileSync(ssrEntry, ssrModule({
|
|
70
|
+
renderPath: path.join(HERE, `render${EXT}`),
|
|
71
|
+
routeFiles,
|
|
72
|
+
root,
|
|
73
|
+
assets: JSON.stringify(assets),
|
|
74
|
+
appPath: optional(root, "app.ts", "app.tsx"),
|
|
75
|
+
shellPath: optional(root, "routes/_app.tsx"),
|
|
76
|
+
errorPath: optional(root, "routes/_error.tsx"),
|
|
77
|
+
}));
|
|
78
|
+
const nitro = await createNitro({
|
|
79
|
+
rootDir: root,
|
|
80
|
+
// Point nitro's scanner at the generated dir, never the project root, or it
|
|
81
|
+
// claims `routes/` as its own server routes and shadows our catch-all.
|
|
82
|
+
serverDir: genDir,
|
|
83
|
+
scanDirs: [],
|
|
84
|
+
output: { dir: outDir },
|
|
85
|
+
publicAssets: [{ dir: clientDir, baseURL: "/" }],
|
|
86
|
+
handlers: [{ route: "/**", handler: ssrEntry, format: "web", lazy: false }],
|
|
87
|
+
rollupConfig: {
|
|
88
|
+
// Islands are imported on the server too, so SSR needs the same handling.
|
|
89
|
+
plugins: [css(new Map()), jsx({ stampIslands: true })],
|
|
90
|
+
moduleTypes: { ".css": "js" },
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
await copyPublicAssets(nitro);
|
|
94
|
+
await nitroBuild(nitro);
|
|
95
|
+
}
|
|
96
|
+
/** Concatenate collected stylesheets into one hashed file. Returns its name. */
|
|
97
|
+
export function writeSheets(dir, sheets) {
|
|
98
|
+
if (sheets.size === 0)
|
|
99
|
+
return null;
|
|
100
|
+
const merged = [...sheets.values()].join("\n");
|
|
101
|
+
let h = 7;
|
|
102
|
+
for (let i = 0; i < merged.length; i++)
|
|
103
|
+
h = (Math.imul(h, 31) + merged.charCodeAt(i)) >>> 0;
|
|
104
|
+
const name = `style-${h.toString(36)}.css`;
|
|
105
|
+
fs.writeFileSync(path.join(dir, name), merged);
|
|
106
|
+
return name;
|
|
107
|
+
}
|
|
108
|
+
//# sourceMappingURL=build.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"build.js","sourceRoot":"","sources":["../src/build.ts"],"names":[],"mappings":"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"}
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env -S deno run -A
|
|
2
|
+
import { build } from "./build.js";
|
|
3
|
+
import { dev } from "./dev.js";
|
|
4
|
+
export async function main(argv) {
|
|
5
|
+
const [cmd = "dev", ...rest] = argv;
|
|
6
|
+
const portArg = rest.find((a) => a.startsWith("--port="));
|
|
7
|
+
const hostArg = rest.find((a) => a.startsWith("--host="));
|
|
8
|
+
const rootArg = rest.find((a) => !a.startsWith("-"));
|
|
9
|
+
const opts = {
|
|
10
|
+
root: rootArg ?? Deno?.cwd?.() ?? process.cwd(),
|
|
11
|
+
port: portArg ? Number(portArg.slice("--port=".length)) : undefined,
|
|
12
|
+
hostname: hostArg ? hostArg.slice("--host=".length) : undefined,
|
|
13
|
+
};
|
|
14
|
+
if (cmd === "build")
|
|
15
|
+
await build(opts);
|
|
16
|
+
else if (cmd === "dev")
|
|
17
|
+
await dev(opts);
|
|
18
|
+
else {
|
|
19
|
+
console.error(`fu: unknown command "${cmd}" (expected dev|build)`);
|
|
20
|
+
throw new Error(`unknown command: ${cmd}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
if (import.meta.main)
|
|
24
|
+
await main(globalThis.process?.argv.slice(2) ?? []);
|
|
25
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAE/B,MAAM,CAAC,KAAK,UAAU,IAAI,CAAC,IAAc;IACvC,MAAM,CAAC,GAAG,GAAG,KAAK,EAAE,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC;IACpC,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC,CAAC;IAC1D,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC,CAAC;IAC1D,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC;IACrD,MAAM,IAAI,GAAG;QACX,IAAI,EAAE,OAAO,IAAI,IAAI,EAAE,GAAG,EAAE,EAAE,IAAI,OAAO,CAAC,GAAG,EAAE;QAC/C,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS;QACnE,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,SAAS;KAChE,CAAC;IACF,IAAI,GAAG,KAAK,OAAO;QAAE,MAAM,KAAK,CAAC,IAAI,CAAC,CAAC;SAClC,IAAI,GAAG,KAAK,KAAK;QAAE,MAAM,GAAG,CAAC,IAAI,CAAC,CAAC;SACnC,CAAC;QACJ,OAAO,CAAC,KAAK,CAAC,wBAAwB,GAAG,wBAAwB,CAAC,CAAC;QACnE,MAAM,IAAI,KAAK,CAAC,oBAAoB,GAAG,EAAE,CAAC,CAAC;IAC7C,CAAC;AACH,CAAC;AAID,IAAI,OAAO,IAAI,CAAC,IAAI;IAAE,MAAM,IAAI,CAAC,UAAU,CAAC,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC"}
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type FunctionComponent } from "preact";
|
|
2
|
+
type Props = Record<string, unknown>;
|
|
3
|
+
/** Hydrate every `[data-island]` marker the server rendered. */
|
|
4
|
+
export declare function hydrateIslands(manifest: Record<string, () => Promise<{
|
|
5
|
+
default: FunctionComponent<Props>;
|
|
6
|
+
}>>): Promise<void>;
|
|
7
|
+
export {};
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { h, hydrate, render } from "preact";
|
|
2
|
+
const mounted = new Map();
|
|
3
|
+
const proxies = new Map();
|
|
4
|
+
let hmrVersion = 0;
|
|
5
|
+
/**
|
|
6
|
+
* Wrap an island in a component whose *identity* never changes. Preact keys
|
|
7
|
+
* hook state to the component type, so swapping the implementation behind a
|
|
8
|
+
* stable wrapper lets the instance — and therefore its signals — survive HMR
|
|
9
|
+
* without a Babel-based fast-refresh transform.
|
|
10
|
+
*
|
|
11
|
+
* Adding or removing a hook still changes hook order; that case needs a reload.
|
|
12
|
+
*/
|
|
13
|
+
function proxyFor(key, impl) {
|
|
14
|
+
const existing = proxies.get(key);
|
|
15
|
+
if (existing) {
|
|
16
|
+
existing.impl.current = impl;
|
|
17
|
+
return existing;
|
|
18
|
+
}
|
|
19
|
+
const holder = { current: impl };
|
|
20
|
+
// `__hmr` is a cache-buster: bumping it makes Preact re-render this instance
|
|
21
|
+
// (same type, so hooks survive) and pick up the swapped implementation. It is
|
|
22
|
+
// stripped before reaching the island.
|
|
23
|
+
const Component = ({ __hmr: _ignored, ...props }) => holder.current(props);
|
|
24
|
+
const entry = { Component, impl: holder };
|
|
25
|
+
proxies.set(key, entry);
|
|
26
|
+
return entry;
|
|
27
|
+
}
|
|
28
|
+
/** Hydrate every `[data-island]` marker the server rendered. */
|
|
29
|
+
export async function hydrateIslands(manifest) {
|
|
30
|
+
for (const el of document.querySelectorAll("[data-island]")) {
|
|
31
|
+
const key = el.dataset.island;
|
|
32
|
+
const loader = manifest[key];
|
|
33
|
+
if (!loader) {
|
|
34
|
+
console.warn("[fu] no island module for", key);
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
const mod = await loader();
|
|
38
|
+
const props = JSON.parse(el.dataset.props || "{}");
|
|
39
|
+
hydrate(h(proxyFor(key, mod.default).Component, props), el);
|
|
40
|
+
const list = mounted.get(key) ?? [];
|
|
41
|
+
list.push({ el, props });
|
|
42
|
+
mounted.set(key, list);
|
|
43
|
+
}
|
|
44
|
+
installHmrHook();
|
|
45
|
+
}
|
|
46
|
+
/** Exposed for the HMR runtime to call after a module is hot-swapped. */
|
|
47
|
+
function installHmrHook() {
|
|
48
|
+
globalThis.__fu_hmr__ = (key, mod) => {
|
|
49
|
+
const list = mounted.get(key);
|
|
50
|
+
if (!list?.length)
|
|
51
|
+
return;
|
|
52
|
+
const { Component } = proxyFor(key, mod.default);
|
|
53
|
+
const stamp = ++hmrVersion;
|
|
54
|
+
for (const { el, props } of list)
|
|
55
|
+
render(h(Component, { ...props, __hmr: stamp }), el);
|
|
56
|
+
console.debug(`[fu] hmr: re-rendered ${list.length}x ${key}`);
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
|
package/dist/dev.d.ts
ADDED