@uniflowed/vite 0.0.0-alpha.7 → 0.0.0-alpha.8

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.
@@ -0,0 +1,151 @@
1
+ // @noflow
2
+ //
3
+ // Plain JavaScript: executed by the host that runs Vite, before any transform.
4
+ //
5
+ // The server/client split, as the bundler applies it.
6
+ //
7
+ // `crates/uf_rsc` decides which modules a `"use client"` boundary is reachable
8
+ // from. That answer used to reach nothing: `virtual:uf/routes` emitted
9
+ // `page: () => import(<file>)` for every route, `virtual:uf/client` imported
10
+ // that table, and so every page in the application was a chunk of the *client*
11
+ // bundle whether or not a browser had anything to do with it.
12
+ //
13
+ // This module is the first thing that reads the answer. What is done with it
14
+ // is in `routesModuleSource`, including the one thing a dropped route keeps.
15
+ //
16
+ // # Why the unit is a route and not a module
17
+ //
18
+ // Dropping a single Server Component from the client bundle is what Next.js
19
+ // does, and it works there because the browser is handed a Flight payload
20
+ // describing the tree the server rendered. uf has no such payload yet:
21
+ // `packages/router/client.js` hydrates by re-rendering the matched tree from
22
+ // the same modules the server rendered it from, so a module missing from the
23
+ // client bundle is a module React cannot hydrate. What *can* be dropped is a
24
+ // route the browser never renders at all — one where no client boundary is
25
+ // reachable from the page, its layouts, its loading fallbacks or the
26
+ // boundaries that cover it. Nothing under it is ever re-rendered in the
27
+ // browser, so nothing under it has to be shipped. See ubugeeei-prod/uf#350.
28
+ //
29
+ // # Why "unknown" means "ship it"
30
+ //
31
+ // The analysis scans `.js`. A page written as `.mdx`, a `.jsx` module, a file
32
+ // past the scanner's size limit: none of them is in the manifest, and the
33
+ // honest reading of a module the analysis never saw is that it might reach a
34
+ // boundary. Every unknown answers `true`, so the split can only ever remove a
35
+ // route uf has positively decided needs no browser — and a manifest that is
36
+ // missing, unreadable, or written by an older uf removes nothing at all.
37
+
38
+ import { readFileSync } from "node:fs";
39
+ import path from "node:path";
40
+
41
+ /** Environment variable naming the manifest, set by `uf build` and `uf dev`. */
42
+ export const RSC_MANIFEST_ENV = "UF_RSC_MANIFEST";
43
+
44
+ /**
45
+ * The manifest schema this understands.
46
+ *
47
+ * Version 1 published the client boundaries and nothing that said which
48
+ * modules sat *above* one, so it cannot answer the question this module asks.
49
+ * An older manifest is therefore refused rather than read optimistically: a
50
+ * missing `proximity` would read as `undefined`, compare unequal to
51
+ * `"reaches-boundary"`, and quietly drop every route from the client bundle.
52
+ */
53
+ const SUPPORTED_VERSION = 2;
54
+
55
+ /**
56
+ * Read the RSC manifest, or `null` when there is nothing usable to read.
57
+ *
58
+ * Never throws. The split is an optimisation over a build that is already
59
+ * correct without it, so no failure here may be a failure of the build.
60
+ *
61
+ * @param {string | undefined} file absolute path, from the environment
62
+ */
63
+ export function readRscManifest(file) {
64
+ if (file == null || file === "") return null;
65
+ let parsed;
66
+ try {
67
+ parsed = JSON.parse(readFileSync(file, "utf8"));
68
+ } catch {
69
+ return null;
70
+ }
71
+ if (parsed == null || typeof parsed !== "object") return null;
72
+ if (parsed.version !== SUPPORTED_VERSION || !Array.isArray(parsed.modules)) return null;
73
+ return parsed;
74
+ }
75
+
76
+ /**
77
+ * Which modules the browser has to be able to evaluate, keyed by project path.
78
+ *
79
+ * A `"use client"` module is a client bundle root by definition, and
80
+ * `proximity` never says so about it — it is the far side of the boundary
81
+ * rather than a module above one — so both halves are asked. This mirrors
82
+ * `RscModule::requires_client_bundle` in `crates/uf_rsc/src/graph.rs`.
83
+ */
84
+ function clientModules(manifest) {
85
+ const modules = new Map();
86
+ for (const module of manifest.modules) {
87
+ if (module == null || typeof module.path !== "string") continue;
88
+ modules.set(
89
+ module.path,
90
+ module.environment === "client" || module.proximity === "reaches-boundary",
91
+ );
92
+ }
93
+ return modules;
94
+ }
95
+
96
+ /**
97
+ * Whether `boundary`'s route path covers `route`'s.
98
+ *
99
+ * Deliberately "covers" and not "is nearest to". `packages/router` picks the
100
+ * nearest not-found and error boundary above a path at render time; asking the
101
+ * same question here would be a second implementation of that rule, and the
102
+ * two would disagree the first time either moved. Every boundary that could
103
+ * apply is counted instead, which can only decide that more routes need the
104
+ * browser than strictly do.
105
+ */
106
+ function covers(boundaryPath, routePath) {
107
+ return (
108
+ boundaryPath === "/" || routePath === boundaryPath || routePath.startsWith(`${boundaryPath}/`)
109
+ );
110
+ }
111
+
112
+ /**
113
+ * Build the predicate `routesModuleSource` asks about each route.
114
+ *
115
+ * Returns `(route) => boolean`: true when the route's page module belongs in
116
+ * the client bundle. With no manifest every route answers true, which is the
117
+ * whole table and exactly what the build emitted before this existed.
118
+ *
119
+ * @param {object | null} manifest from {@link readRscManifest}
120
+ * @param {string} root absolute project root
121
+ * @param {{notFound?: Array<object>, errors?: Array<object>}} [boundaries]
122
+ * the scanned table, so a boundary that needs the browser keeps the routes
123
+ * it covers in the client bundle
124
+ */
125
+ export function clientRouteFilter(manifest, root, boundaries = {}) {
126
+ if (manifest == null) return () => true;
127
+ const modules = clientModules(manifest);
128
+
129
+ const needed = (file) => {
130
+ if (typeof file !== "string") return true;
131
+ const relative = path.relative(root, file).split(path.sep).join("/");
132
+ const answer = modules.get(relative);
133
+ return answer === undefined ? true : answer;
134
+ };
135
+
136
+ const notFound = boundaries.notFound ?? [];
137
+ const errors = boundaries.errors ?? [];
138
+
139
+ return (route) => {
140
+ if (needed(route.page)) return true;
141
+ if (route.layouts.some(needed)) return true;
142
+ if ((route.loading ?? []).some((entry) => needed(entry.module))) return true;
143
+ for (const boundary of notFound) {
144
+ if (covers(boundary.path, route.path) && needed(boundary.page)) return true;
145
+ }
146
+ for (const boundary of errors) {
147
+ if (covers(boundary.path, route.path) && needed(boundary.module)) return true;
148
+ }
149
+ return false;
150
+ };
151
+ }
package/internal/serve.js CHANGED
@@ -4,13 +4,11 @@
4
4
  //
