@janit/fu 0.0.4 → 0.2.1

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/README.md CHANGED
@@ -8,9 +8,10 @@ No Vite, no Babel, no esbuild. The bundler, JSX transform and CSS engine are
8
8
  all Rust.
9
9
 
10
10
  ```
11
- core ~300 lines router + render + client + hmr runtime
12
- drivers ~400 lines build + dev + shared rolldown plugins
11
+ core ~560 lines router + render + client + hmr runtime
12
+ drivers ~490 lines build + dev + shared driver glue + rolldown plugins
13
13
  ```
14
+ (code lines, comments and blanks excluded)
14
15
 
15
16
  ## What this is
16
17
 
@@ -27,10 +28,10 @@ It is experimental and primarily just for myself. Use it accordingly.
27
28
  deno add npm:@janit/fu # or: npm i @janit/fu
28
29
  ```
29
30
 
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.)
31
+ npm is the only channel. The framework hands its own runtime modules to
32
+ rolldown, so it must be a real directory on disk, which an npm install is. It
33
+ was on JSR up to 0.0.4 and was withdrawn from there: Deno keeps JSR packages
34
+ as remote `https:` modules, and a bundler cannot fetch one.
34
35
 
35
36
  The npm package ships compiled JavaScript, because Deno refuses to type-strip
36
37
  TypeScript inside `node_modules`.
@@ -47,15 +48,19 @@ deno task todo:dev # the fu-todo example app
47
48
  deno task todo:build
48
49
  deno task todo:start # serve its build on :1337
49
50
 
50
- deno task test # 41 unit tests
51
- deno task check # types + tests + jsr publish dry-run
52
- deno task check:pkg # build and serve a real app from the packed npm artefact
51
+ deno task test # unit tests, no browser
52
+ deno task check # format, lint, types (framework and both apps), tests
53
+ deno task check:pkg # build, serve and dev-serve a real app from the packed npm artefact
53
54
  ```
54
55
 
55
56
  Both example apps build against **this checkout's `src/`**, never a published
56
57
  package, so the framework and the apps can be changed and tested together
57
- before anything is released. `npm install` at the root is the only setup step;
58
- it serves the whole repo.
58
+ before anything is released. The drivers make that hold: they alias the
59
+ framework's own package name to the runtime copy they are running from, so an
60
+ app's `import { App } from "@janit/fu"` cannot drift to a stale
61
+ `dist/` through Node's package self-reference. The apps are Deno workspace
62
+ members, so `deno check` sees the same mapping. `npm install` at the root is
63
+ the only setup step; it serves the whole repo.
59
64
 
60
65
  Then run the build on whichever runtime you like — one artifact, three runtimes:
61
66
 
@@ -146,6 +151,18 @@ export const redirects: Middleware<State> = (ctx) => {
146
151
  };
147
152
  ```
148
153
 
154
+ Nothing in a request says whether `fu dev` or a build is serving it, and the
155
+ same middleware runs in both. So the dev server sets `FU_DEV=1` in its own
156
+ environment before it starts the app, and a built server never does. Read it
157
+ for anything that must be laxer in dev — a Content-Security-Policy, say, which
158
+ has to name the HMR socket on `port + 1`, because a different port is a
159
+ different origin:
160
+
161
+ ```ts
162
+ const dev = Deno.env.get("FU_DEV") === "1"; // process.env.FU_DEV on Node and Bun
163
+ const connectSrc = dev ? "connect-src 'self' ws://localhost:*" : "connect-src 'self'";
164
+ ```
165
+
149
166
  ## Errors
150
167
 
151
168
  Anything thrown by a handler, a page or a middleware is turned into a response
@@ -176,8 +193,13 @@ Two things the framework does for you:
176
193
  During a deploy a valid URL can 404 for a few seconds, and a shared cache
177
194
  would pin that.
178
195
 
179
- Errors are caught at the route boundary and returned *through* the middleware
180
- chain, so security headers and logging middleware still see them.
196
+ Errors from a handler or a page are caught at the route boundary and returned
197
+ *through* the middleware chain, so security headers and logging middleware still
198
+ see them. A middleware that throws is different: the throw propagates outward
199
+ past every `await ctx.next()`, so an outer middleware can catch it, and only what
200
+ nobody catches becomes an error response — at the very top, after the chain has
201
+ unwound, without the headers the middleware would have set. To refuse a request
202
+ from middleware, return the response rather than throwing.
181
203
 
182
204
  ## Page metadata
183
205
 
@@ -232,9 +254,17 @@ type RouteManifest = Record<string, () => Promise<RouteModule>>;
232
254
  type Assets = { js: { href: string }[]; css: { href: string }[] };
233
255
  ```
