@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 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
@@ -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"}
@@ -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
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env -S deno run -A
2
+ export declare function main(argv: string[]): Promise<void>;
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
@@ -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"}
@@ -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
@@ -0,0 +1,2 @@
1
+ import type { FuOptions } from "./types.ts";
2
+ export declare function dev(opts: FuOptions): Promise<void>;