@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 +56 -21
- package/dist/app.d.ts +0 -2
- package/dist/app.js +0 -4
- package/dist/app.js.map +1 -1
- package/dist/build.d.ts +0 -2
- package/dist/build.js +10 -85
- package/dist/build.js.map +1 -1
- package/dist/cli.js +7 -7
- package/dist/cli.js.map +1 -1
- package/dist/client.d.ts +7 -1
- package/dist/client.js +18 -7
- package/dist/client.js.map +1 -1
- package/dist/dev.d.ts +8 -0
- package/dist/dev.js +53 -63
- package/dist/dev.js.map +1 -1
- package/dist/driver.d.ts +66 -0
- package/dist/driver.js +181 -0
- package/dist/driver.js.map +1 -0
- package/dist/errors.d.ts +6 -0
- package/dist/errors.js +10 -5
- package/dist/errors.js.map +1 -1
- package/dist/hmr-runtime.js +24 -20
- package/dist/plugins.d.ts +7 -24
- package/dist/plugins.js +18 -58
- package/dist/plugins.js.map +1 -1
- package/dist/render.d.ts +10 -5
- package/dist/render.js +13 -11
- package/dist/render.js.map +1 -1
- package/dist/router.d.ts +17 -0
- package/dist/router.js +72 -12
- package/dist/router.js.map +1 -1
- package/package.json +6 -6
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 ~
|
|
12
|
-
drivers ~
|
|
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
|
-
|
|
31
|
-
rolldown,
|
|
32
|
-
|
|
33
|
-
|
|
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 #
|
|
51
|
-
deno task check # types
|
|
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.
|
|
58
|
-
|
|
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
|
|
180
|
-
chain, so security headers and logging middleware still
|
|
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
|
-
|
|
237
|
-
|
|
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
|
|
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
|
|
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
|
|
278
|
-
from `../src/mod.ts` on the way into
|
|
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
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;
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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
|
|
23
|
+
const cssFile = writeSheets(project.clientDir, sheets);
|
|
59
24
|
// ---- server ----
|
|
60
|
-
|
|
61
|
-
fs.mkdirSync(genDir, { recursive: true });
|
|
62
|
-
const assets = {
|
|
25
|
+
const ssrEntry = writeSsrEntry(project, {
|
|
63
26
|
js: [{ href: "/" + entry.fileName }],
|
|
64
|
-
css:
|
|
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":"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,
|
|
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
|
|
7
|
-
const
|
|
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:
|
|
11
|
-
port:
|
|
12
|
-
hostname:
|
|
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(
|
|
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,
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
|
|
31
|
-
|
|
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,
|
|
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);
|
package/dist/client.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAA0B,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAepE,MAAM,OAAO,GAAG,IAAI,GAAG,EAAqB,CAAC;AAC7C,MAAM,OAAO,GAAG,IAAI,GAAG,EAAuB,CAAC;AAC/C,IAAI,UAAU,GAAG,CAAC,CAAC;AAEnB;;;;;;;GAOG;AACH,SAAS,QAAQ,CAAC,GAAW,EAAE,IAA8B;IAC3D,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAClC,IAAI,QAAQ,EAAE,CAAC;QACb,QAAQ,CAAC,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;QAC7B,OAAO,QAAQ,CAAC;IAClB,CAAC;IACD,MAAM,MAAM,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;IACjC,6EAA6E;IAC7E,8EAA8E;IAC9E,uCAAuC;IACvC,MAAM,SAAS,GAAG,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,KAAK,EAAS,EAAE,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;IAClF,MAAM,KAAK,GAAgB,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;IACvD,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IACxB,OAAO,KAAK,CAAC;AACf,CAAC;AAED
|
|
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 {
|
|
9
|
-
/**
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
*
|
|
18
|
-
*
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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.
|