5
5
  // Serving what `uf build` wrote — one request handler, behind two front doors.
6
6
  //
7
- // `uf build` writes three things and, until this module existed, nothing could
8
- // answer a request with any of them: a client bundle and prerendered HTML in
7
+ // `uf build` writes three things: a client bundle and prerendered HTML in
9
8
  // `dist/`, and a server bundle in `.uf/build/server/server.js` that exports
10
- // `render`, `dispatch`, `routes` and `notFound`. The prerendered half could be
11
- // put on a static host; the other half could only be reached from `uf dev`, so
12
- // a route handler and a route with parameters and no `generateStaticParams`
13
- // worked in development and did not exist in a build.
9
+ // `render`, `dispatch`, `runMiddleware`, `routes`, `middleware`, `notFound`
10
+ // and `errors`. This module finds them, reads the client manifest, and hands
11
+ // both to the handler `uf preview` and `uf start` mount.
14
12
  //
15
13
  // `uf preview` and `uf start` are the two front doors, and they share
16
14
  // everything below on purpose. A preview whose answers differ from the
@@ -26,58 +24,71 @@
26
24
  // running a production build should not need Vite installed to
27
25
  // answer a request.
28
26
  //
29
- // # Why this is JavaScript
27
+ // # Where the answering actually happens
30
28
  //
