@uniflowed/vite 0.0.0-alpha.4 → 0.0.0-alpha.41
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/driver.js +2269 -188
- package/index.js +1180 -80
- package/internal/a11y-runtime.js +234 -0
- package/internal/a11y.js +102 -0
- package/internal/assets.js +644 -0
- package/internal/barrel-imports.js +459 -0
- package/internal/compile-assets.js +67 -0
- package/internal/config.js +39 -6
- package/internal/dev-state.js +161 -0
- package/internal/devtools.js +117 -0
- package/internal/diagnostics.js +369 -0
- package/internal/events.js +23 -0
- package/internal/flight.js +777 -0
- package/internal/flow-keywords.js +1 -1
- package/internal/frontmatter.js +33 -0
- package/internal/http.js +98 -0
- package/internal/module-graph.js +134 -0
- package/internal/native-web.js +26 -0
- package/internal/openapi.js +218 -0
- package/internal/relay.js +74 -0
- package/internal/routes.js +1581 -72
- package/internal/rsc.js +475 -0
- package/internal/serve.js +741 -0
- package/internal/server-components.js +109 -0
- package/internal/worker-builtins.js +170 -0
- package/package.json +27 -5
|
@@ -0,0 +1,741 @@
|
|
|
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`. `packages/server/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 store is shared by every copy of one release of `@uniflowed/server`,
|
|
68
|
+
// and the release that matters is the one inside the application bundle. A
|
|
69
|
+
// host that resolved its own could be holding another release, would begin a
|
|
70
|
+
// request the application cannot see, and nothing would fail loudly — the
|
|
71
|
+
// guard would run, the page would render, and every `cookies()` in it would
|
|
72
|
+
// throw as though no 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
|
+
import("@uniflowed/server/cache"),
|
|
90
|
+
]).then(([application, host, cache]) => ({ ...application, ...host, ...cache }));
|
|
91
|
+
return deploymentModules;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* The cache `rendering.cache` describes, or `undefined` for no cache at all.
|
|
96
|
+
*
|
|
97
|
+
* `undefined` rather than a store with both switches off, and the difference is
|
|
98
|
+
* visible from an application: a handler with no `cache` installs nothing on
|
|
99
|
+
* the request, so `revalidateTag()` raises "there is not one here" instead of
|
|
100
|
+
* reporting that it expired nothing. A project that turned the cache off should
|
|
101
|
+
* be told that it did, not handed a cache that quietly does nothing.
|
|
102
|
+
*
|
|
103
|
+
* One store per server process, built when the handler is. With no
|
|
104
|
+
* `rendering.cache.store` that is the whole of what "in memory, per process"
|
|
105
|
+
* means in practice: `uf preview` and `uf start` each hold one, and two of them
|
|
106
|
+
* running at once share nothing. With one, the two processes share whatever the
|
|
107
|
+
* provider is in front of — see [`providerFor`].
|
|
108
|
+
*
|
|
109
|
+
* The store's remaining options are still not configurable from `uf.config.js`
|
|
110
|
+
* and are still not named here: `maxEntries` and `now` are facts about one
|
|
111
|
+
* process's heap and one process's clock, and a parameter threaded through for
|
|
112
|
+
* a setting nobody can set would be the shape of configurability with none of
|
|
113
|
+
* the substance.
|
|
114
|
+
*
|
|
115
|
+
* @param {{route?: boolean, fetch?: boolean, data?: boolean, store?: string, storeDir?: string} | undefined} declared
|
|
116
|
+
* @param {(options?: object) => object} createCacheStore
|
|
117
|
+
* @param {{root: string, build: string | null}} where
|
|
118
|
+
*/
|
|
119
|
+
async function cacheFor(declared, createCacheStore, where) {
|
|
120
|
+
const route = declared?.route === true;
|
|
121
|
+
const fetchCache = declared?.fetch === true;
|
|
122
|
+
const data = declared?.data === true;
|
|
123
|
+
if (!route && !fetchCache && !data) return undefined;
|
|
124
|
+
const provider = await providerFor(declared, where);
|
|
125
|
+
const store =
|
|
126
|
+
provider == null ? createCacheStore() : createCacheStore({ provider, build: where.build });
|
|
127
|
+
return { store, route, fetch: fetchCache, data };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* The durable provider `rendering.cache.store` names, or `null` for memory.
|
|
132
|
+
*
|
|
133
|
+
* Three answers, and the third is the one that matters to
|
|
134
|
+
* `docs/red-lines.md`'s third line. `"memory"` — the default, and what every
|
|
135
|
+
* project that says nothing gets — keeps the store exactly as it was.
|
|
136
|
+
* `"filesystem"` is uf's built-in, and it is a convenience rather than an
|
|
137
|
+
* architecture. Anything else is a **module specifier**, resolved from the
|
|
138
|
+
* project, exporting `createCacheProvider`: the same shape `builder.module`
|
|
139
|
+
* has, chosen for the same reason that document gives — "a provider a project
|
|
140
|
+
* can replace has to be a name it can write, and an enum with one variant
|
|
141
|
+
* cannot become one without a release of uf".
|
|
142
|
+
*
|
|
143
|
+
* So a project with a Redis, a KV namespace or an S3 bucket writes twenty lines
|
|
144
|
+
* against `@uniflowed/server/cache`'s `CacheProvider` type, names the module
|
|
145
|
+
* here, and uf never learns which of those it was.
|
|
146
|
+
*
|
|
147
|
+
* # Why a missing build identity is a refusal
|
|
148
|
+
*
|
|
149
|
+
* Because the alternatives are both worse. Falling back to memory would give a
|
|
150
|
+
* project that asked for a cache surviving restarts one that does not, and the
|
|
151
|
+
* symptom is a `MISS` on every cold request — indistinguishable from a cache
|
|
152
|
+
* that is simply cold. Generating an identity per process would be worse again:
|
|
153
|
+
* four servers would write four copies of everything into one directory and
|
|
154
|
+
* read none of each other's. The deployment rules say a target that cannot
|
|
155
|
+
* provide a durable store has to say so, and this is a host saying so.
|
|
156
|
+
*
|
|
157
|
+
* @param {{store?: string, storeDir?: string} | undefined} declared
|
|
158
|
+
* @param {{root: string, build: string | null}} where
|
|
159
|
+
*/
|
|
160
|
+
async function providerFor(declared, { root, build, regenerates }) {
|
|
161
|
+
// A build that regenerates pages keeps what it regenerated on disk unless the
|
|
162
|
+
// project named a store. A restart that took every regenerated page back to
|
|
163
|
+
// the build's copy would be a server whose pages went back in time, which is
|
|
164
|
+
// the one thing regeneration is for not doing. `"memory"`, said out loud, is
|
|
165
|
+
// still memory.
|
|
166
|
+
const named = declared?.store ?? (regenerates === true ? "filesystem" : "memory");
|
|
167
|
+
if (named === "memory") return null;
|
|
168
|
+
if (build == null) {
|
|
169
|
+
throw new Error(
|
|
170
|
+
`uf: rendering.cache.store is ${JSON.stringify(named)}, which keeps entries between ` +
|
|
171
|
+
"restarts, and there is no build identity to key them by. `uf build` writes one " +
|
|
172
|
+
"beside the server bundle; set UF_BUILD_ID to name it yourself. Without one, a " +
|
|
173
|
+
"deploy would answer the new build's URLs with the previous build's documents.",
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
const directory = path.resolve(root, declared?.storeDir ?? path.join(".uf", "cache", "route"));
|
|
177
|
+
if (named === "filesystem") {
|
|
178
|
+
const { createFilesystemCache } = await import("@uniflowed/server/cache/filesystem");
|
|
179
|
+
return createFilesystemCache({ directory });
|
|
180
|
+
}
|
|
181
|
+
const provider = await import(providerSpecifier(root, named));
|
|
182
|
+
const create = provider.createCacheProvider ?? provider.default;
|
|
183
|
+
if (typeof create !== "function") {
|
|
184
|
+
throw new Error(
|
|
185
|
+
`uf: rendering.cache.store names ${JSON.stringify(named)}, which exports no ` +
|
|
186
|
+
"`createCacheProvider`. A durable cache provider is a module exporting that " +
|
|
187
|
+
"function; see @uniflowed/server/cache's CacheProvider type for what it returns.",
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
return create({ build, directory });
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Everything a served build consists of.
|
|
195
|
+
*
|
|
196
|
+
* Read once at startup rather than per request: the manifest does not change
|
|
197
|
+
* while the server runs, and importing the server bundle again per request
|
|
198
|
+
* would re-evaluate every module in the application.
|
|
199
|
+
*
|
|
200
|
+
* `@uniflowed/server` is loaded here too, and not lazily on the first request:
|
|
201
|
+
* a missing or broken install should fail the command that starts the server,
|
|
202
|
+
* with the message the import raises, rather than a minute later inside
|
|
203
|
+
* whichever request happened to arrive first.
|
|
204
|
+
*
|
|
205
|
+
* @param {{root: string, outDir: string, serverDir: string}} build
|
|
206
|
+
*/
|
|
207
|
+
export async function loadBuild({ root, outDir, serverDir }) {
|
|
208
|
+
const distDir = path.resolve(root, outDir);
|
|
209
|
+
const entryFile = path.join(path.resolve(root, serverDir), "server.js");
|
|
210
|
+
|
|
211
|
+
// Named separately, because the two failures have different fixes and a
|
|
212
|
+
// combined "run uf build" would be wrong for one of them: a `dist/` with no
|
|
213
|
+
// server bundle beside it is what a `.uf/` that was cleaned looks like.
|
|
214
|
+
await readable(entryFile, `the server bundle is missing at ${entryFile}`);
|
|
215
|
+
const manifestFile = path.join(distDir, ".vite", "manifest.json");
|
|
216
|
+
await readable(manifestFile, `the client manifest is missing at ${manifestFile}`);
|
|
217
|
+
|
|
218
|
+
const manifest = JSON.parse(await readFile(manifestFile, "utf8"));
|
|
219
|
+
const entry = await import(pathToFileURL(entryFile).href);
|
|
220
|
+
await deployment();
|
|
221
|
+
const build = await buildIdentity(root, serverDir);
|
|
222
|
+
const regeneration = await readRegeneration(path.resolve(root, serverDir));
|
|
223
|
+
return {
|
|
224
|
+
entry,
|
|
225
|
+
assets: await documentAssetsFor(path.resolve(root, serverDir), manifest),
|
|
226
|
+
distDir,
|
|
227
|
+
root,
|
|
228
|
+
build,
|
|
229
|
+
regeneration,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** What `uf build` records a document's tags in, beside the server bundle. */
|
|
234
|
+
export const DOCUMENT_ASSETS_FILE = "uf-document-assets.json";
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* The tags a served document needs: the ones `uf build` recorded, or — for a
|
|
238
|
+
* build from before it recorded them — the client manifest's.
|
|
239
|
+
*
|
|
240
|
+
* Recorded rather than recomputed, because the client manifest is no longer the
|
|
241
|
+
* whole answer. An application React Server Components render links the
|
|
242
|
+
* stylesheets its rsc graph emitted and the ones each client module's chunk
|
|
243
|
+
* carries, and neither is reachable from the client entry the manifest is walked
|
|
244
|
+
* from — so a server that recomputed the tags rendered every page without the
|
|
245
|
+
* stylesheets its prerendered pages had. `uf start`, `uf preview`, `--adapter`
|
|
246
|
+
* and `--compile` all read them from here.
|
|
247
|
+
*
|
|
248
|
+
* @param {string} serverDir absolute path of the server bundle's directory
|
|
249
|
+
* @param {object} manifest the client build's Vite manifest
|
|
250
|
+
*/
|
|
251
|
+
export async function documentAssetsFor(serverDir, manifest) {
|
|
252
|
+
const file = path.join(serverDir, DOCUMENT_ASSETS_FILE);
|
|
253
|
+
let recorded;
|
|
254
|
+
try {
|
|
255
|
+
recorded = await readFile(file, "utf8");
|
|
256
|
+
} catch {
|
|
257
|
+
return assetsFromManifest(manifest);
|
|
258
|
+
}
|
|
259
|
+
try {
|
|
260
|
+
return JSON.parse(recorded);
|
|
261
|
+
} catch {
|
|
262
|
+
throw new Error(`uf: ${file} is not the JSON \`uf build\` writes; run \`uf build\` again`);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* The file beside the server bundle that names the pages a build regenerates.
|
|
268
|
+
*
|
|
269
|
+
* Written by `driver.js`'s build, and only for a build that has such a page.
|
|
270
|
+
*/
|
|
271
|
+
export const REGENERATION_FILE = "regenerate.json";
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Where a regenerated page's document goes, under the build's output directory.
|
|
275
|
+
*
|
|
276
|
+
* Somewhere no static half answers the page's own URL, which is the point: a
|
|
277
|
+
* file at `dist/posts/a/index.html` would be served by every host before the
|
|
278
|
+
* server saw the request, forever, whatever the page's lifetime said.
|
|
279
|
+
*/
|
|
280
|
+
export const REGENERATED_DIRECTORY = "__uf/regenerate";
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* The pages this build regenerates, or `undefined` for a build that has none.
|
|
284
|
+
*
|
|
285
|
+
* `undefined` rather than an empty manifest, so a build with nothing to
|
|
286
|
+
* regenerate serves exactly as every build did before regeneration existed.
|
|
287
|
+
*/
|
|
288
|
+
export async function readRegeneration(serverDir) {
|
|
289
|
+
let text;
|
|
290
|
+
try {
|
|
291
|
+
text = await readFile(path.join(serverDir, REGENERATION_FILE), "utf8");
|
|
292
|
+
} catch (error) {
|
|
293
|
+
if (error?.code === "ENOENT") return undefined;
|
|
294
|
+
throw error;
|
|
295
|
+
}
|
|
296
|
+
return JSON.parse(text);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* What `import()` should be given for a provider a project named.
|
|
301
|
+
*
|
|
302
|
+
* A relative path in `uf.config.js` is relative to *the project*, which is what
|
|
303
|
+
* anybody writing `"./cache/redis.js"` means and is not what `import()` from
|
|
304
|
+
* this module would do — it would look beside `@uniflowed/vite`, find nothing,
|
|
305
|
+
* and report a missing module the config file does not mention. `builder.module`
|
|
306
|
+
* settled the same question the same way in `uf_cli`'s `project_directory`.
|
|
307
|
+
*
|
|
308
|
+
* A bare specifier is left alone: `"@acme/uf-cache-redis"` is a package, and
|
|
309
|
+
* resolving it is Node's job and not this function's.
|
|
310
|
+
*
|
|
311
|
+
* @param {string} root
|
|
312
|
+
* @param {string} named
|
|
313
|
+
*/
|
|
314
|
+
export function providerSpecifier(root, named) {
|
|
315
|
+
if (!named.startsWith(".") && !path.isAbsolute(named)) return named;
|
|
316
|
+
return pathToFileURL(path.resolve(root, named)).href;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/** What `uf build` writes its identity into, beside the server bundle. */
|
|
320
|
+
export const BUILD_ID_FILE = "uf-build-id";
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* The identity of the build being served, or `null`.
|
|
324
|
+
*
|
|
325
|
+
* Only a durable cache reads it, and only a durable cache needs it: an entry
|
|
326
|
+
* that cannot outlive the process cannot outlive the build either, so every
|
|
327
|
+
* command that keeps its cache in memory is entitled to `null` here and never
|
|
328
|
+
* looks. `packages/server/internal/cache-key.js` argues the rest.
|
|
329
|
+
*
|
|
330
|
+
* `UF_BUILD_ID` first, then the file `uf build` wrote. The environment wins for
|
|
331
|
+
* the reason it wins in `crates/uf_rsc`'s `BuildId::from_env_or_generate`,
|
|
332
|
+
* which reads the same variable for the same kind of fact: a deployment that
|
|
333
|
+
* needs two artefacts to *be* one build — a blue/green pair, a rebuild of a
|
|
334
|
+
* tagged commit — has no other way to say so.
|
|
335
|
+
*
|
|
336
|
+
* `null` rather than a generated fallback, and that is the whole point of the
|
|
337
|
+
* function. A per-process identity would give four servers four caches with a
|
|
338
|
+
* shared disk between them, which is worse than four memories: it would write
|
|
339
|
+
* four copies of everything and read none of them. Whoever asked for a durable
|
|
340
|
+
* store is told there is no build to key it by, and gets to fix it.
|
|
341
|
+
*/
|
|
342
|
+
export async function buildIdentity(root, serverDir) {
|
|
343
|
+
const named = process.env.UF_BUILD_ID;
|
|
344
|
+
if (typeof named === "string" && named !== "") return named;
|
|
345
|
+
try {
|
|
346
|
+
const file = path.join(path.resolve(root, serverDir), BUILD_ID_FILE);
|
|
347
|
+
const value = (await readFile(file, "utf8")).trim();
|
|
348
|
+
return value === "" ? null : value;
|
|
349
|
+
} catch {
|
|
350
|
+
return null;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
async function readable(file, message) {
|
|
355
|
+
try {
|
|
356
|
+
await stat(file);
|
|
357
|
+
} catch {
|
|
358
|
+
throw new Error(`uf: ${message}; run \`uf build\` first`);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* The tags a rendered document needs, from the client build's manifest.
|
|
364
|
+
*
|
|
365
|
+
* Two walks over the manifest, because the two answers are different. A
|
|
366
|
+
* `modulepreload` is worth emitting only for a chunk this document will
|
|
367
|
+
* certainly load, which is the entry's *static* imports. A stylesheet has to
|
|
368
|
+
* be emitted for anything the page might render, and the router loads every
|
|
369
|
+
* route module dynamically — so a stylesheet imported by a layout is reached
|
|
370
|
+
* through `dynamicImports` and through nothing else. Following only the static
|
|
371
|
+
* graph, as this did, meant a layout could import a stylesheet and the built
|
|
372
|
+
* HTML would silently ship without it.
|
|
373
|
+
*
|
|
374
|
+
* The cost is that a project with per-route stylesheets links all of them on
|
|
375
|
+
* every page. Narrowing that needs the route table to say which chunk each
|
|
376
|
+
* route came from, which the manifest alone cannot tell us.
|
|
377
|
+
*
|
|
378
|
+
* The entry is found by its `isEntry` flag rather than by key, because a
|
|
379
|
+
* virtual module's manifest key is an implementation detail of the bundler.
|
|
380
|
+
*
|
|
381
|
+
* This is the one piece of serving a build that is genuinely Vite's — a Vite
|
|
382
|
+
* manifest, read the way Vite writes it — which is why it stayed behind when
|
|
383
|
+
* the rest moved to `@uniflowed/server`. `uf build --adapter` calls it too,
|
|
384
|
+
* at build time, and bakes the answer into what it emits.
|
|
385
|
+
*/
|
|
386
|
+
export function assetsFromManifest(manifest, base = "") {
|
|
387
|
+
// `client` by name first. An application React Server Components render
|
|
388
|
+
// gives the client build one entry per client module as well, and the
|
|
389
|
+
// document's script is the application's entry, not whichever of those the
|
|
390
|
+
// manifest happens to list first.
|
|
391
|
+
const chunks = Object.values(manifest);
|
|
392
|
+
const entry =
|
|
393
|
+
chunks.find((chunk) => chunk.isEntry && chunk.name === "client") ??
|
|
394
|
+
chunks.find((chunk) => chunk.isEntry);
|
|
395
|
+
if (entry == null) throw new Error("uf: the client manifest has no entry chunk");
|
|
396
|
+
|
|
397
|
+
const styles = new Set(entry.css ?? []);
|
|
398
|
+
const seen = new Set();
|
|
399
|
+
const collectStyles = (chunk) => {
|
|
400
|
+
for (const imported of [...(chunk.imports ?? []), ...(chunk.dynamicImports ?? [])]) {
|
|
401
|
+
if (seen.has(imported)) continue;
|
|
402
|
+
seen.add(imported);
|
|
403
|
+
const dependency = manifest[imported];
|
|
404
|
+
if (dependency == null) continue;
|
|
405
|
+
for (const css of dependency.css ?? []) styles.add(css);
|
|
406
|
+
collectStyles(dependency);
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
collectStyles(entry);
|
|
410
|
+
|
|
411
|
+
const preloads = new Set();
|
|
412
|
+
const collectPreloads = (chunk) => {
|
|
413
|
+
for (const imported of chunk.imports ?? []) {
|
|
414
|
+
const dependency = manifest[imported];
|
|
415
|
+
if (dependency == null || preloads.has(dependency.file)) continue;
|
|
416
|
+
preloads.add(dependency.file);
|
|
417
|
+
collectPreloads(dependency);
|
|
418
|
+
}
|
|
419
|
+
};
|
|
420
|
+
collectPreloads(entry);
|
|
421
|
+
|
|
422
|
+
// Under `app.router.basePath` when there is one: a prerendered document is
|
|
423
|
+
// written outside Vite's HTML transform, so nothing else would put it there.
|
|
424
|
+
return {
|
|
425
|
+
scripts: [`${base}/${entry.file}`],
|
|
426
|
+
styles: [...styles].map((file) => `${base}/${file}`),
|
|
427
|
+
preloads: [...preloads].map((file) => `${base}/${file}`),
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Answer one request inside it, and settle it when the answer has been written.
|
|
433
|
+
*
|
|
434
|
+
* `body` is everything that decides the response *and writes it*; this is the
|
|
435
|
+
* line after. `settle` is in a `finally` because a request that failed is
|
|
436
|
+
* still a request that happened: a middleware that logged the arrival is owed
|
|
437
|
+
* its callback whether the render threw or not, and `drainDeferred` already
|
|
438
|
+
* reports a failing task rather than propagating it.
|
|
439
|
+
*
|
|
440
|
+
* `entry.beginRequest` and not an import: the request store is shared by every
|
|
441
|
+
* copy of one release of `@uniflowed/server`, and the release that matters is
|
|
442
|
+
* the one inside the application bundle. See `serverModuleSource` in
|
|
443
|
+
* `./routes.js`.
|
|
444
|
+
*
|
|
445
|
+
* The one case this cannot be exact about is a request uf hands back rather
|
|
446
|
+
* than answers: a caller whose `catch` is `next(error)` gives the response to
|
|
447
|
+
* Vite's chain, which writes a 500 at a moment nothing here can observe, so
|
|
448
|
+
* such a request settles when uf lets go of it. `nodeListener` and the
|
|
449
|
+
* compiled binary write their own failures and settle after them. It is worth
|
|
450
|
+
* naming rather than papering over, and it is the failure path of a request
|
|
451
|
+
* that already went wrong — not the ordinary one this exists for.
|
|
452
|
+
*
|
|
453
|
+
* @param {{beginRequest: (request: Request) => {context: object, run: <T>(body: () => Promise<T>) => Promise<T>, settle: () => Promise<void>}}} entry
|
|
454
|
+
* @param {Request} request
|
|
455
|
+
* @param {() => Promise<mixed>} body
|
|
456
|
+
*/
|
|
457
|
+
export async function withRequest(entry, request, body) {
|
|
458
|
+
const { run, settle } = await beginRequest(entry, request);
|
|
459
|
+
try {
|
|
460
|
+
return await run(body);
|
|
461
|
+
} finally {
|
|
462
|
+
await settle();
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Begin a request on this host, with what this host can do already on it.
|
|
468
|
+
*
|
|
469
|
+
* The half of [`withRequest`] that a caller which may *not* answer needs.
|
|
470
|
+
* `uf dev` runs the application's middleware, its action endpoint and its
|
|
471
|
+
* dispatcher for every request, and hands the ones none of them claimed back
|
|
472
|
+
* to Vite's chain — at which point the response is written somewhere this
|
|
473
|
+
* module cannot see, so settling has to wait for the socket rather than for a
|
|
474
|
+
* `finally` here. A caller that always answers should use [`withRequest`] and
|
|
475
|
+
* not think about it.
|
|
476
|
+
*
|
|
477
|
+
* `entry.beginRequest` and not an import: the request store is shared by every
|
|
478
|
+
* copy of one release of `@uniflowed/server`, and the release that matters is
|
|
479
|
+
* the one inside the application bundle. See `serverModuleSource` in
|
|
480
|
+
* `./routes.js`.
|
|
481
|
+
*
|
|
482
|
+
* What this host can do is put on the request the way `createFetchHandler`
|
|
483
|
+
* puts it on the one it owns. `uf dev` and `uf build --compile` reach a route
|
|
484
|
+
* handler without going through that function, and a handler that streams
|
|
485
|
+
* events or queues work has to get the same answer from all four front doors —
|
|
486
|
+
* a capability that is present under `uf start` and absent under `uf dev` is
|
|
487
|
+
* the difference this whole seam exists to remove.
|
|
488
|
+
*
|
|
489
|
+
* `nodeCapabilities`, because both of those *are* a Node process with a
|
|
490
|
+
* socket: a body reaches the client as it is written, and the process is still
|
|
491
|
+
* there afterwards. Neither passes an upgrader or a queue, because uf defines
|
|
492
|
+
* both and implements neither.
|
|
493
|
+
*
|
|
494
|
+
* @param {{beginRequest: (request: Request) => {context: object, run: <T>(body: () => Promise<T>) => Promise<T>, settle: () => Promise<void>}}} entry
|
|
495
|
+
* @param {Request} request
|
|
496
|
+
*/
|
|
497
|
+
export async function beginRequest(entry, request) {
|
|
498
|
+
const lifecycle = entry.beginRequest(request);
|
|
499
|
+
const { nodeCapabilities } = await deployment();
|
|
500
|
+
lifecycle.context.capabilities ??= nodeCapabilities();
|
|
501
|
+
return lifecycle;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* The application half: route handlers, then rendering.
|
|
506
|
+
*
|
|
507
|
+
* `@uniflowed/server/fetch`'s `createFetchHandler`, reached through the
|
|
508
|
+
* dynamic import above. Kept as a function here — rather than making every
|
|
509
|
+
* caller await the module — because the two servers construct their handler
|
|
510
|
+
* before they take a socket, and an `await` in that position would put the
|
|
511
|
+
* import between the port and the first request rather than before both.
|
|
512
|
+
*
|
|
513
|
+
* It must be called inside a request its caller began; it begins none, because
|
|
514
|
+
* it has a `Response` in hand and not a response on the wire. A caller that
|
|
515
|
+
* forgets is not left to discover it: `entry.runMiddleware` refuses outside a
|
|
516
|
+
* request and names what establishes one. See "Who owns the request" above.
|
|
517
|
+
*
|
|
518
|
+
* `cache` is `rendering.cache` from `uf.config.js`, straight through: this is
|
|
519
|
+
* the point where four switches that used to reach a JSON file and nothing else
|
|
520
|
+
* become a store a request can hit. See ubugeeei-prod/uf#277.
|
|
521
|
+
*
|
|
522
|
+
* `root` and `build` come with it, and only the cache reads either: `root` is
|
|
523
|
+
* where a `storeDir` is resolved from and `build` is what a durable entry is
|
|
524
|
+
* keyed by. Both are `undefined` for a caller that constructs a handler by
|
|
525
|
+
* hand, which is the memory-only store and needs neither.
|
|
526
|
+
*
|
|
527
|
+
* @param {{entry: object, assets: object, cache?: object, root?: string, build?: string | null}} build
|
|
528
|
+
*/
|
|
529
|
+
export function createApplicationHandler({ entry, assets, cache, root, build, regeneration }) {
|
|
530
|
+
const ready = deployment().then(
|
|
531
|
+
async ({ createFetchHandler, createCacheStore, nodeCapabilities }) =>
|
|
532
|
+
createFetchHandler({
|
|
533
|
+
app: entry,
|
|
534
|
+
document: assets,
|
|
535
|
+
cache: await cacheFor(cache, createCacheStore, {
|
|
536
|
+
root: root ?? process.cwd(),
|
|
537
|
+
build: build ?? null,
|
|
538
|
+
regenerates: regeneration != null,
|
|
539
|
+
}),
|
|
540
|
+
// The pages this build regenerates, from the manifest beside the server
|
|
541
|
+
// bundle. Absent for a build with none, which then serves exactly as it
|
|
542
|
+
// did before regeneration existed.
|
|
543
|
+
...(regeneration == null ? {} : { regeneration }),
|
|
544
|
+
// `uf preview` and `uf start` are a Node process with a socket, which is
|
|
545
|
+
// what a deployed `--adapter node` build is too — so a route handler
|
|
546
|
+
// that streams events answers the same way in the preview it is checked
|
|
547
|
+
// in and in the deployment it ends up as. See `withRequest` above.
|
|
548
|
+
capabilities: nodeCapabilities(),
|
|
549
|
+
}),
|
|
550
|
+
);
|
|
551
|
+
return async function handle(request) {
|
|
552
|
+
return (await ready)(request);
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* The static half: a file under `root`, or `null` for the caller to carry on.
|
|
558
|
+
*
|
|
559
|
+
* `@uniflowed/server/node`'s, for the same reason as above: what a deployment
|
|
560
|
+
* runs and what `uf start` runs have to be the same code, not the same idea.
|
|
561
|
+
*/
|
|
562
|
+
export function createStaticHandler({ root }) {
|
|
563
|
+
const ready = deployment().then(({ createStaticHandler: create }) => create({ root }));
|
|
564
|
+
return async function serveStatic(request) {
|
|
565
|
+
return (await ready)(request);
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* Static files, then the application: the whole of what a built uf app serves.
|
|
571
|
+
*
|
|
572
|
+
* Static first, and that ordering is a compatibility requirement rather than a
|
|
573
|
+
* preference. Vite's preview server runs its own file middleware before
|
|
574
|
+
* anything added afterwards can see the request, so `uf preview` serves a file
|
|
575
|
+
* first whether or not this agrees — and `uf start` disagreeing would mean a
|
|
576
|
+
* project whose handler path collides with a file in `public/` behaves one way
|
|
577
|
+
* when it is checked and the other way when it is deployed.
|
|
578
|
+
*
|
|
579
|
+
* It is `@uniflowed/server/node`'s own `createServeHandler`, the one a deployed
|
|
580
|
+
* `server.js` runs, rather than the two halves composed a second time here.
|
|
581
|
+
* That one also hands the application the build's files when the static half
|
|
582
|
+
* has nothing, which is how a regenerated page starts from the document the
|
|
583
|
+
* build wrote; a composition of its own here would be a `uf start` whose
|
|
584
|
+
* regenerated pages rendered on their first request while a deployment's did
|
|
585
|
+
* not.
|
|
586
|
+
*
|
|
587
|
+
* It is handed the bundle's `routing` too, so `app.router`'s redirects answer
|
|
588
|
+
* before the files and its headers go on whatever answers, here exactly as in
|
|
589
|
+
* a deployment. `uf preview` puts the same two in front of Vite's file
|
|
590
|
+
* middleware as well; see [`answerRouting`].
|
|
591
|
+
*
|
|
592
|
+
* @param {{entry: object, assets: object, distDir: string, cache?: object, root?: string, build?: string | null, regeneration?: object}} build
|
|
593
|
+
*/
|
|
594
|
+
export function createServeHandler({ entry, assets, distDir, cache, root, build, regeneration }) {
|
|
595
|
+
const application = createApplicationHandler({ entry, assets, cache, root, build, regeneration });
|
|
596
|
+
const ready = deployment().then(({ createServeHandler: create }) =>
|
|
597
|
+
create({ staticDir: distDir, handle: application, routing: entry.routing }),
|
|
598
|
+
);
|
|
599
|
+
return async function handle(request) {
|
|
600
|
+
return (await ready)(request);
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Whether `routing` has anything to say in front of a file server.
|
|
606
|
+
*
|
|
607
|
+
* Redirects and headers are the two that do; a rewrite is the application's.
|
|
608
|
+
* Asked before a middleware is mounted at all, so a project with no rules pays
|
|
609
|
+
* nothing per request under `uf dev` or `uf preview`.
|
|
610
|
+
*
|
|
611
|
+
* @param {{redirects?: unknown[], headers?: unknown[]} | undefined} routing
|
|
612
|
+
*/
|
|
613
|
+
export function answersInFrontOfFiles(routing) {
|
|
614
|
+
return (
|
|
615
|
+
(routing?.redirects?.length ?? 0) > 0 ||
|
|
616
|
+
(routing?.headers?.length ?? 0) > 0 ||
|
|
617
|
+
(routing?.basePath ?? "") !== "" ||
|
|
618
|
+
(routing?.trailingSlash ?? "ignore") !== "ignore"
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* `app.router`'s headers and redirects, for a door whose files Vite serves.
|
|
624
|
+
*
|
|
625
|
+
* `uf dev` and `uf preview` mount this in front of Vite's own middleware:
|
|
626
|
+
* the headers are pinned on the Node response, so they survive the
|
|
627
|
+
* `writeHead` Vite's file server writes its own with, and a redirect is
|
|
628
|
+
* answered before any file is looked for. `true` when it answered.
|
|
629
|
+
*
|
|
630
|
+
* @param {object} routing the bundle's `routing`
|
|
631
|
+
* @param {Request} request the address alone; see `toAddressRequest`
|
|
632
|
+
* @param {import("node:http").ServerResponse} response
|
|
633
|
+
*/
|
|
634
|
+
export async function answerRouting(routing, request, response) {
|
|
635
|
+
const { admit, headersFor, pinHeaders, send: write } = await deployment();
|
|
636
|
+
pinHeaders(response, headersFor(routing, request));
|
|
637
|
+
// A request outside the base path, the other spelling of a path, or a
|
|
638
|
+
// redirect rule: answered here, before Vite's own base middleware would
|
|
639
|
+
// answer the first in its words rather than uf's.
|
|
640
|
+
const admitted = admit(routing, request);
|
|
641
|
+
if (admitted.kind !== "answer") return false;
|
|
642
|
+
await write(response, admitted.response);
|
|
643
|
+
return true;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* A request [`answerRouting`] let through, spelled so Vite's own middleware
|
|
648
|
+
* recognises it.
|
|
649
|
+
*
|
|
650
|
+
* Vite serves under its `base` with the trailing slash, `/docs/`, and its base
|
|
651
|
+
* middleware answers every other path with a 404 of its own, the bare `/docs`
|
|
652
|
+
* included. But `/docs` is the application's root under `app.router.basePath`,
|
|
653
|
+
* and the only spelling of it unless the trailing-slash policy is `"always"`,
|
|
654
|
+
* which has already answered `/docs` with a `308` by the time this is asked.
|
|
655
|
+
* So a request for exactly the base goes on to Vite as `/docs/`, which Vite
|
|
656
|
+
* takes the base off and hands on as the root. The application is handed the
|
|
657
|
+
* root either way.
|
|
658
|
+
*
|
|
659
|
+
* @param {{basePath?: string} | undefined} routing the bundle's `routing`
|
|
660
|
+
* @param {import("node:http").IncomingMessage} request
|
|
661
|
+
*/
|
|
662
|
+
export function forViteBase(routing, request) {
|
|
663
|
+
const base = routing?.basePath ?? "";
|
|
664
|
+
const url = request.url ?? "/";
|
|
665
|
+
if (base === "") return;
|
|
666
|
+
const queryAt = url.indexOf("?");
|
|
667
|
+
const pathname = queryAt === -1 ? url : url.slice(0, queryAt);
|
|
668
|
+
if (pathname === base) {
|
|
669
|
+
request.url = `${base}/${queryAt === -1 ? "" : url.slice(queryAt)}`;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* `app.router.rewrites` for this request, for `uf dev`: the rewritten request,
|
|
675
|
+
* or `null`.
|
|
676
|
+
*
|
|
677
|
+
* `@uniflowed/server`'s `rewriteFor`, which `createFetchHandler` asks for every
|
|
678
|
+
* other front door at the same point — after the files, before the guard.
|
|
679
|
+
*
|
|
680
|
+
* @param {object | undefined} routing the bundle's `routing`
|
|
681
|
+
* @param {Request} request
|
|
682
|
+
*/
|
|
683
|
+
export async function rewriteRouting(routing, request) {
|
|
684
|
+
const { rewriteFor } = await deployment();
|
|
685
|
+
return rewriteFor(routing, request);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/**
|
|
689
|
+
* Whether a file server may answer this request, or uf has to go first.
|
|
690
|
+
*
|
|
691
|
+
* `@uniflowed/server`'s `prerenderedMayAnswer`, reached the same way
|
|
692
|
+
* `createStaticHandler` is. It exists out here because of the one request
|
|
693
|
+
* `uf preview` may not leave to Vite: `createServeHandler` above is mounted
|
|
694
|
+
* *behind* Vite's static middleware, which is fine for everything except a
|
|
695
|
+
* request carrying the draft cookie. `createStaticHandler` declines a
|
|
696
|
+
* prerendered document for such a request and, under `uf preview`, never sees
|
|
697
|
+
* it — so draft mode appeared to be off there while it worked under `uf dev`
|
|
698
|
+
* and `uf start`. That is ubugeeei-prod/uf#620.
|
|
699
|
+
*
|
|
700
|
+
* `driver.js` asks this in a middleware mounted in *front* of Vite's, and
|
|
701
|
+
* mounts `createServeHandler` behind it for the answer, so a draft request
|
|
702
|
+
* goes through the same handler `uf start` uses and gets the same answer —
|
|
703
|
+
* including its stylesheets and chunks, which that handler still serves off
|
|
704
|
+
* disk. Every other request is untouched and Vite's file middleware runs as
|
|
705
|
+
* before.
|
|
706
|
+
*
|
|
707
|
+
* A gate rather than a second handler, because the caller has a request
|
|
708
|
+
* lifecycle to open and must not open one for a request it is about to hand
|
|
709
|
+
* on.
|
|
710
|
+
*/
|
|
711
|
+
export function createPrerenderGate() {
|
|
712
|
+
const ready = deployment().then(({ prerenderedMayAnswer }) => prerenderedMayAnswer);
|
|
713
|
+
return async function prerenderedMayAnswer(cookieHeader) {
|
|
714
|
+
return (await ready)(cookieHeader ?? null);
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
/**
|
|
719
|
+
* A `Request`/`Response` handler as a Node request listener.
|
|
720
|
+
*
|
|
721
|
+
* `@uniflowed/server/node`'s, which is also what the `server.js` an adapter
|
|
722
|
+
* writes runs — so a request reaching `uf start` and the same request reaching
|
|
723
|
+
* a deployed directory go through one translation rather than two, and settle
|
|
724
|
+
* at one moment rather than at two.
|
|
725
|
+
*
|
|
726
|
+
* `entry` is the second argument rather than something this reaches for: it is
|
|
727
|
+
* the application bundle's own `beginRequest` that has to own the request, for
|
|
728
|
+
* the reason in "Who owns the request" above. It is required, and a listener
|
|
729
|
+
* built without one fails on its first request — the same trade
|
|
730
|
+
* `createFetchHandler` makes about `app.runMiddleware`, and for the same
|
|
731
|
+
* reason: an optional lifecycle is a lifecycle somebody forgets, and what is
|
|
732
|
+
* lost when they do is every `after()` in the application.
|
|
733
|
+
*/
|
|
734
|
+
export function nodeListener(handle, entry) {
|
|
735
|
+
const ready = deployment().then(({ nodeListener: create }) =>
|
|
736
|
+
create(handle, { beginRequest: entry.beginRequest }),
|
|
737
|
+
);
|
|
738
|
+
return async function listener(incoming, outgoing) {
|
|
739
|
+
return (await ready)(incoming, outgoing);
|
|
740
|
+
};
|
|
741
|
+
}
|