234
256
 
235
- Everything bundler-specific lives in the drivers (`src/build.ts`, `src/dev.ts`)
236
- and the shared plugins (`src/plugins.ts`). The same core ran unchanged under a
237
- Vite driver during prototyping, so swapping the build layer stays cheap.
257
+ Everything bundler-specific lives in the drivers (`src/build.ts`, `src/dev.ts`),
258
+ the glue they share (`src/driver.ts`: locating the runtime modules, scanning a
259
+ project, the rolldown and nitro option sets) and the rolldown plugins
260
+ (`src/plugins.ts`). The same core ran unchanged under a Vite driver during
261
+ prototyping, so swapping the build layer stays cheap.
262
+
263
+ Routing is on `URLPattern`, but `exec` costs microseconds per route on Deno,
264
+ so it only runs when it can matter: a route with no pattern syntax is matched
265
+ by string equality against its canonical pathname, and a pattern whose
266
+ segment count cannot fit the path is skipped. A static hit is ~65 ns and a
267
+ dynamic one ~1.6 µs with 16 routes, down from 28 µs and 57 µs.
238
268
 
239
269
  See [the design doc](docs/design.md) for
240
270
  the full rationale and the nine undocumented traps this implementation encodes.
@@ -245,16 +275,18 @@ the full rationale and the nine undocumented traps this implementation encodes.
245
275
  deno task test
