@uniflowed/vite 0.0.0-alpha.6 → 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,308 @@
1
+ // @noflow
2
+ //
3
+ // Plain JavaScript: executed by the host that serves a build.
4
+ //
5
+ // Serving what `uf build` wrote — one request handler, behind two front doors.
6
+ //
7
+ // `uf build` writes three things: a client bundle and prerendered HTML in
8
+ // `dist/`, and a server bundle in `.uf/build/server/server.js` that exports
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.
12
+ //
13
+ // `uf preview` and `uf start` are the two front doors, and they share
14
+ // everything below on purpose. A preview whose answers differ from the
15
+ // production server's is worse than no preview, because it is checked and
16
+ // believed. The difference between the two commands is which socket the
17
+ // handler is bolted to, not what it decides:
18
+ //
19
+ // preview — Vite's own preview server, with this handler behind its static
20
+ // middleware, so `vite.preview.proxy`, `vite.preview.https`,
21
+ // `headers` and `cors` are in effect and what is being checked is
22
+ // the build *as Vite serves it*.
23
+ // start — `node:http`, with no bundler in the process, because a host
24
+ // running a production build should not need Vite installed to
25
+ // answer a request.
26
+ //
27
+ // # Where the answering actually happens
28
+ //
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.
34
+ //
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.
45
+ //
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.
73
+
74
+ import { readFile, stat } from "node:fs/promises";
75
+ import path from "node:path";
76
+ import { pathToFileURL } from "node:url";
77
+
78
+ /**
79
+ * `@uniflowed/server`'s two halves, loaded once.
80
+ *
81
+ * Cached as the promise rather than the modules, so two concurrent callers
82
+ * share one import rather than racing to start two.
83
+ */
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
+ }
92
+
93
+ /**
94
+ * Everything a served build consists of.
95
+ *
96
+ * Read once at startup rather than per request: the manifest does not change
97
+ * while the server runs, and importing the server bundle again per request
98
+ * would re-evaluate every module in the application.
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
+ *
105
+ * @param {{root: string, outDir: string, serverDir: string}} build
106
+ */
107
+ export async function loadBuild({ root, outDir, serverDir }) {
108
+ const distDir = path.resolve(root, outDir);
109
+ const entryFile = path.join(path.resolve(root, serverDir), "server.js");
110
+
111
+ // Named separately, because the two failures have different fixes and a
112
+ // combined "run uf build" would be wrong for one of them: a `dist/` with no
113
+ // server bundle beside it is what a `.uf/` that was cleaned looks like.
114
+ await readable(entryFile, `the server bundle is missing at ${entryFile}`);
115
+ const manifestFile = path.join(distDir, ".vite", "manifest.json");
116
+ await readable(manifestFile, `the client manifest is missing at ${manifestFile}`);
117
+
118
+ const manifest = JSON.parse(await readFile(manifestFile, "utf8"));
119
+ const entry = await import(pathToFileURL(entryFile).href);
120
+ await deployment();
121
+ return { entry, assets: assetsFromManifest(manifest), distDir };
122
+ }
123
+
124
+ async function readable(file, message) {
125
+ try {
126
+ await stat(file);
127
+ } catch {
128
+ throw new Error(`uf: ${message}; run \`uf build\` first`);
129
+ }
130
+ }
131
+
132
+ /**
133
+ * The tags a rendered document needs, from the client build's manifest.
134
+ *
135
+ * Two walks over the manifest, because the two answers are different. A
136
+ * `modulepreload` is worth emitting only for a chunk this document will
137
+ * certainly load, which is the entry's *static* imports. A stylesheet has to
138
+ * be emitted for anything the page might render, and the router loads every
139
+ * route module dynamically — so a stylesheet imported by a layout is reached
140
+ * through `dynamicImports` and through nothing else. Following only the static
141
+ * graph, as this did, meant a layout could import a stylesheet and the built
142
+ * HTML would silently ship without it.
143
+ *
144
+ * The cost is that a project with per-route stylesheets links all of them on
145
+ * every page. Narrowing that needs the route table to say which chunk each
146
+ * route came from, which the manifest alone cannot tell us.
147
+ *
148
+ * The entry is found by its `isEntry` flag rather than by key, because a
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.
155
+ */
156
+ export function assetsFromManifest(manifest) {
157
+ const entry = Object.values(manifest).find((chunk) => chunk.isEntry);
158
+ if (entry == null) throw new Error("uf: the client manifest has no entry chunk");
159
+
160
+ const styles = new Set(entry.css ?? []);
161
+ const seen = new Set();
162
+ const collectStyles = (chunk) => {
163
+ for (const imported of [...(chunk.imports ?? []), ...(chunk.dynamicImports ?? [])]) {
164
+ if (seen.has(imported)) continue;
165
+ seen.add(imported);
166
+ const dependency = manifest[imported];
167
+ if (dependency == null) continue;
168
+ for (const css of dependency.css ?? []) styles.add(css);
169
+ collectStyles(dependency);
170
+ }
171
+ };
172
+ collectStyles(entry);
173
+
174
+ const preloads = new Set();
175
+ const collectPreloads = (chunk) => {
176
+ for (const imported of chunk.imports ?? []) {
177
+ const dependency = manifest[imported];
178
+ if (dependency == null || preloads.has(dependency.file)) continue;
179
+ preloads.add(dependency.file);
180
+ collectPreloads(dependency);
181
+ }
182
+ };
183
+ collectPreloads(entry);
184
+
185
+ return {
186
+ scripts: [`/${entry.file}`],
187
+ styles: [...styles].map((file) => `/${file}`),
188
+ preloads: [...preloads].map((file) => `/${file}`),
189
+ };
190
+ }
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
+
227
+ /**
228
+ * The application half: route handlers, then rendering.
229
+ *
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.
235
+ *
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.
240
+ *
241
+ * @param {{entry: object, assets: object}} build
242
+ */
243
+ export function createApplicationHandler({ entry, assets }) {
244
+ const ready = deployment().then(({ createFetchHandler }) =>
245
+ createFetchHandler({ app: entry, document: assets }),
246
+ );
247
+ return async function handle(request) {
248
+ return (await ready)(request);
249
+ };
250
+ }
251
+
252
+ /**
253
+ * The static half: a file under `root`, or `null` for the caller to carry on.
254
+ *
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.
257
+ */
258
+ export function createStaticHandler({ root }) {
259
+ const ready = deployment().then(({ createStaticHandler: create }) => create({ root }));
260
+ return async function serveStatic(request) {
261
+ return (await ready)(request);
262
+ };
263
+ }
264
+
265
+ /**
266
+ * Static files, then the application: the whole of what a built uf app serves.
267
+ *
268
+ * Static first, and that ordering is a compatibility requirement rather than a
269
+ * preference. Vite's preview server runs its own file middleware before
270
+ * anything added afterwards can see the request, so `uf preview` serves a file
271
+ * first whether or not this agrees — and `uf start` disagreeing would mean a
272
+ * project whose handler path collides with a file in `public/` behaves one way
273
+ * when it is checked and the other way when it is deployed.
274
+ *
275
+ * @param {{entry: object, assets: object, distDir: string}} build
276
+ */
277
+ export function createServeHandler({ entry, assets, distDir }) {
278
+ const serveStatic = createStaticHandler({ root: distDir });
279
+ const application = createApplicationHandler({ entry, assets });
280
+ return async function handle(request) {
281
+ return (await serveStatic(request)) ?? (await application(request));
282
+ };
283
+ }
284
+
285
+ /**
286
+ * A `Request`/`Response` handler as a Node request listener.
287
+ *
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.
292
+ *
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.
300
+ */
301
+ export function nodeListener(handle, entry) {
302
+ const ready = deployment().then(({ nodeListener: create }) =>
303
+ create(handle, { beginRequest: entry.beginRequest }),
304
+ );
305
+ return async function listener(incoming, outgoing) {
306
+ return (await ready)(incoming, outgoing);
307
+ };
308
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniflowed/vite",
3
- "version": "0.0.0-alpha.6",
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.6",
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",