31
- // The rest of uf's hot paths are Rust, and this one deliberately is not: it
32
- // runs in the deployed application rather than in the build, and
33
- // `ubugeeei-redundancy.md` is explicit that a deployment must not inherit a
34
- // native dependency from the toolchain that produced it. An edge or serverless
35
- // target that cannot run a Rust binary still has to be able to run this.
29
+ // Not here, any more. Every decision about *what* a request is answered with
30
+ // lives in `@uniflowed/server` `@uniflowed/server/fetch` for the application
31
+ // half and `@uniflowed/server/node` for the files and the socket and this
32
+ // module is the part that is genuinely Vite's: finding the build on disk and
33
+ // reading the manifest a Vite build wrote.
36
34
  //
37
- // # The seam the adapters need
35
+ // It moved because of `uf build --adapter`. `tests/library/serve.test.js` said
36
+ // what was wrong with the old arrangement while it was still the only one:
37
+ // "`internal/serve.js` is the seam a deploy adapter will need, and naming it
38
+ // in `exports` before one exists would be promising an interface nothing has
39
+ // used yet." An adapter exists now, and it may not import this package —
40
+ // `@uniflowed/vite` is the bundler, and the whole claim of deployable output
41
+ // is that the host needs neither the bundler nor the toolchain. So the seam is
42
+ // a package export of `@uniflowed/server`, and `uf preview`, `uf start` and
43
+ // every adapter now answer out of one implementation instead of copies that
44
+ // agree until they do not.
38
45
  //
39
- // [`createApplicationHandler`] takes a `Request` and returns a `Response` and
40
- // touches no filesystem, so it is the part that ports to a worker unchanged.
41
- // [`createStaticHandler`] reads files and is therefore host-specific, which is
42
- // exactly the split a deploy adapter has to make: on a CDN-backed target the
43
- // static half is not the application's job at all.
46
+ // # Why those imports are dynamic
47
+ //
48
+ // `@uniflowed/server` is Flow, and `driver.js` registers the loader hooks that
49
+ // make Flow importable *in its body* after every static import in this graph
50
+ // has already been evaluated. So they are reached the same way the server
51
+ // bundle is: with `await import`, from [`loadBuild`], which is the point at
52
+ // which this process stops being plain JavaScript and starts being the
53
+ // project's.
54
+ //
55
+ // # Who owns the request
56
+ //
57
+ // The host does, and none of the three handlers below: each of them has a
58
+ // `Response` in hand rather than a response on the wire, and what `after()`
59
+ // promises is the wire. [`withRequest`] is the shape for a caller that writes
60
+ // into a Node response itself — `uf dev` and `uf preview` — and
61
+ // `@uniflowed/server/node`'s `nodeListener` does the same thing for `uf start`
62
+ // and for the `server.js` an adapter writes. Each of them begins the request
63
+ // with `entry.beginRequest`, runs the whole of answering it inside `run`, and
64
+ // settles it on the line after the last byte.
65
+ //
66
+ // It has to be the *entry's* `beginRequest` rather than one imported here: the
67
+ // request lives in an `AsyncLocalStorage` belonging to one copy of
68
+ // `@uniflowed/server`, and the copy that matters is the one inside the
69
+ // application bundle. A host that resolved its own would begin a request the
70
+ // application cannot see, and nothing would fail loudly — the guard would run,
71
+ // the page would render, and every `cookies()` in it would throw as though no
72
+ // host had run at all. See ubugeeei-prod/uf#389.
44
73
 
45
- import { createReadStream } from "node:fs";
46
74
  import { readFile, stat } from "node:fs/promises";
47
75
  import path from "node:path";
48
- import { Readable } from "node:stream";
49
76
  import { pathToFileURL } from "node:url";
50
77
 