246
276
  ```
247
277
 
248
- `deno test` over `src/`, no browser needed. The suite is written against the
278
+ `deno test` over the whole workspace, no browser needed. The framework suite is written against the
249
279
  failure modes this framework actually hit, so each one guards a real regression:
250
280
 
251
281
  | area | what it pins down |
252
282
  |---|---|
253
- | router | static beats dynamic beats wildcard; wildcards span segments; params decode |
283
+ | router | static beats dynamic beats wildcard; wildcards span segments and match their own base path; params decode, but an escaped `/`, NUL, backslash or dot segment never reaches one; a non-ASCII static route matches its percent-encoded request |
254
284
  | middleware | outer unwinds last and decorates inner short-circuits; `next()` twice rejects; throws propagate |
255
285
  | render | head escaping; JSON-LD cannot close its own `<script>`; 404 vs 405; islands get a marker |
256
286
  | errors | a 500 never leaks its message; error responses are uncacheable; a broken error page falls back |
257
- | plugins | `composes` keeps every class name; CSS output changes with content; the JSX transform never touches rolldown's runtime; the H3Event unwrap |
287
+ | plugins | `composes` keeps every class name; CSS output changes with content; the JSX transform never touches rolldown's runtime; the package self-alias |
288
+ | driver | the server entry imports only the optional files that exist; the H3Event unwrap |
289
+ | dev | the HMR socket admits only the dev server's own pages, not another site or a rebound name |
258
290
 
259
291
  Two of these were found by writing the suite, not before it: `ctx.next()` called
260
292
  twice resumed at the wrong depth, and a middleware throwing synchronously
@@ -274,8 +306,11 @@ private history leaks:
274
306
  the privacy boundary, a staged-tree backstop that refuses the push if anything
275
307
  private reaches the index anyway, semver tagging derived from the public repo's
276
308
  existing tags, and version stamping so the git tag and `deno.json` cannot drift.
277
- `fu-todo/` is stripped from `janit/fu`; the framework specifier is rewritten
278
- from `../src/mod.ts` on the way into `janit/fu-todo`.
309
+ `fu-todo/` is stripped from `janit/fu`, and from its Deno workspace; the
310
+ framework specifier is rewritten from `../src/mod.ts` on the way into
311
+ `janit/fu-todo`, which also gets its own `nodeModulesDir` since it is no
312
+ longer a workspace member there. Both flows can be rehearsed against a local
313
+ bare repository by overriding `PUBLIC_REPO`.
279
314
 
280
315
  ## Known gaps
281
316
 
package/dist/app.d.ts CHANGED
@@ -20,6 +20,4 @@ export declare class App<S = Record<string, unknown>> {
20
20
  use(middleware: Middleware<S>): this;
21
21
  /** The registered chain, in registration order. */
22
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
23
  }
package/dist/app.js CHANGED
@@ -72,9 +72,5 @@ export class App {
72
72
  get middleware() {
73
73
  return this.#middleware;
74
74
  }
75
- /** Wrap a terminal handler in this app's chain. */
76
- compose(terminal) {
77
- return compose(this.#middleware, terminal);
78
- }
79
75
  }
80
76
  //# sourceMappingURL=app.js.map
package/dist/app.js.map CHANGED
@@ -1 +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"}
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;CACF"}
package/dist/build.d.ts CHANGED
@@ -1,4 +1,2 @@
1
1
  import type { FuOptions } from "./types.js";
2
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 CHANGED
@@ -1,52 +1,17 @@
1
1
  // Production driver: rolldown builds the client, nitro builds the server.
2
2
  import { rolldown } from "rolldown";
3
3
  import { build as nitroBuild, copyPublicAssets, createNitro } from "nitro/builder";
4
- import * as fs from "node:fs";
5
4
  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
- }
5
+ import { clientInput, emptyDir, nitroOptions, scanProject, writeSheets, writeSsrEntry, } from "./driver.js";
25
6
  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"));
7
+ const project = scanProject(opts.root);
8
+ const outDir = opts.outDir ? path.resolve(opts.outDir) : path.join(project.root, ".output");
32
9
  // ---- client ----
33
- fs.rmSync(clientDir, { recursive: true, force: true });
34
- fs.mkdirSync(clientDir, { recursive: true });
10
+ emptyDir(project.clientDir);
35
11
  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
- });
12
+ const bundle = await rolldown(clientInput(project, sheets));
48
13
  const result = await bundle.write({
49
- dir: clientDir,
14
+ dir: project.clientDir,
50
15
  format: "esm",
51
16
  entryFileNames: "[name]-[hash].js",
52
17
  chunkFileNames: "[name]-[hash].js",
@@ -55,54 +20,14 @@ export async function build(opts) {
55
20
  const entry = result.output.find((o) => o.type === "chunk" && o.isEntry);
56
21
  if (!entry)
57
22
  throw new Error("fu: client build produced no entry chunk");
58
- const cssHref = writeSheets(clientDir, sheets);
23
+ const cssFile = writeSheets(project.clientDir, sheets);
59
24
  // ---- server ----
60
- fs.rmSync(genDir, { recursive: true, force: true });
61
- fs.mkdirSync(genDir, { recursive: true });
62
- const assets = {
25
+ const ssrEntry = writeSsrEntry(project, {
63
26
  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
- },
27
+ css: cssFile ? [{ href: "/" + cssFile }] : [],
92
28
  });
29
+ const nitro = await createNitro({ ...nitroOptions(project, ssrEntry), output: { dir: outDir } });
93
30
  await copyPublicAssets(nitro);
94
31
  await nitroBuild(nitro);
95
32
  }
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
33
  //# sourceMappingURL=build.js.map
package/dist/build.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"build.js","sourceRoot":"","sources":["../src/build.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"build.js","sourceRoot":"","sources":["../src/build.ts"],"names":[],"mappings":"AAAA,0EAA0E;AAC1E,OAAO,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AACpC,OAAO,EAAE,KAAK,IAAI,UAAU,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AACnF,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAClC,OAAO,EACL,WAAW,EACX,QAAQ,EACR,YAAY,EACZ,WAAW,EACX,WAAW,EACX,aAAa,GACd,MAAM,aAAa,CAAC;AAGrB,MAAM,CAAC,KAAK,UAAU,KAAK,CAAC,IAAe;IACzC,MAAM,OAAO,GAAG,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACvC,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;IAE5F,mBAAmB;IACnB,QAAQ,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IAC5B,MAAM,MAAM,GAAG,IAAI,GAAG,EAAkB,CAAC;IACzC,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,WAAW,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC;IAC5D,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC;QAChC,GAAG,EAAE,OAAO,CAAC,SAAS;QACtB,MAAM,EAAE,KAAK;QACb,cAAc,EAAE,kBAAkB;QAClC,cAAc,EAAE,kBAAkB;KACnC,CAAC,CAAC;IACH,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;IAErB,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,OAAO,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC;IACzE,IAAI,CAAC,KAAK;QAAE,MAAM,IAAI,KAAK,CAAC,0CAA0C,CAAC,CAAC;IACxE,MAAM,OAAO,GAAG,WAAW,CAAC,OAAO,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;IAEvD,mBAAmB;IACnB,MAAM,QAAQ,GAAG,aAAa,CAAC,OAAO,EAAE;QACtC,EAAE,EAAE,CAAC,EAAE,IAAI,EAAE,GAAG,GAAG,KAAK,CAAC,QAAQ,EAAE,CAAC;QACpC,GAAG,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,GAAG,GAAG,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE;KAC9C,CAAC,CAAC;IACH,MAAM,KAAK,GAAG,MAAM,WAAW,CAAC,EAAE,GAAG,YAAY,CAAC,OAAO,EAAE,QAAQ,CAAC,EAAE,MAAM,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE,EAAE,CAAC,CAAC;IACjG,MAAM,gBAAgB,CAAC,KAAK,CAAC,CAAC;IAC9B,MAAM,UAAU,CAAC,KAAK,CAAC,CAAC;AAC1B,CAAC"}
package/dist/cli.js CHANGED
@@ -1,15 +1,15 @@
1
1
  #!/usr/bin/env -S deno run -A
2
+ import process from "node:process";
2
3
  import { build } from "./build.js";
3
4
  import { dev } from "./dev.js";
4
5
  export async function main(argv) {
5
6
  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("-"));
7
+ const flag = (name) => rest.find((a) => a.startsWith(`--${name}=`))?.slice(name.length + 3);
8
+ const port = flag("port");
9
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,
10
+ root: rest.find((a) => !a.startsWith("-")) ?? process.cwd(),
11
+ port: port ? Number(port) : undefined,
12
+ hostname: flag("host"),
13
13
  };
14
14
  if (cmd === "build")
15
15
  await build(opts);
@@ -21,5 +21,5 @@ export async function main(argv) {
21
21
  }
22
22
  }
23
23
  if (import.meta.main)
24
- await main(globalThis.process?.argv.slice(2) ?? []);
24
+ await main(process.argv.slice(2));
25
25
  //# sourceMappingURL=cli.js.map
package/dist/cli.js.map CHANGED
@@ -1 +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"}
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,OAAO,OAAO,MAAM,cAAc,CAAC;AACnC,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,IAAI,GAAG,CAAC,IAAY,EAAE,EAAE,CAC5B,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,KAAK,IAAI,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IACvE,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC;IAC1B,MAAM,IAAI,GAAG;QACX,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,IAAI,OAAO,CAAC,GAAG,EAAE;QAC3D,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS;QACrC,QAAQ,EAAE,IAAI,CAAC,MAAM,CAAC;KACvB,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;AAED,IAAI,OAAO,IAAI,CAAC,IAAI;IAAE,MAAM,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC"}
package/dist/client.d.ts CHANGED
@@ -1,6 +1,12 @@
1
1
  import { type FunctionComponent } from "preact";
2
2
  type Props = Record<string, unknown>;
3
- /** Hydrate every `[data-island]` marker the server rendered. */
3
+ /**
4
+ * Hydrate every `[data-island]` marker the server rendered.
5
+ *
6
+ * Every distinct island module is fetched at once, then the markers are
7
+ * hydrated in document order; awaiting each in turn would serialise one
8
+ * network round-trip per island.
9
+ */
4
10
  export declare function hydrateIslands(manifest: Record<string, () => Promise<{
5
11
  default: FunctionComponent<Props>;
6
12
  }>>): Promise<void>;
package/dist/client.js CHANGED
@@ -25,18 +25,29 @@ function proxyFor(key, impl) {
25
25
  proxies.set(key, entry);
26
26
  return entry;
27
27
  }
28
- /** Hydrate every `[data-island]` marker the server rendered. */
28
+ /**
29
+ * Hydrate every `[data-island]` marker the server rendered.
30
+ *
31
+ * Every distinct island module is fetched at once, then the markers are
32
+ * hydrated in document order; awaiting each in turn would serialise one
33
+ * network round-trip per island.
34
+ */
29
35
  export async function hydrateIslands(manifest) {
30
- for (const el of document.querySelectorAll("[data-island]")) {
31
- const key = el.dataset.island;
36
+ const els = [...document.querySelectorAll("[data-island]")];
37
+ const keys = [...new Set(els.map((el) => el.dataset.island))];
38
+ const loaded = new Map(await Promise.all(keys.map(async (key) => {
32
39
  const loader = manifest[key];
33
- if (!loader) {
40
+ if (!loader)
34
41
  console.warn("[fu] no island module for", key);
42
+ return [key, loader ? (await loader()).default : null];
43
+ })));
44
+ for (const el of els) {
45
+ const key = el.dataset.island;
46
+ const impl = loaded.get(key);
47
+ if (!impl)
35
48
  continue;
36
- }
37
- const mod = await loader();
38
49
  const props = JSON.parse(el.dataset.props || "{}");
39
- hydrate(h(proxyFor(key, mod.default).Component, props), el);
50
+ hydrate(h(proxyFor(key, impl).Component, props), el);
40
51
  const list = mounted.get(key) ?? [];
41
52
  list.push({ el, props });
42
53
  mounted.set(key, list);
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
package/dist/dev.d.ts CHANGED
@@ -1,2 +1,10 @@
1
1
  import type { FuOptions } from "./types.js";
2
+ /**
3
+ * Whether a page at `origin` may open the HMR socket. A browser lets any site
4
+ * open a WebSocket to localhost, and this one streams module source on every
5
+ * save, so only the dev server's own pages get in: its port, on a loopback
6
+ * name or the host it was bound to. The HMR runtime dials localhost, so a page
7
+ * on another machine never gets a working socket anyway.
8
+ */
9
+ export declare function hmrOriginAllowed(origin: string | null, port: number, hostname: string): boolean;
2
10
  export declare function dev(opts: FuOptions): Promise<void>;
package/dist/dev.js CHANGED
@@ -5,71 +5,75 @@ import { serve } from "crossws/server";
5
5
  import { build as nitroBuild, createDevServer, createNitro, prepare } from "nitro/builder";
6
6
  import * as fs from "node:fs";
7
7
  import * as path from "node:path";
8
- import { bootModule, css, jsx, optional, ssrModule, virtual, walk } from "./plugins.js";
9
- /**
10
- * Directory holding the framework's own runtime modules, whose paths are handed
11
- * to rolldown. `import.meta.dirname` is undefined when this module is loaded
12
- * from a remote URL, and a bundler cannot fetch `https:` modules anyway — so
13
- * fail with the reason rather than a TypeError three frames later.
14
- */
15
- const HERE = import.meta.dirname ?? remoteFrameworkError();
8
+ import { clientInput, emptyDir, nitroOptions, runtime, scanProject, writeSheets, writeSsrEntry, } from "./driver.js";
9
+ /** Set an env var on whichever runtime we are on; `Deno.env` is Deno-only. */
10
+ function setEnv(name, value) {
11
+ const g = globalThis;
12
+ if (g.Deno)
13
+ g.Deno.env.set(name, value);
14
+ else if (g.process)
15
+ g.process.env[name] = value;
16
+ }
17
+ const LOOPBACK = new Set(["localhost", "127.0.0.1", "[::1]"]);
16
18
  /**
17
- * Extension of the framework's own runtime modules: `.ts` when running from
18
- * source (this repo, or JSR), `.js` when running from the compiled npm build.
19
+ * Whether a page at `origin` may open the HMR socket. A browser lets any site
20
+ * open a WebSocket to localhost, and this one streams module source on every
21
+ * save, so only the dev server's own pages get in: its port, on a loopback
22
+ * name or the host it was bound to. The HMR runtime dials localhost, so a page
23
+ * on another machine never gets a working socket anyway.
19
24
  */
20
- const EXT = import.meta.url.endsWith(".js") ? ".js" : ".ts";
21
- function remoteFrameworkError() {
22
- throw new Error("fu: the framework is loaded from a remote URL (" + import.meta.url + "), " +
23
- "so its runtime modules cannot be handed to the bundler. Install it " +
24
- "instead — `npm:@janit/fu` in a Deno import map, or `npm i @janit/fu` — " +
25
- "so it resolves to a real directory.");
25
+ export function hmrOriginAllowed(origin, port, hostname) {
26
+ if (!origin)
27
+ return false;
28
+ let url;
29
+ try {
30
+ url = new URL(origin);
31
+ }
32
+ catch {
33
+ return false;
34
+ }
35
+ if (url.protocol !== "http:" && url.protocol !== "https:")
36
+ return false;
37
+ if (url.port !== String(port))
38
+ return false;
39
+ return LOOPBACK.has(url.hostname) || url.hostname === hostname;
26
40
  }
27
41
  export async function dev(opts) {
28
- const root = path.resolve(opts.root);
42
+ const project = scanProject(opts.root);
43
+ const { clientDir } = project;
29
44
  const port = opts.port ?? 1337;
30
45
  // Bind all interfaces so the dev server is reachable from other machines
31
46
  // and from inside containers, not just loopback.
32
47
  const hostname = opts.hostname ?? "0.0.0.0";
33
48
  const hmrPort = port + 1;
34
- const clientDir = path.join(root, "dist/client");
35
- const genDir = path.join(root, ".fu");
36
- const routeFiles = walk(root, path.join(root, "routes"));
37
- const islandFiles = walk(root, path.join(root, "islands"));
38
- fs.rmSync(clientDir, { recursive: true, force: true });
39
- fs.mkdirSync(clientDir, { recursive: true });
49
+ // An app cannot otherwise tell dev from production: nothing in the
50
+ // environment says so, and the same middleware runs in both. Anything that
51
+ // must be laxer in dev — a CSP that has to allow the HMR socket on
52
+ // `hmrPort`, a verbose error page — reads this.
53
+ setEnv("FU_DEV", "1");
54
+ emptyDir(clientDir);
40
55
  const sheets = new Map();
41
56
  const peers = new Map();
42
57
  let cssVersion = 0;
43
- const writeSheets = () => {
44
- if (sheets.size)
45
- fs.writeFileSync(path.join(clientDir, "style.css"), [...sheets.values()].join("\n"));
46
- };
58
+ const flushCss = () => writeSheets(clientDir, sheets, "style.css");
47
59
  // `implement` takes the runtime SOURCE, not a path — a path gets inlined
48
60
  // literally and parsed as a regex. `$ADDR` is only substituted in rolldown's
49
61
  // own default runtime, so do it here.
50
- const hmrRuntime = fs.readFileSync(path.join(HERE, "hmr-runtime.js"), "utf8")
51
- .replaceAll("$ADDR", `localhost:${hmrPort}`);
62
+ const hmrRuntime = fs.readFileSync(runtime.hmr, "utf8").replaceAll("$ADDR", `localhost:${hmrPort}`);
52
63
  const engine = await DevEngine.create({
53
- input: { boot: "fu:boot" },
54
- plugins: [
55
- virtual({ "fu:boot": bootModule(path.join(HERE, `client${EXT}`), islandFiles, root) }),
56
- css(sheets),
57
- jsx({ hmr: true }),
58
- ],
59
- platform: "browser",
60
- moduleTypes: { ".css": "js" },
64
+ ...clientInput(project, sheets, true),
61
65
  experimental: { devMode: { host: "localhost", port: hmrPort, implement: hmrRuntime } },
62
66
  }, { dir: clientDir, format: "esm", entryFileNames: "[name].js", chunkFileNames: "[name].js" }, {
63
67
  watch: { enabled: true },
64
68
  onOutput(o) {
65
69
  if (o instanceof Error)
66
70
  return console.error("[fu] client build failed:", o.message);
67
- writeSheets();
71
+ flushCss();
68
72
  },
69
73
  onHmrUpdates(r) {
70
74
  if (r instanceof Error)
71
75
  return console.error("[fu] hmr error:", r.message);
72
- writeSheets();
76
+ flushCss();
73
77
  for (const { clientId, update } of r.updates) {
74
78
  const peer = peers.get(clientId);
75
79
  if (!peer || update.type === "Noop")
@@ -112,8 +116,14 @@ export async function dev(opts) {
112
116
  };
113
117
  serve({
114
118
  port: hmrPort,
119
+ hostname,
115
120
  fetch: () => new Response("fu hmr"),
116
121
  websocket: {
122
+ upgrade(req) {
123
+ if (!hmrOriginAllowed(req.headers.get("origin"), port, hostname)) {
124
+ return new Response("Forbidden", { status: 403 });
125
+ }
126
+ },
117
127
  async open(peer) {
118
128
  const id = clientIdOf(peer);
119
129
  if (!id)
@@ -132,31 +142,11 @@ export async function dev(opts) {
132
142
  },
133
143
  });
134
144
  // SSR.
135
- fs.rmSync(genDir, { recursive: true, force: true });
136
- fs.mkdirSync(genDir, { recursive: true });
137
- const ssrEntry = path.join(genDir, "ssr.ts");
138
- const assets = `{ js: [{ href: "/boot.js" }], css: [{ href: "/style.css" }] }`;
139
- fs.writeFileSync(ssrEntry, ssrModule({
140
- renderPath: path.join(HERE, `render${EXT}`),
141
- routeFiles,
142
- root,
143
- assets,
144
- appPath: optional(root, "app.ts", "app.tsx"),
145
- shellPath: optional(root, "routes/_app.tsx"),
146
- errorPath: optional(root, "routes/_error.tsx"),
147
- }));
148
- const nitro = await createNitro({
149
- dev: true,
150
- rootDir: root,
151
- serverDir: genDir,
152
- scanDirs: [],
153
- publicAssets: [{ dir: clientDir, baseURL: "/" }],
154
- handlers: [{ route: "/**", handler: ssrEntry, format: "web", lazy: false }],
155
- rollupConfig: {
156
- plugins: [css(new Map()), jsx({ stampIslands: true })],
157
- moduleTypes: { ".css": "js" },
158
- },
145
+ const ssrEntry = writeSsrEntry(project, {
146
+ js: [{ href: "/boot.js" }],
147
+ css: [{ href: "/style.css" }],
159
148
  });
149
+ const nitro = await createNitro({ ...nitroOptions(project, ssrEntry), dev: true });
160
150
  const server = createDevServer(nitro);
161
151
  server.listen({ port, hostname });
162
152
  // Order matters: listen -> prepare -> build. `build` starts the dev runner.