@uniflowed/vite 0.0.0-alpha.9 → 0.1.0
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 +1854 -190
- package/index.js +931 -121
- package/internal/a11y-runtime.js +234 -0
- package/internal/a11y.js +102 -0
- package/internal/assets.js +366 -18
- package/internal/barrel-imports.js +459 -0
- package/internal/compile-assets.js +67 -0
- package/internal/config.js +30 -3
- package/internal/dev-state.js +161 -0
- package/internal/devtools.js +117 -0
- package/internal/diagnostics.js +369 -0
- package/internal/events.js +8 -5
- package/internal/flight.js +803 -0
- package/internal/flow-keywords.js +1 -1
- package/internal/frontmatter.js +33 -0
- package/internal/http.js +21 -2
- package/internal/image-endpoint.js +118 -0
- package/internal/instrumentation.js +23 -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 +1294 -81
- package/internal/rsc.js +335 -11
- package/internal/serve.js +583 -29
- package/internal/server-components.js +109 -0
- package/internal/worker-builtins.js +170 -0
- package/package.json +28 -6
package/internal/serve.js
CHANGED
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
// module is the part that is genuinely Vite's: finding the build on disk and
|
|
33
33
|
// reading the manifest a Vite build wrote.
|
|
34
34
|
//
|
|
35
|
-
// It moved because of `uf build --adapter`. `
|
|
35
|
+
// It moved because of `uf build --adapter`. `packages/server/serve.test.js` said
|
|
36
36
|
// what was wrong with the old arrangement while it was still the only one:
|
|
37
37
|
// "`internal/serve.js` is the seam a deploy adapter will need, and naming it
|
|
38
38
|
// in `exports` before one exists would be promising an interface nothing has
|
|
@@ -64,17 +64,20 @@
|
|
|
64
64
|
// settles it on the line after the last byte.
|
|
65
65
|
//
|
|
66
66
|
// It has to be the *entry's* `beginRequest` rather than one imported here: the
|
|
67
|
-
// request
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
// application cannot see, and nothing would fail loudly — the
|
|
71
|
-
// the page would render, and every `cookies()` in it would
|
|
72
|
-
// host had run at all. See ubugeeei-prod/uf#389.
|
|
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
73
|
|
|
74
|
+
import { createHash } from "node:crypto";
|
|
74
75
|
import { readFile, stat } from "node:fs/promises";
|
|
75
76
|
import path from "node:path";
|
|
76
77
|
import { pathToFileURL } from "node:url";
|
|
77
78
|
|
|
79
|
+
import { localImageEndpoint, servesRemoteImages } from "./image-endpoint.js";
|
|
80
|
+
|
|
78
81
|
/**
|
|
79
82
|
* `@uniflowed/server`'s two halves, loaded once.
|
|
80
83
|
*
|
|
@@ -86,10 +89,133 @@ function deployment() {
|
|
|
86
89
|
deploymentModules ??= Promise.all([
|
|
87
90
|
import("@uniflowed/server/fetch"),
|
|
88
91
|
import("@uniflowed/server/node"),
|
|
89
|
-
|
|
92
|
+
import("@uniflowed/server/cache"),
|
|
93
|
+
]).then(([application, host, cache]) => ({ ...application, ...host, ...cache }));
|
|
90
94
|
return deploymentModules;
|
|
91
95
|
}
|
|
92
96
|
|
|
97
|
+
/**
|
|
98
|
+
* The cache `rendering.cache` describes, or `undefined` for no cache at all.
|
|
99
|
+
*
|
|
100
|
+
* `undefined` rather than a store with both switches off, and the difference is
|
|
101
|
+
* visible from an application: a handler with no `cache` installs nothing on
|
|
102
|
+
* the request, so `revalidateTag()` raises "there is not one here" instead of
|
|
103
|
+
* reporting that it expired nothing. A project that turned the cache off should
|
|
104
|
+
* be told that it did, not handed a cache that quietly does nothing.
|
|
105
|
+
*
|
|
106
|
+
* One store per server process, built when the handler is. With no
|
|
107
|
+
* `rendering.cache.store` that is the whole of what "in memory, per process"
|
|
108
|
+
* means in practice: `uf preview` and `uf start` each hold one, and two of them
|
|
109
|
+
* running at once share nothing. With one, the two processes share whatever the
|
|
110
|
+
* provider is in front of — see [`providerFor`].
|
|
111
|
+
*
|
|
112
|
+
* The store's remaining options are still not configurable from `uf.config.js`
|
|
113
|
+
* and are still not named here: `maxEntries` and `now` are facts about one
|
|
114
|
+
* process's heap and one process's clock, and a parameter threaded through for
|
|
115
|
+
* a setting nobody can set would be the shape of configurability with none of
|
|
116
|
+
* the substance.
|
|
117
|
+
*
|
|
118
|
+
* @param {{route?: boolean, fetch?: boolean, data?: boolean, store?: string, storeDir?: string} | undefined} declared
|
|
119
|
+
* @param {(options?: object) => object} createCacheStore
|
|
120
|
+
* @param {{root: string, build: string | null}} where
|
|
121
|
+
*/
|
|
122
|
+
async function cacheFor(declared, createCacheStore, where) {
|
|
123
|
+
const route = declared?.route === true;
|
|
124
|
+
const fetchCache = declared?.fetch === true;
|
|
125
|
+
const data = declared?.data === true;
|
|
126
|
+
if (!route && !fetchCache && !data) return undefined;
|
|
127
|
+
const provider = await providerFor(declared, where);
|
|
128
|
+
const store =
|
|
129
|
+
provider == null ? createCacheStore() : createCacheStore({ provider, build: where.build });
|
|
130
|
+
return { store, route, fetch: fetchCache, data };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Where `/__uf/image` keeps its variants: over the provider
|
|
135
|
+
* `rendering.cache.store` names, or `undefined` for the endpoint's own memory.
|
|
136
|
+
*
|
|
137
|
+
* The same provider as the route cache, because it is the one a project
|
|
138
|
+
* already said survives a restart and is shared between processes — a second
|
|
139
|
+
* setting for "where do image variants go" would be a second answer to a
|
|
140
|
+
* question the project answered once. Asked only when a store is named, so a
|
|
141
|
+
* project that names none needs no build identity for its images.
|
|
142
|
+
*
|
|
143
|
+
* @param {{store?: string, storeDir?: string} | undefined} declared
|
|
144
|
+
* @param {(options?: object) => object} createCacheStore
|
|
145
|
+
* @param {{root: string, build: string | null}} where
|
|
146
|
+
*/
|
|
147
|
+
async function imageStoreFor(declared, createCacheStore, where) {
|
|
148
|
+
const named = declared?.store ?? "memory";
|
|
149
|
+
if (named === "memory") return undefined;
|
|
150
|
+
const provider = await providerFor(declared, where);
|
|
151
|
+
if (provider == null) return undefined;
|
|
152
|
+
const { MAX_MEMORY_VARIANTS } = await import("@uniflowed/server/image");
|
|
153
|
+
return createCacheStore({ provider, build: where.build, maxEntries: MAX_MEMORY_VARIANTS });
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* The durable provider `rendering.cache.store` names, or `null` for memory.
|
|
158
|
+
*
|
|
159
|
+
* Three answers, and the third is the one that matters to
|
|
160
|
+
* `docs/red-lines.md`'s third line. `"memory"` — the default, and what every
|
|
161
|
+
* project that says nothing gets — keeps the store exactly as it was.
|
|
162
|
+
* `"filesystem"` is uf's built-in, and it is a convenience rather than an
|
|
163
|
+
* architecture. Anything else is a **module specifier**, resolved from the
|
|
164
|
+
* project, exporting `createCacheProvider`: the same shape `builder.module`
|
|
165
|
+
* has, chosen for the same reason that document gives — "a provider a project
|
|
166
|
+
* can replace has to be a name it can write, and an enum with one variant
|
|
167
|
+
* cannot become one without a release of uf".
|
|
168
|
+
*
|
|
169
|
+
* So a project with a Redis, a KV namespace or an S3 bucket writes twenty lines
|
|
170
|
+
* against `@uniflowed/server/cache`'s `CacheProvider` type, names the module
|
|
171
|
+
* here, and uf never learns which of those it was.
|
|
172
|
+
*
|
|
173
|
+
* # Why a missing build identity is a refusal
|
|
174
|
+
*
|
|
175
|
+
* Because the alternatives are both worse. Falling back to memory would give a
|
|
176
|
+
* project that asked for a cache surviving restarts one that does not, and the
|
|
177
|
+
* symptom is a `MISS` on every cold request — indistinguishable from a cache
|
|
178
|
+
* that is simply cold. Generating an identity per process would be worse again:
|
|
179
|
+
* four servers would write four copies of everything into one directory and
|
|
180
|
+
* read none of each other's. The deployment rules say a target that cannot
|
|
181
|
+
* provide a durable store has to say so, and this is a host saying so.
|
|
182
|
+
*
|
|
183
|
+
* @param {{store?: string, storeDir?: string} | undefined} declared
|
|
184
|
+
* @param {{root: string, build: string | null}} where
|
|
185
|
+
*/
|
|
186
|
+
async function providerFor(declared, { root, build, regenerates }) {
|
|
187
|
+
// A build that regenerates pages keeps what it regenerated on disk unless the
|
|
188
|
+
// project named a store. A restart that took every regenerated page back to
|
|
189
|
+
// the build's copy would be a server whose pages went back in time, which is
|
|
190
|
+
// the one thing regeneration is for not doing. `"memory"`, said out loud, is
|
|
191
|
+
// still memory.
|
|
192
|
+
const named = declared?.store ?? (regenerates === true ? "filesystem" : "memory");
|
|
193
|
+
if (named === "memory") return null;
|
|
194
|
+
if (build == null) {
|
|
195
|
+
throw new Error(
|
|
196
|
+
`uf: rendering.cache.store is ${JSON.stringify(named)}, which keeps entries between ` +
|
|
197
|
+
"restarts, and there is no build identity to key them by. `uf build` writes one " +
|
|
198
|
+
"beside the server bundle; set UF_BUILD_ID to name it yourself. Without one, a " +
|
|
199
|
+
"deploy would answer the new build's URLs with the previous build's documents.",
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
const directory = path.resolve(root, declared?.storeDir ?? path.join(".uf", "cache", "route"));
|
|
203
|
+
if (named === "filesystem") {
|
|
204
|
+
const { createFilesystemCache } = await import("@uniflowed/server/cache/filesystem");
|
|
205
|
+
return createFilesystemCache({ directory });
|
|
206
|
+
}
|
|
207
|
+
const provider = await import(providerSpecifier(root, named));
|
|
208
|
+
const create = provider.createCacheProvider ?? provider.default;
|
|
209
|
+
if (typeof create !== "function") {
|
|
210
|
+
throw new Error(
|
|
211
|
+
`uf: rendering.cache.store names ${JSON.stringify(named)}, which exports no ` +
|
|
212
|
+
"`createCacheProvider`. A durable cache provider is a module exporting that " +
|
|
213
|
+
"function; see @uniflowed/server/cache's CacheProvider type for what it returns.",
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
return create({ build, directory });
|
|
217
|
+
}
|
|
218
|
+
|
|
93
219
|
/**
|
|
94
220
|
* Everything a served build consists of.
|
|
95
221
|
*
|
|
@@ -118,7 +244,184 @@ export async function loadBuild({ root, outDir, serverDir }) {
|
|
|
118
244
|
const manifest = JSON.parse(await readFile(manifestFile, "utf8"));
|
|
119
245
|
const entry = await import(pathToFileURL(entryFile).href);
|
|
120
246
|
await deployment();
|
|
121
|
-
|
|
247
|
+
const build = await buildIdentity(root, serverDir);
|
|
248
|
+
const regeneration = await readRegeneration(path.resolve(root, serverDir));
|
|
249
|
+
const partial = await readPartialPrerenders(path.resolve(root, serverDir));
|
|
250
|
+
return {
|
|
251
|
+
entry,
|
|
252
|
+
assets: await documentAssetsFor(path.resolve(root, serverDir), manifest),
|
|
253
|
+
distDir,
|
|
254
|
+
root,
|
|
255
|
+
build,
|
|
256
|
+
regeneration,
|
|
257
|
+
partial,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** What `uf build` records a document's tags in, beside the server bundle. */
|
|
262
|
+
export const DOCUMENT_ASSETS_FILE = "uf-document-assets.json";
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* The tags a served document needs: the ones `uf build` recorded, or — for a
|
|
266
|
+
* build from before it recorded them — the client manifest's.
|
|
267
|
+
*
|
|
268
|
+
* Recorded rather than recomputed, because the client manifest is no longer the
|
|
269
|
+
* whole answer. An application React Server Components render links the
|
|
270
|
+
* stylesheets its rsc graph emitted and the ones each client module's chunk
|
|
271
|
+
* carries, and neither is reachable from the client entry the manifest is walked
|
|
272
|
+
* from — so a server that recomputed the tags rendered every page without the
|
|
273
|
+
* stylesheets its prerendered pages had. `uf start`, `uf preview`, `--adapter`
|
|
274
|
+
* and `--compile` all read them from here.
|
|
275
|
+
*
|
|
276
|
+
* @param {string} serverDir absolute path of the server bundle's directory
|
|
277
|
+
* @param {object} manifest the client build's Vite manifest
|
|
278
|
+
*/
|
|
279
|
+
export async function documentAssetsFor(serverDir, manifest) {
|
|
280
|
+
const file = path.join(serverDir, DOCUMENT_ASSETS_FILE);
|
|
281
|
+
let recorded;
|
|
282
|
+
try {
|
|
283
|
+
recorded = await readFile(file, "utf8");
|
|
284
|
+
} catch {
|
|
285
|
+
return assetsFromManifest(manifest);
|
|
286
|
+
}
|
|
287
|
+
try {
|
|
288
|
+
return JSON.parse(recorded);
|
|
289
|
+
} catch {
|
|
290
|
+
throw new Error(`uf: ${file} is not the JSON \`uf build\` writes; run \`uf build\` again`);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* The file beside the server bundle that names the pages a build regenerates.
|
|
296
|
+
*
|
|
297
|
+
* Written by `driver.js`'s build, and only for a build that has such a page.
|
|
298
|
+
*/
|
|
299
|
+
export const REGENERATION_FILE = "regenerate.json";
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Where a regenerated page's document goes, under the build's output directory.
|
|
303
|
+
*
|
|
304
|
+
* Somewhere no static half answers the page's own URL, which is the point: a
|
|
305
|
+
* file at `dist/posts/a/index.html` would be served by every host before the
|
|
306
|
+
* server saw the request, forever, whatever the page's lifetime said.
|
|
307
|
+
*/
|
|
308
|
+
export const REGENERATED_DIRECTORY = "__uf/regenerate";
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* The pages this build regenerates, or `undefined` for a build that has none.
|
|
312
|
+
*
|
|
313
|
+
* `undefined` rather than an empty manifest, so a build with nothing to
|
|
314
|
+
* regenerate serves exactly as every build did before regeneration existed.
|
|
315
|
+
*/
|
|
316
|
+
export async function readRegeneration(serverDir) {
|
|
317
|
+
let text;
|
|
318
|
+
try {
|
|
319
|
+
text = await readFile(path.join(serverDir, REGENERATION_FILE), "utf8");
|
|
320
|
+
} catch (error) {
|
|
321
|
+
if (error?.code === "ENOENT") return undefined;
|
|
322
|
+
throw error;
|
|
323
|
+
}
|
|
324
|
+
return JSON.parse(text);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* The file beside the server bundle that holds the static shells of the pages a
|
|
329
|
+
* build prerendered partially.
|
|
330
|
+
*
|
|
331
|
+
* Beside the server bundle rather than under the output directory, because a
|
|
332
|
+
* shell is not a document anybody should be sent on its own: its holes are
|
|
333
|
+
* filled by the server that reads this file, and a file server would send it
|
|
334
|
+
* with every hole showing its fallback forever.
|
|
335
|
+
*/
|
|
336
|
+
export const PARTIAL_PRERENDER_FILE = "partial-prerender.json";
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* The pages this build prerendered partially, or `undefined` for a build with
|
|
340
|
+
* none — which then serves exactly as every build did before partial
|
|
341
|
+
* prerendering existed.
|
|
342
|
+
*/
|
|
343
|
+
export async function readPartialPrerenders(serverDir) {
|
|
344
|
+
let text;
|
|
345
|
+
try {
|
|
346
|
+
text = await readFile(path.join(serverDir, PARTIAL_PRERENDER_FILE), "utf8");
|
|
347
|
+
} catch (error) {
|
|
348
|
+
if (error?.code === "ENOENT") return undefined;
|
|
349
|
+
throw error;
|
|
350
|
+
}
|
|
351
|
+
return JSON.parse(text);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* What `import()` should be given for a provider a project named.
|
|
356
|
+
*
|
|
357
|
+
* A relative path in `uf.config.js` is relative to *the project*, which is what
|
|
358
|
+
* anybody writing `"./cache/redis.js"` means and is not what `import()` from
|
|
359
|
+
* this module would do — it would look beside `@uniflowed/vite`, find nothing,
|
|
360
|
+
* and report a missing module the config file does not mention. `builder.module`
|
|
361
|
+
* settled the same question the same way in `uf_cli`'s `project_directory`.
|
|
362
|
+
*
|
|
363
|
+
* A bare specifier is left alone: `"@acme/uf-cache-redis"` is a package, and
|
|
364
|
+
* resolving it is Node's job and not this function's.
|
|
365
|
+
*
|
|
366
|
+
* @param {string} root
|
|
367
|
+
* @param {string} named
|
|
368
|
+
*/
|
|
369
|
+
export function providerSpecifier(root, named) {
|
|
370
|
+
if (!named.startsWith(".") && !path.isAbsolute(named)) return named;
|
|
371
|
+
return pathToFileURL(path.resolve(root, named)).href;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/** What `uf build` writes its identity into, beside the server bundle. */
|
|
375
|
+
export const BUILD_ID_FILE = "uf-build-id";
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* The identity of the build being served, or `null`.
|
|
379
|
+
*
|
|
380
|
+
* Only a durable cache reads it, and only a durable cache needs it: an entry
|
|
381
|
+
* that cannot outlive the process cannot outlive the build either, so every
|
|
382
|
+
* command that keeps its cache in memory is entitled to `null` here and never
|
|
383
|
+
* looks. `packages/server/internal/cache-key.js` argues the rest.
|
|
384
|
+
*
|
|
385
|
+
* `UF_BUILD_ID` first, then the file `uf build` wrote. The environment wins for
|
|
386
|
+
* the reason it wins in `crates/uf_rsc`'s `BuildId::from_env_or_generate`,
|
|
387
|
+
* which reads the same variable for the same kind of fact: a deployment that
|
|
388
|
+
* needs two artefacts to *be* one build — a blue/green pair, a rebuild of a
|
|
389
|
+
* tagged commit — has no other way to say so.
|
|
390
|
+
*
|
|
391
|
+
* `null` rather than a generated fallback, and that is the whole point of the
|
|
392
|
+
* function. A per-process identity would give four servers four caches with a
|
|
393
|
+
* shared disk between them, which is worse than four memories: it would write
|
|
394
|
+
* four copies of everything and read none of them. Whoever asked for a durable
|
|
395
|
+
* store is told there is no build to key it by, and gets to fix it.
|
|
396
|
+
*/
|
|
397
|
+
export async function buildIdentity(root, serverDir) {
|
|
398
|
+
const named = process.env.UF_BUILD_ID;
|
|
399
|
+
if (typeof named === "string" && named !== "") return named;
|
|
400
|
+
try {
|
|
401
|
+
const file = path.join(path.resolve(root, serverDir), BUILD_ID_FILE);
|
|
402
|
+
const value = (await readFile(file, "utf8")).trim();
|
|
403
|
+
return value === "" ? null : value;
|
|
404
|
+
} catch {
|
|
405
|
+
return null;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* The id a build's documents publish, derived from the build id.
|
|
411
|
+
*
|
|
412
|
+
* A browser names it on every action call and payload request, and a front
|
|
413
|
+
* door on another build refuses rather than answering; see
|
|
414
|
+
* `@uniflowed/server`'s `internal/deployment.js`. It has to be public — it is
|
|
415
|
+
* in every document — and the build id must not be: it is the key the action
|
|
416
|
+
* ids are an HMAC under whenever `UF_BUILD_ID` names both. So this is a
|
|
417
|
+
* digest of it, under a label of its own, and sixteen hex characters of that:
|
|
418
|
+
* the same for two artefacts that are one build, different for any two that
|
|
419
|
+
* are not, and no help to anybody guessing the key.
|
|
420
|
+
*
|
|
421
|
+
* @param {string} buildId
|
|
422
|
+
*/
|
|
423
|
+
export function deploymentIdFor(buildId) {
|
|
424
|
+
return createHash("sha256").update("uf:deployment\0").update(buildId).digest("hex").slice(0, 16);
|
|
122
425
|
}
|
|
123
426
|
|
|
124
427
|
async function readable(file, message) {
|
|
@@ -153,8 +456,15 @@ async function readable(file, message) {
|
|
|
153
456
|
* the rest moved to `@uniflowed/server`. `uf build --adapter` calls it too,
|
|
154
457
|
* at build time, and bakes the answer into what it emits.
|
|
155
458
|
*/
|
|
156
|
-
export function assetsFromManifest(manifest) {
|
|
157
|
-
|
|
459
|
+
export function assetsFromManifest(manifest, base = "") {
|
|
460
|
+
// `client` by name first. An application React Server Components render
|
|
461
|
+
// gives the client build one entry per client module as well, and the
|
|
462
|
+
// document's script is the application's entry, not whichever of those the
|
|
463
|
+
// manifest happens to list first.
|
|
464
|
+
const chunks = Object.values(manifest);
|
|
465
|
+
const entry =
|
|
466
|
+
chunks.find((chunk) => chunk.isEntry && chunk.name === "client") ??
|
|
467
|
+
chunks.find((chunk) => chunk.isEntry);
|
|
158
468
|
if (entry == null) throw new Error("uf: the client manifest has no entry chunk");
|
|
159
469
|
|
|
160
470
|
const styles = new Set(entry.css ?? []);
|
|
@@ -182,10 +492,12 @@ export function assetsFromManifest(manifest) {
|
|
|
182
492
|
};
|
|
183
493
|
collectPreloads(entry);
|
|
184
494
|
|
|
495
|
+
// Under `app.router.basePath` when there is one: a prerendered document is
|
|
496
|
+
// written outside Vite's HTML transform, so nothing else would put it there.
|
|
185
497
|
return {
|
|
186
|
-
scripts: [
|
|
187
|
-
styles: [...styles].map((file) =>
|
|
188
|
-
preloads: [...preloads].map((file) =>
|
|
498
|
+
scripts: [`${base}/${entry.file}`],
|
|
499
|
+
styles: [...styles].map((file) => `${base}/${file}`),
|
|
500
|
+
preloads: [...preloads].map((file) => `${base}/${file}`),
|
|
189
501
|
};
|
|
190
502
|
}
|
|
191
503
|
|
|
@@ -198,10 +510,10 @@ export function assetsFromManifest(manifest) {
|
|
|
198
510
|
* its callback whether the render threw or not, and `drainDeferred` already
|
|
199
511
|
* reports a failing task rather than propagating it.
|
|
200
512
|
*
|
|
201
|
-
* `entry.beginRequest` and not an import: the request
|
|
202
|
-
*
|
|
203
|
-
*
|
|
204
|
-
*
|
|
513
|
+
* `entry.beginRequest` and not an import: the request store is shared by every
|
|
514
|
+
* copy of one release of `@uniflowed/server`, and the release that matters is
|
|
515
|
+
* the one inside the application bundle. See `serverModuleSource` in
|
|
516
|
+
* `./routes.js`.
|
|
205
517
|
*
|
|
206
518
|
* The one case this cannot be exact about is a request uf hands back rather
|
|
207
519
|
* than answers: a caller whose `catch` is `next(error)` gives the response to
|
|
@@ -211,12 +523,12 @@ export function assetsFromManifest(manifest) {
|
|
|
211
523
|
* naming rather than papering over, and it is the failure path of a request
|
|
212
524
|
* that already went wrong — not the ordinary one this exists for.
|
|
213
525
|
*
|
|
214
|
-
* @param {{beginRequest: (request: Request) => {run: <T>(body: () => Promise<T>) => Promise<T>, settle: () => Promise<void>}}} entry
|
|
526
|
+
* @param {{beginRequest: (request: Request) => {context: object, run: <T>(body: () => Promise<T>) => Promise<T>, settle: () => Promise<void>}}} entry
|
|
215
527
|
* @param {Request} request
|
|
216
528
|
* @param {() => Promise<mixed>} body
|
|
217
529
|
*/
|
|
218
530
|
export async function withRequest(entry, request, body) {
|
|
219
|
-
const { run, settle } =
|
|
531
|
+
const { run, settle } = await beginRequest(entry, request);
|
|
220
532
|
try {
|
|
221
533
|
return await run(body);
|
|
222
534
|
} finally {
|
|
@@ -224,6 +536,44 @@ export async function withRequest(entry, request, body) {
|
|
|
224
536
|
}
|
|
225
537
|
}
|
|
226
538
|
|
|
539
|
+
/**
|
|
540
|
+
* Begin a request on this host, with what this host can do already on it.
|
|
541
|
+
*
|
|
542
|
+
* The half of [`withRequest`] that a caller which may *not* answer needs.
|
|
543
|
+
* `uf dev` runs the application's middleware, its action endpoint and its
|
|
544
|
+
* dispatcher for every request, and hands the ones none of them claimed back
|
|
545
|
+
* to Vite's chain — at which point the response is written somewhere this
|
|
546
|
+
* module cannot see, so settling has to wait for the socket rather than for a
|
|
547
|
+
* `finally` here. A caller that always answers should use [`withRequest`] and
|
|
548
|
+
* not think about it.
|
|
549
|
+
*
|
|
550
|
+
* `entry.beginRequest` and not an import: the request store is shared by every
|
|
551
|
+
* copy of one release of `@uniflowed/server`, and the release that matters is
|
|
552
|
+
* the one inside the application bundle. See `serverModuleSource` in
|
|
553
|
+
* `./routes.js`.
|
|
554
|
+
*
|
|
555
|
+
* What this host can do is put on the request the way `createFetchHandler`
|
|
556
|
+
* puts it on the one it owns. `uf dev` and `uf build --compile` reach a route
|
|
557
|
+
* handler without going through that function, and a handler that streams
|
|
558
|
+
* events or queues work has to get the same answer from all four front doors —
|
|
559
|
+
* a capability that is present under `uf start` and absent under `uf dev` is
|
|
560
|
+
* the difference this whole seam exists to remove.
|
|
561
|
+
*
|
|
562
|
+
* `nodeCapabilities`, because both of those *are* a Node process with a
|
|
563
|
+
* socket: a body reaches the client as it is written, and the process is still
|
|
564
|
+
* there afterwards. Neither passes an upgrader or a queue, because uf defines
|
|
565
|
+
* both and implements neither.
|
|
566
|
+
*
|
|
567
|
+
* @param {{beginRequest: (request: Request) => {context: object, run: <T>(body: () => Promise<T>) => Promise<T>, settle: () => Promise<void>}}} entry
|
|
568
|
+
* @param {Request} request
|
|
569
|
+
*/
|
|
570
|
+
export async function beginRequest(entry, request) {
|
|
571
|
+
const lifecycle = entry.beginRequest(request);
|
|
572
|
+
const { nodeCapabilities } = await deployment();
|
|
573
|
+
lifecycle.context.capabilities ??= nodeCapabilities();
|
|
574
|
+
return lifecycle;
|
|
575
|
+
}
|
|
576
|
+
|
|
227
577
|
/**
|
|
228
578
|
* The application half: route handlers, then rendering.
|
|
229
579
|
*
|
|
@@ -238,11 +588,67 @@ export async function withRequest(entry, request, body) {
|
|
|
238
588
|
* forgets is not left to discover it: `entry.runMiddleware` refuses outside a
|
|
239
589
|
* request and names what establishes one. See "Who owns the request" above.
|
|
240
590
|
*
|
|
241
|
-
*
|
|
591
|
+
* `cache` is `rendering.cache` from `uf.config.js`, straight through: this is
|
|
592
|
+
* the point where four switches that used to reach a JSON file and nothing else
|
|
593
|
+
* become a store a request can hit. See ubugeeei-prod/uf#277.
|
|
594
|
+
*
|
|
595
|
+
* `root` and `build` come with it, and only the cache reads either: `root` is
|
|
596
|
+
* where a `storeDir` is resolved from and `build` is what a durable entry is
|
|
597
|
+
* keyed by. Both are `undefined` for a caller that constructs a handler by
|
|
598
|
+
* hand, which is the memory-only store and needs neither.
|
|
599
|
+
*
|
|
600
|
+
* `images` is `app.builtins.images`, and it adds `/__uf/image` to what the
|
|
601
|
+
* handler answers when it lists remote hosts; see `./image-endpoint.js`.
|
|
602
|
+
*
|
|
603
|
+
* @param {{entry: object, assets: object, cache?: object, root?: string, build?: string | null, regeneration?: object, images?: object, partial?: object}} build
|
|
242
604
|
*/
|
|
243
|
-
export function createApplicationHandler({
|
|
244
|
-
|
|
245
|
-
|
|
605
|
+
export function createApplicationHandler({
|
|
606
|
+
entry,
|
|
607
|
+
assets,
|
|
608
|
+
cache,
|
|
609
|
+
root,
|
|
610
|
+
build,
|
|
611
|
+
regeneration,
|
|
612
|
+
images,
|
|
613
|
+
partial,
|
|
614
|
+
}) {
|
|
615
|
+
const ready = deployment().then(
|
|
616
|
+
async ({ createFetchHandler, createCacheStore, nodeCapabilities }) =>
|
|
617
|
+
createFetchHandler({
|
|
618
|
+
app: entry,
|
|
619
|
+
document: assets,
|
|
620
|
+
cache: await cacheFor(cache, createCacheStore, {
|
|
621
|
+
root: root ?? process.cwd(),
|
|
622
|
+
build: build ?? null,
|
|
623
|
+
regenerates: regeneration != null,
|
|
624
|
+
}),
|
|
625
|
+
// `/__uf/image`, for a project that listed remote hosts. See
|
|
626
|
+
// `./image-endpoint.js`.
|
|
627
|
+
...(servesRemoteImages(images)
|
|
628
|
+
? {
|
|
629
|
+
images: await localImageEndpoint({
|
|
630
|
+
images,
|
|
631
|
+
root: root ?? process.cwd(),
|
|
632
|
+
store: await imageStoreFor(cache, createCacheStore, {
|
|
633
|
+
root: root ?? process.cwd(),
|
|
634
|
+
build: build ?? null,
|
|
635
|
+
}),
|
|
636
|
+
}),
|
|
637
|
+
}
|
|
638
|
+
: {}),
|
|
639
|
+
// The pages this build regenerates, from the manifest beside the server
|
|
640
|
+
// bundle. Absent for a build with none, which then serves exactly as it
|
|
641
|
+
// did before regeneration existed.
|
|
642
|
+
...(regeneration == null ? {} : { regeneration }),
|
|
643
|
+
// And the static shells of the pages it prerendered partially, from the
|
|
644
|
+
// file beside the server bundle, for the same reason.
|
|
645
|
+
...(partial == null ? {} : { partial }),
|
|
646
|
+
// `uf preview` and `uf start` are a Node process with a socket, which is
|
|
647
|
+
// what a deployed `--adapter node` build is too — so a route handler
|
|
648
|
+
// that streams events answers the same way in the preview it is checked
|
|
649
|
+
// in and in the deployment it ends up as. See `withRequest` above.
|
|
650
|
+
capabilities: nodeCapabilities(),
|
|
651
|
+
}),
|
|
246
652
|
);
|
|
247
653
|
return async function handle(request) {
|
|
248
654
|
return (await ready)(request);
|
|
@@ -272,13 +678,161 @@ export function createStaticHandler({ root }) {
|
|
|
272
678
|
* project whose handler path collides with a file in `public/` behaves one way
|
|
273
679
|
* when it is checked and the other way when it is deployed.
|
|
274
680
|
*
|
|
275
|
-
*
|
|
681
|
+
* It is `@uniflowed/server/node`'s own `createServeHandler`, the one a deployed
|
|
682
|
+
* `server.js` runs, rather than the two halves composed a second time here.
|
|
683
|
+
* That one also hands the application the build's files when the static half
|
|
684
|
+
* has nothing, which is how a regenerated page starts from the document the
|
|
685
|
+
* build wrote; a composition of its own here would be a `uf start` whose
|
|
686
|
+
* regenerated pages rendered on their first request while a deployment's did
|
|
687
|
+
* not.
|
|
688
|
+
*
|
|
689
|
+
* It is handed the bundle's `routing` too, so `app.router`'s redirects answer
|
|
690
|
+
* before the files and its headers go on whatever answers, here exactly as in
|
|
691
|
+
* a deployment. `uf preview` puts the same two in front of Vite's file
|
|
692
|
+
* middleware as well; see [`answerRouting`].
|
|
693
|
+
*
|
|
694
|
+
* @param {{entry: object, assets: object, distDir: string, cache?: object, root?: string, build?: string | null, regeneration?: object, partial?: object}} build
|
|
276
695
|
*/
|
|
277
|
-
export function createServeHandler({
|
|
278
|
-
|
|
279
|
-
|
|
696
|
+
export function createServeHandler({
|
|
697
|
+
entry,
|
|
698
|
+
assets,
|
|
699
|
+
distDir,
|
|
700
|
+
cache,
|
|
701
|
+
root,
|
|
702
|
+
build,
|
|
703
|
+
regeneration,
|
|
704
|
+
images,
|
|
705
|
+
partial,
|
|
706
|
+
}) {
|
|
707
|
+
const application = createApplicationHandler({
|
|
708
|
+
entry,
|
|
709
|
+
assets,
|
|
710
|
+
cache,
|
|
711
|
+
root,
|
|
712
|
+
build,
|
|
713
|
+
regeneration,
|
|
714
|
+
images,
|
|
715
|
+
partial,
|
|
716
|
+
});
|
|
717
|
+
const ready = deployment().then(({ createServeHandler: create }) =>
|
|
718
|
+
create({ staticDir: distDir, handle: application, routing: entry.routing }),
|
|
719
|
+
);
|
|
280
720
|
return async function handle(request) {
|
|
281
|
-
return (await
|
|
721
|
+
return (await ready)(request);
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
/**
|
|
726
|
+
* Whether `routing` has anything to say in front of a file server.
|
|
727
|
+
*
|
|
728
|
+
* Redirects and headers are the two that do; a rewrite is the application's.
|
|
729
|
+
* Asked before a middleware is mounted at all, so a project with no rules pays
|
|
730
|
+
* nothing per request under `uf dev` or `uf preview`.
|
|
731
|
+
*
|
|
732
|
+
* @param {{redirects?: unknown[], headers?: unknown[]} | undefined} routing
|
|
733
|
+
*/
|
|
734
|
+
export function answersInFrontOfFiles(routing) {
|
|
735
|
+
return (
|
|
736
|
+
(routing?.redirects?.length ?? 0) > 0 ||
|
|
737
|
+
(routing?.headers?.length ?? 0) > 0 ||
|
|
738
|
+
(routing?.basePath ?? "") !== "" ||
|
|
739
|
+
(routing?.trailingSlash ?? "ignore") !== "ignore"
|
|
740
|
+
);
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
/**
|
|
744
|
+
* `app.router`'s headers and redirects, for a door whose files Vite serves.
|
|
745
|
+
*
|
|
746
|
+
* `uf dev` and `uf preview` mount this in front of Vite's own middleware:
|
|
747
|
+
* the headers are pinned on the Node response, so they survive the
|
|
748
|
+
* `writeHead` Vite's file server writes its own with, and a redirect is
|
|
749
|
+
* answered before any file is looked for. `true` when it answered.
|
|
750
|
+
*
|
|
751
|
+
* @param {object} routing the bundle's `routing`
|
|
752
|
+
* @param {Request} request the address alone; see `toAddressRequest`
|
|
753
|
+
* @param {import("node:http").ServerResponse} response
|
|
754
|
+
*/
|
|
755
|
+
export async function answerRouting(routing, request, response) {
|
|
756
|
+
const { admit, headersFor, pinHeaders, send: write } = await deployment();
|
|
757
|
+
pinHeaders(response, headersFor(routing, request));
|
|
758
|
+
// A request outside the base path, the other spelling of a path, or a
|
|
759
|
+
// redirect rule: answered here, before Vite's own base middleware would
|
|
760
|
+
// answer the first in its words rather than uf's.
|
|
761
|
+
const admitted = admit(routing, request);
|
|
762
|
+
if (admitted.kind !== "answer") return false;
|
|
763
|
+
await write(response, admitted.response);
|
|
764
|
+
return true;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
/**
|
|
768
|
+
* A request [`answerRouting`] let through, spelled so Vite's own middleware
|
|
769
|
+
* recognises it.
|
|
770
|
+
*
|
|
771
|
+
* Vite serves under its `base` with the trailing slash, `/docs/`, and its base
|
|
772
|
+
* middleware answers every other path with a 404 of its own, the bare `/docs`
|
|
773
|
+
* included. But `/docs` is the application's root under `app.router.basePath`,
|
|
774
|
+
* and the only spelling of it unless the trailing-slash policy is `"always"`,
|
|
775
|
+
* which has already answered `/docs` with a `308` by the time this is asked.
|
|
776
|
+
* So a request for exactly the base goes on to Vite as `/docs/`, which Vite
|
|
777
|
+
* takes the base off and hands on as the root. The application is handed the
|
|
778
|
+
* root either way.
|
|
779
|
+
*
|
|
780
|
+
* @param {{basePath?: string} | undefined} routing the bundle's `routing`
|
|
781
|
+
* @param {import("node:http").IncomingMessage} request
|
|
782
|
+
*/
|
|
783
|
+
export function forViteBase(routing, request) {
|
|
784
|
+
const base = routing?.basePath ?? "";
|
|
785
|
+
const url = request.url ?? "/";
|
|
786
|
+
if (base === "") return;
|
|
787
|
+
const queryAt = url.indexOf("?");
|
|
788
|
+
const pathname = queryAt === -1 ? url : url.slice(0, queryAt);
|
|
789
|
+
if (pathname === base) {
|
|
790
|
+
request.url = `${base}/${queryAt === -1 ? "" : url.slice(queryAt)}`;
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
/**
|
|
795
|
+
* `app.router.rewrites` for this request, for `uf dev`: the rewritten request,
|
|
796
|
+
* or `null`.
|
|
797
|
+
*
|
|
798
|
+
* `@uniflowed/server`'s `rewriteFor`, which `createFetchHandler` asks for every
|
|
799
|
+
* other front door at the same point — after the files, before the guard.
|
|
800
|
+
*
|
|
801
|
+
* @param {object | undefined} routing the bundle's `routing`
|
|
802
|
+
* @param {Request} request
|
|
803
|
+
*/
|
|
804
|
+
export async function rewriteRouting(routing, request) {
|
|
805
|
+
const { rewriteFor } = await deployment();
|
|
806
|
+
return rewriteFor(routing, request);
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
/**
|
|
810
|
+
* Whether a file server may answer this request, or uf has to go first.
|
|
811
|
+
*
|
|
812
|
+
* `@uniflowed/server`'s `prerenderedMayAnswer`, reached the same way
|
|
813
|
+
* `createStaticHandler` is. It exists out here because of the one request
|
|
814
|
+
* `uf preview` may not leave to Vite: `createServeHandler` above is mounted
|
|
815
|
+
* *behind* Vite's static middleware, which is fine for everything except a
|
|
816
|
+
* request carrying the draft cookie. `createStaticHandler` declines a
|
|
817
|
+
* prerendered document for such a request and, under `uf preview`, never sees
|
|
818
|
+
* it — so draft mode appeared to be off there while it worked under `uf dev`
|
|
819
|
+
* and `uf start`. That is ubugeeei-prod/uf#620.
|
|
820
|
+
*
|
|
821
|
+
* `driver.js` asks this in a middleware mounted in *front* of Vite's, and
|
|
822
|
+
* mounts `createServeHandler` behind it for the answer, so a draft request
|
|
823
|
+
* goes through the same handler `uf start` uses and gets the same answer —
|
|
824
|
+
* including its stylesheets and chunks, which that handler still serves off
|
|
825
|
+
* disk. Every other request is untouched and Vite's file middleware runs as
|
|
826
|
+
* before.
|
|
827
|
+
*
|
|
828
|
+
* A gate rather than a second handler, because the caller has a request
|
|
829
|
+
* lifecycle to open and must not open one for a request it is about to hand
|
|
830
|
+
* on.
|
|
831
|
+
*/
|
|
832
|
+
export function createPrerenderGate() {
|
|
833
|
+
const ready = deployment().then(({ prerenderedMayAnswer }) => prerenderedMayAnswer);
|
|
834
|
+
return async function prerenderedMayAnswer(cookieHeader) {
|
|
835
|
+
return (await ready)(cookieHeader ?? null);
|
|
282
836
|
};
|
|
283
837
|
}
|
|
284
838
|
|