51
78
  /**
52
- * Content types for what a uf build emits.
79
+ * `@uniflowed/server`'s two halves, loaded once.
53
80
  *
54
- * A closed table rather than a dependency, and deliberately short: every entry
55
- * is an extension `uf build` actually writes or a project actually puts in
56
- * `public/`. Anything else is `application/octet-stream`, which a browser
57
- * downloads rather than executes — the safe answer for a file whose type we do
58
- * not know, and the reason this is not a guess based on the bytes.
81
+ * Cached as the promise rather than the modules, so two concurrent callers
82
+ * share one import rather than racing to start two.
59
83
  */
60
- const CONTENT_TYPES = Object.freeze({
61
- ".avif": "image/avif",
62
- ".css": "text/css; charset=utf-8",
63
- ".gif": "image/gif",
64
- ".html": "text/html; charset=utf-8",
65
- ".ico": "image/x-icon",
66
- ".jpeg": "image/jpeg",
67
- ".jpg": "image/jpeg",
68
- ".js": "text/javascript; charset=utf-8",
69
- ".json": "application/json; charset=utf-8",
70
- ".map": "application/json; charset=utf-8",
71
- ".mjs": "text/javascript; charset=utf-8",
72
- ".png": "image/png",
73
- ".svg": "image/svg+xml",
74
- ".txt": "text/plain; charset=utf-8",
75
- ".webmanifest": "application/manifest+json",
76
- ".webp": "image/webp",
77
- ".woff": "font/woff",
78
- ".woff2": "font/woff2",
79
- ".xml": "application/xml; charset=utf-8",
80
- });
84
+ let deploymentModules = null;
85
+ function deployment() {
86
+ deploymentModules ??= Promise.all([
87
+ import("@uniflowed/server/fetch"),
88
+ import("@uniflowed/server/node"),
89
+ ]).then(([application, host]) => ({ ...application, ...host }));
90
+ return deploymentModules;
91
+ }
81
92
 
82
93
  /**
83
94
  * Everything a served build consists of.
@@ -86,6 +97,11 @@ const CONTENT_TYPES = Object.freeze({
86
97
  * while the server runs, and importing the server bundle again per request
87
98
  * would re-evaluate every module in the application.
88
99
  *
100
+ * `@uniflowed/server` is loaded here too, and not lazily on the first request:
101
+ * a missing or broken install should fail the command that starts the server,
102
+ * with the message the import raises, rather than a minute later inside
103
+ * whichever request happened to arrive first.
104
+ *
89
105
  * @param {{root: string, outDir: string, serverDir: string}} build
90
106
  */
91
107
  export async function loadBuild({ root, outDir, serverDir }) {
@@ -101,6 +117,7 @@ export async function loadBuild({ root, outDir, serverDir }) {
101
117
 
102
118
  const manifest = JSON.parse(await readFile(manifestFile, "utf8"));
103
119
  const entry = await import(pathToFileURL(entryFile).href);
120
+ await deployment();
104
121
  return { entry, assets: assetsFromManifest(manifest), distDir };
105
122
  }
106
123
 
@@ -130,6 +147,11 @@ async function readable(file, message) {
130
147
  *
131
148
  * The entry is found by its `isEntry` flag rather than by key, because a
132
149
  * virtual module's manifest key is an implementation detail of the bundler.
150
+ *
151
+ * This is the one piece of serving a build that is genuinely Vite's — a Vite
152
+ * manifest, read the way Vite writes it — which is why it stayed behind when
153
+ * the rest moved to `@uniflowed/server`. `uf build --adapter` calls it too,
154
+ * at build time, and bakes the answer into what it emits.
133
155
  */
134
156
  export function assetsFromManifest(manifest) {
135
157
  const entry = Object.values(manifest).find((chunk) => chunk.isEntry);
@@ -167,96 +189,76 @@ export function assetsFromManifest(manifest) {
167
189
  };
168
190
  }
169
191
 
192
+ /**
193
+ * Answer one request inside it, and settle it when the answer has been written.
194
+ *
195
+ * `body` is everything that decides the response *and writes it*; this is the
196
+ * line after. `settle` is in a `finally` because a request that failed is
197
+ * still a request that happened: a middleware that logged the arrival is owed
198
+ * its callback whether the render threw or not, and `drainDeferred` already
199
+ * reports a failing task rather than propagating it.
200
+ *
201
+ * `entry.beginRequest` and not an import: the request lives in an
202
+ * `AsyncLocalStorage` belonging to one copy of `@uniflowed/server`, and the
203
+ * copy that matters is the one inside the application bundle. See
204
+ * `serverModuleSource` in `./routes.js`.
205
+ *
206
+ * The one case this cannot be exact about is a request uf hands back rather
207
+ * than answers: a caller whose `catch` is `next(error)` gives the response to
208
+ * Vite's chain, which writes a 500 at a moment nothing here can observe, so
209
+ * such a request settles when uf lets go of it. `nodeListener` and the
210
+ * compiled binary write their own failures and settle after them. It is worth
211
+ * naming rather than papering over, and it is the failure path of a request
212
+ * that already went wrong — not the ordinary one this exists for.
213
+ *
214
+ * @param {{beginRequest: (request: Request) => {run: <T>(body: () => Promise<T>) => Promise<T>, settle: () => Promise<void>}}} entry
215
+ * @param {Request} request
216
+ * @param {() => Promise<mixed>} body
217
+ */
218
+ export async function withRequest(entry, request, body) {
219
+ const { run, settle } = entry.beginRequest(request);
220
+ try {
221
+ return await run(body);
222
+ } finally {
223
+ await settle();
224
+ }
225
+ }
226
+
170
227
  /**
171
228
  * The application half: route handlers, then rendering.
172
229
  *
173
- * Touches no filesystem and holds no Node types, so this is the function a
174
- * deploy adapter for a worker or a serverless function wraps. Returns `null`
175
- * for nothing, evera request that matches no handler and no route is a
176
- * rendered 404, because the renderer is what knows what the project's
177
- * `_uf.not-found` page says.
230
+ * `@uniflowed/server/fetch`'s `createFetchHandler`, reached through the
231
+ * dynamic import above. Kept as a function here rather than making every
232
+ * caller await the module because the two servers construct their handler
233
+ * before they take a socket, and an `await` in that position would put the
234
+ * import between the port and the first request rather than before both.
178
235
  *
179
- * The order is the dev server's, and has to stay the dev server's: handlers
180
- * first and for every method, because a handler is the only thing that can
181
- * answer a `POST` and it may also answer a `GET` for a path that has no page.
182
- * A page cannot answer a `POST`, so a non-navigation that no handler claimed
183
- * is a 404 rather than a rendered page with a 200.
236
+ * It must be called inside a request its caller began; it begins none, because
237
+ * it has a `Response` in hand and not a response on the wire. A caller that
238
+ * forgets is not left to discover it: `entry.runMiddleware` refuses outside a
239
+ * request and names what establishes one. See "Who owns the request" above.
184
240
  *
185
241
  * @param {{entry: object, assets: object}} build
186
242
  */
187
243
  export function createApplicationHandler({ entry, assets }) {
244
+ const ready = deployment().then(({ createFetchHandler }) =>
245
+ createFetchHandler({ app: entry, document: assets }),
246
+ );
188
247
  return async function handle(request) {
189
- const handled = await entry.dispatch(request);
190
- if (handled != null) return handled;
191
-
192
- const method = request.method.toUpperCase();
193
- if (method !== "GET" && method !== "HEAD") {
194
- return new Response(null, { status: 404 });
195
- }
196
-
197
- const url = new URL(request.url);
198
- const result = await entry.render(url.pathname + url.search, assets);
199
- const headers = new Headers(result.headers ?? {});
200
- headers.set("content-type", "text/html; charset=utf-8");
201
- // A `HEAD` gets the status and the headers and no body, which is what the
202
- // renderer cannot know to do for itself.
203
- return new Response(method === "HEAD" ? null : result.html, {
204
- status: result.status ?? 200,
205
- headers,
206
- });
248
+ return (await ready)(request);
207
249
  };
208
250
  }
209
251
 
210
252
  /**
211
253
  * The static half: a file under `root`, or `null` for the caller to carry on.
212
254
  *
213
- * `GET` and `HEAD` only. A `POST` to a path that happens to have a file under
214
- * it belongs to a route handler, and answering it with the file's bytes would
215
- * be the same mistake as rendering a page for it.
216
- *
217
- * # The path is checked once, after it is resolved
218
- *
219
- * `docs/security.md` rule 2: never authorize against a raw request string or a
220
- * partially decoded path. The pathname is decoded first, then resolved against
221
- * the root, and *then* checked to be inside it — so `%2e%2e%2f`, a backslash
222
- * on Windows, and a symlinked directory all reduce to the same question, asked
223
- * once, of the value that is actually opened.
255
+ * `@uniflowed/server/node`'s, for the same reason as above: what a deployment
256
+ * runs and what `uf start` runs have to be the same code, not the same idea.
224
257
  */
225
258
  export function createStaticHandler({ root }) {
226
- const distDir = path.resolve(root);
227
-
259
+ const ready = deployment().then(({ createStaticHandler: create }) => create({ root }));
228
260
  return async function serveStatic(request) {
229
- const method = request.method.toUpperCase();
230
- if (method !== "GET" && method !== "HEAD") return null;
231
-
232
- const pathname = decodePathname(new URL(request.url).pathname);
233
- if (pathname == null) return null;
234
-
235
- const resolved = path.resolve(distDir, `.${pathname}`);
236
- if (resolved !== distDir && !resolved.startsWith(distDir + path.sep)) return null;
237
-
238
- // `/guide/` and `/guide` are the same prerendered document, and neither
239
- // spelling is the one a person types. `<path>.html` is last because a
240
- // build writes `guide/index.html`, and only a hand-placed file in
241
- // `public/` is ever `guide.html`.
242
- const candidates = pathname.endsWith("/")
243
- ? [path.join(resolved, "index.html")]
244
- : [resolved, path.join(resolved, "index.html"), `${resolved}.html`];
245
-
246
- for (const candidate of candidates) {
247
- const info = await statFile(candidate);
248
- if (info == null || !info.isFile()) continue;
249
- const headers = {
250
- "content-type":
251
- CONTENT_TYPES[path.extname(candidate).toLowerCase()] ?? "application/octet-stream",
252
- "content-length": String(info.size),
253
- };
254
- if (method === "HEAD") return new Response(null, { headers });
255
- // Streamed rather than read into memory, so serving a large asset costs
256
- // a buffer rather than the file.
257
- return new Response(Readable.toWeb(createReadStream(candidate)), { headers });
258
- }
259
- return null;
261
+ return (await ready)(request);
260
262
  };
261
263
  }
262
264
 
@@ -280,107 +282,27 @@ export function createServeHandler({ entry, assets, distDir }) {
280
282
  };
281
283
  }
282
284
 
283
- function decodePathname(pathname) {
284
- try {
285
- const decoded = decodeURIComponent(pathname);
286
- // A NUL truncates the name every C-level `open` sees, so a path holding
287
- // one is refused rather than normalised into something shorter.
288
- return decoded.includes("\0") ? null : decoded;
289
- } catch {
290
- // A percent escape that is not one. There is no file behind it.
291
- return null;
292
- }
293
- }
294
-
295
- async function statFile(file) {
296
- try {
297
- return await stat(file);
298
- } catch {
299
- return null;
300
- }
301
- }
302
-
303
285
  /**
304
286
  * A `Request`/`Response` handler as a Node request listener.
305
287
  *
306
- * The handler contract is the platform's, so this adapter belongs here rather
307
- * than in every host that wants to run one — `uf dev`'s middleware, `uf
308
- * preview`'s, and `uf start`'s own server all reach for the same two halves.
288
+ * `@uniflowed/server/node`'s, which is also what the `server.js` an adapter
289
+ * writes runs so a request reaching `uf start` and the same request reaching
290
+ * a deployed directory go through one translation rather than two, and settle
291
+ * at one moment rather than at two.
309
292
  *
310
- * A handler that throws is answered with a bare 500 and reported on stderr:
311
- * the body must not carry the stack, because the body goes to whoever asked,
312
- * and stderr is where the operator is already looking. It is not an event on
313
- * stdout because a request failing is the application's news, not the driver's
314
- * the driver's stdout says what the *server* is doing.
293
+ * `entry` is the second argument rather than something this reaches for: it is
294
+ * the application bundle's own `beginRequest` that has to own the request, for
295
+ * the reason in "Who owns the request" above. It is required, and a listener
296
+ * built without one fails on its first request the same trade
297
+ * `createFetchHandler` makes about `app.runMiddleware`, and for the same
298
+ * reason: an optional lifecycle is a lifecycle somebody forgets, and what is
299
+ * lost when they do is every `after()` in the application.
315
300
  */
316
- export function nodeListener(handle) {
301
+ export function nodeListener(handle, entry) {
302
+ const ready = deployment().then(({ nodeListener: create }) =>
303
+ create(handle, { beginRequest: entry.beginRequest }),
304
+ );
317
305
  return async function listener(incoming, outgoing) {
318
- try {
319
- await send(outgoing, await handle(await toRequest(incoming)));
320
- } catch (error) {
321
- console.error(error);
322
- if (outgoing.headersSent) {
323
- outgoing.destroy();
324
- return;
325
- }
326
- outgoing.statusCode = 500;
327
- outgoing.setHeader("content-type", "text/plain; charset=utf-8");
328
- outgoing.end("500 Internal Server Error\n");
329
- }
306
+ return (await ready)(incoming, outgoing);
330
307
  };
331
308
  }
332
-
333
- /**
334
- * A Node request as a `Request`.
335
- *
336
- * The body is read as a stream where the host supports it, because a handler
337
- * that accepts an upload should not need the whole thing buffered before it
338
- * starts.
339
- */
340
- export async function toRequest(incoming, config) {
341
- const host = incoming.headers.host ?? "localhost";
342
- const protocol = config?.server?.https == null ? "http" : "https";
343
- const url = new URL(incoming.originalUrl ?? incoming.url ?? "/", `${protocol}://${host}`);
344
-
345
- const headers = new Headers();
346
- for (const [name, value] of Object.entries(incoming.headers)) {
347
- if (value == null) continue;
348
- for (const entry of Array.isArray(value) ? value : [value]) {
349
- headers.append(name, entry);
350
- }
351
- }
352
-
353
- const method = (incoming.method ?? "GET").toUpperCase();
354
- const init = { method, headers };
355
- if (method !== "GET" && method !== "HEAD") {
356
- // `duplex` is required by the specification whenever a body is a stream,
357
- // and Node throws without it.
358
- init.body = incoming;
359
- init.duplex = "half";
360
- }
361
- return new Request(url, init);
362
- }
363
-
364
- /** Write a `Response` to a Node response. */
365
- export async function send(outgoing, result) {
366
- outgoing.statusCode = result.status;
367
- if (result.statusText !== "") {
368
- outgoing.statusMessage = result.statusText;
369
- }
370
- for (const [name, value] of result.headers) {
371
- outgoing.setHeader(name, value);
372
- }
373
- if (result.body == null) {
374
- outgoing.end();
375
- return;
376
- }
377
- // Streamed rather than buffered, so a handler returning a large or
378
- // open-ended body is not read into memory first.
379
- const reader = result.body.getReader();
380
- while (true) {
381
- const { done, value } = await reader.read();
382
- if (done) break;
383
- outgoing.write(value);
384
- }
385
- outgoing.end();
386
- }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniflowed/vite",
3
- "version": "0.0.0-alpha.7",
3
+ "version": "0.0.0-alpha.8",
4
4
  "description": "Vite, driven by uf.config.js: every Flow module through `uf transform`, MDX, the file-system router and static rendering as Vite plugins.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -25,7 +25,8 @@
25
25
  "dependencies": {
26
26
  "@mdx-js/rollup": "^3.1.1",
27
27
  "@shikijs/rehype": "^3.23.0",
28
- "@uniflowed/host": "0.0.0-alpha.7",
28
+ "@uniflowed/host": "0.0.0-alpha.8",
29
+ "@uniflowed/server": "0.0.0-alpha.8",
29
30
  "rehype-slug": "^6.0.0",
30
31
  "remark-frontmatter": "^5.0.0",
31
32
  "remark-gfm": "^4.0.1",