@uniflowed/vite 0.0.0-alpha.12 → 0.0.0-alpha.14
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 +329 -145
- package/index.js +266 -51
- package/internal/diagnostics.js +366 -0
- package/internal/events.js +5 -5
- package/internal/routes.js +295 -27
- package/internal/rsc.js +256 -1
- package/internal/serve.js +46 -3
- package/package.json +11 -3
package/driver.js
CHANGED
|
@@ -6,7 +6,10 @@
|
|
|
6
6
|
// `uf start` spawn.
|
|
7
7
|
//
|
|
8
8
|
// <host> driver.js dev --root <dir> [--mode <m>] [--host <h>] [--port <n>] [--strict-port]
|
|
9
|
+
// [--uf-env-file <file>]...
|
|
9
10
|
// <host> driver.js build --root <dir> [--mode <m>] [--out-dir <dir>]
|
|
11
|
+
// [--prerender everything|possible|nothing]
|
|
12
|
+
// [--static-build] [--because <sentence>]
|
|
10
13
|
// <host> driver.js compile --root <dir> [--mode <m>] [--out-dir <dir>] --assets <file> --bundle <dir>
|
|
11
14
|
// <host> driver.js deploy --root <dir> [--mode <m>] [--out-dir <dir>] --adapter <name> --work <dir> --output <dir>
|
|
12
15
|
// <host> driver.js preview --root <dir> [--mode <m>] [--out-dir <dir>] [--host <h>] [--port <n>]
|
|
@@ -19,6 +22,12 @@
|
|
|
19
22
|
// environment — see `viteConfig` below and `crates/uf_config/src/env_files.rs`.
|
|
20
23
|
// `start` has no Vite in it and therefore no mode.
|
|
21
24
|
//
|
|
25
|
+
// `--uf-env-file` names those files, one flag each, so `dev` can watch them and
|
|
26
|
+
// say when one moved; nothing here reads their contents. The prefix is load
|
|
27
|
+
// bearing: node claims `--env-file` for itself and honours it wherever it
|
|
28
|
+
// appears on the command line, script arguments included, so a driver argument
|
|
29
|
+
// by that name is an argument node eats and then exits 9 over.
|
|
30
|
+
//
|
|
22
31
|
// `uf` in Rust owns the terminal; this process owns Vite. They talk over
|
|
23
32
|
// stdout, one JSON event per line (see `./internal/events.js`), and the driver
|
|
24
33
|
// exits when its stdin closes so it cannot outlive the command that started
|
|
@@ -34,7 +43,7 @@ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node
|
|
|
34
43
|
import path from "node:path";
|
|
35
44
|
import { pathToFileURL } from "node:url";
|
|
36
45
|
|
|
37
|
-
import { emit, errorEvent, eventLogger
|
|
46
|
+
import { emit, errorEvent, eventLogger } from "./internal/events.js";
|
|
38
47
|
import { loadUfConfig, projectConfig } from "./internal/config.js";
|
|
39
48
|
import { send, toRequest } from "./internal/http.js";
|
|
40
49
|
import { withProjectConfig } from "./merge.js";
|
|
@@ -52,6 +61,16 @@ function argument(name) {
|
|
|
52
61
|
return at === -1 ? null : process.argv[at + 1];
|
|
53
62
|
}
|
|
54
63
|
|
|
64
|
+
/** Every value of a repeated argument, in the order they were given. */
|
|
65
|
+
function argumentAll(name) {
|
|
66
|
+
const values = [];
|
|
67
|
+
for (let at = 0; at < process.argv.length; at += 1) {
|
|
68
|
+
if (process.argv[at] === name && process.argv[at + 1] != null)
|
|
69
|
+
values.push(process.argv[at + 1]);
|
|
70
|
+
}
|
|
71
|
+
return values;
|
|
72
|
+
}
|
|
73
|
+
|
|
55
74
|
function flag(name) {
|
|
56
75
|
return process.argv.includes(name);
|
|
57
76
|
}
|
|
@@ -182,26 +201,21 @@ async function viteConfig(config, mode) {
|
|
|
182
201
|
* Vite in middleware mode serves nothing on its own: with no `index.html` at
|
|
183
202
|
* the project root it answers every navigation with "Cannot GET /", which is
|
|
184
203
|
* what `uf dev` used to do for every project it started. A uf project has no
|
|
185
|
-
* `index.html` — the document comes from a layout — so the server has to
|
|
186
|
-
* it
|
|
187
|
-
*
|
|
188
|
-
* 1. load the server entry through `ssrLoadModule`, so it is transformed the
|
|
189
|
-
* same way the browser's copy is and picks up edits without a restart;
|
|
190
|
-
* 2. run the middleware guarding this path, which may answer instead;
|
|
191
|
-
* 3. render the URL, pointing the client script at the dev entry rather than
|
|
192
|
-
* at a built asset;
|
|
193
|
-
* 4. hand the HTML to `transformIndexHtml`, which is what injects the HMR
|
|
194
|
-
* client and lets any Vite plugin see the document.
|
|
204
|
+
* `index.html` — the document comes from a layout — so the server has to
|
|
205
|
+
* render it.
|
|
195
206
|
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
*
|
|
207
|
+
* That rendering is **not** here. It is one middleware, in `./index.js`'s
|
|
208
|
+
* `configureServer`, and this function installs none of its own. It used to
|
|
209
|
+
* install a second one, and two middlewares rendering the same request is how
|
|
210
|
+
* `uf dev` came to answer a route handler with a page and a redirect without
|
|
211
|
+
* its `Location`: `configureServer`'s post hook runs inside `createServer`,
|
|
212
|
+
* and anything added here runs after it returns, so of the two the plugin's
|
|
213
|
+
* was always the one that decided. See ubugeeei-prod/uf#349 and #338, and the
|
|
214
|
+
* comment above that middleware for what it now has to do.
|
|
202
215
|
*
|
|
203
|
-
*
|
|
204
|
-
*
|
|
216
|
+
* What is left here is the half that is genuinely the driver's: the Vite
|
|
217
|
+
* config, the socket, the event channel back to `uf`, and the two watchers
|
|
218
|
+
* below.
|
|
205
219
|
*/
|
|
206
220
|
async function dev() {
|
|
207
221
|
const { createServer } = await import("vite");
|
|
@@ -212,89 +226,6 @@ async function dev() {
|
|
|
212
226
|
const inline = await viteConfig(config, argument("--mode") ?? "development");
|
|
213
227
|
const server = await createServer({ ...inline, appType: "custom" });
|
|
214
228
|
|
|
215
|
-
// In dev the browser loads the client entry from Vite, not from a manifest;
|
|
216
|
-
// its stylesheets arrive through that module rather than as <link> tags.
|
|
217
|
-
const assets = { scripts: [`/@id/${VIRTUAL.client}`], styles: [], preloads: [] };
|
|
218
|
-
|
|
219
|
-
server.middlewares.use(async (request, response, next) => {
|
|
220
|
-
const url = request.originalUrl ?? request.url ?? "/";
|
|
221
|
-
// Declared out here so the catch below can still settle: a request that
|
|
222
|
-
// failed is a request that happened, and a middleware that logged its
|
|
223
|
-
// arrival is owed its callback either way.
|
|
224
|
-
let lifecycle = null;
|
|
225
|
-
try {
|
|
226
|
-
const entry = await server.ssrLoadModule(VIRTUAL.server);
|
|
227
|
-
const asRequest = await toRequest(request, server.config);
|
|
228
|
-
|
|
229
|
-
// The request begins here and ends when the document has been written,
|
|
230
|
-
// which is what `after()` promises and what `uf preview`, `uf start` and
|
|
231
|
-
// a compiled binary all do too — a middleware that logs a response's
|
|
232
|
-
// status has to mean the same thing in development as in production.
|
|
233
|
-
// `entry.beginRequest` rather than an import: the storage that holds the
|
|
234
|
-
// request belongs to the application's own copy of `@uniflowed/server`.
|
|
235
|
-
// See `internal/serve.js` and ubugeeei-prod/uf#389.
|
|
236
|
-
lifecycle = entry.beginRequest(asRequest);
|
|
237
|
-
const answered = await lifecycle.run(async () => {
|
|
238
|
-
// Middleware first, above everything: it guards a subtree, so it has to
|
|
239
|
-
// run for a page, for a route handler, and for a path under it that
|
|
240
|
-
// matches neither. Running it inside the dispatcher and again inside the
|
|
241
|
-
// renderer would have left `/dashboard/typo` unguarded and run it twice
|
|
242
|
-
// for a path that is both.
|
|
243
|
-
const guarded = await entry.runMiddleware(asRequest);
|
|
244
|
-
if (guarded != null) {
|
|
245
|
-
await send(response, guarded);
|
|
246
|
-
return true;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
// Route handlers next, and for every method: a handler is the only
|
|
250
|
-
// thing that answers a POST, and it may also answer a GET for a path
|
|
251
|
-
// that has no page.
|
|
252
|
-
const handled = await entry.dispatch(asRequest);
|
|
253
|
-
if (handled != null) {
|
|
254
|
-
await send(response, handled);
|
|
255
|
-
return true;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
// Only a navigation reaches the renderer. A page cannot answer a POST,
|
|
259
|
-
// and letting one try would turn a missing handler into a rendered page
|
|
260
|
-
// with a 200 rather than a 404.
|
|
261
|
-
if (request.method !== "GET" && request.method !== "HEAD") {
|
|
262
|
-
return false;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
const result = await entry.render(url, assets, {
|
|
266
|
-
// A boundary that threw after the shell went out. `result.error` cannot
|
|
267
|
-
// carry it — the caller already has the result by then — so the
|
|
268
|
-
// terminal hears about it here or not at all.
|
|
269
|
-
onError: (error) => reportRenderError(server, url, error),
|
|
270
|
-
});
|
|
271
|
-
if (result.error != null) reportRenderError(server, url, result.error);
|
|
272
|
-
const html = await server.transformIndexHtml(url, await result.text());
|
|
273
|
-
response.statusCode = result.status ?? 200;
|
|
274
|
-
response.setHeader("content-type", "text/html; charset=utf-8");
|
|
275
|
-
response.end(html);
|
|
276
|
-
return true;
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
if (!answered) {
|
|
280
|
-
// The one path where uf is not the one writing the response: a
|
|
281
|
-
// non-navigation nothing claimed goes back to Vite's chain. The guard
|
|
282
|
-
// has still run and may have deferred work, so `close` — the socket
|
|
283
|
-
// saying the response is over, however it ended — is the only honest
|
|
284
|
-
// signal left that the bytes are out.
|
|
285
|
-
response.once("close", lifecycle.settle);
|
|
286
|
-
next();
|
|
287
|
-
return;
|
|
288
|
-
}
|
|
289
|
-
await lifecycle.settle();
|
|
290
|
-
} catch (error) {
|
|
291
|
-
if (lifecycle != null) await lifecycle.settle();
|
|
292
|
-
// Map the stack back onto the Flow source before it reaches the overlay.
|
|
293
|
-
if (error instanceof Error) server.ssrFixStacktrace(error);
|
|
294
|
-
next(error);
|
|
295
|
-
}
|
|
296
|
-
});
|
|
297
|
-
|
|
298
229
|
await server.listen();
|
|
299
230
|
const urls = server.resolvedUrls ?? { local: [], network: [] };
|
|
300
231
|
emit("listening", {
|
|
@@ -305,6 +236,7 @@ async function dev() {
|
|
|
305
236
|
),
|
|
306
237
|
});
|
|
307
238
|
watchSources(server);
|
|
239
|
+
watchEnvFiles(server);
|
|
308
240
|
|
|
309
241
|
const shutdown = async () => {
|
|
310
242
|
await server.close();
|
|
@@ -351,6 +283,45 @@ function watchSources(server) {
|
|
|
351
283
|
}
|
|
352
284
|
}
|
|
353
285
|
|
|
286
|
+
/**
|
|
287
|
+
* Restart the server when one of the `.env` files uf read changes.
|
|
288
|
+
*
|
|
289
|
+
* uf reads the `.env` cascade itself, in Rust, before this process starts —
|
|
290
|
+
* one parser, one precedence, one answer for every command (see `viteConfig`
|
|
291
|
+
* above and `crates/uf_config/src/env_files.rs`) — and `envDir: false` turns
|
|
292
|
+
* Vite's own file loading off so there cannot be two answers. The cost of that
|
|
293
|
+
* was that nothing watched them: a value edited while `uf dev` ran changed
|
|
294
|
+
* nothing until somebody restarted the command by hand, and the guide had to
|
|
295
|
+
* document it as a limitation. See ubugeeei-prod/uf#428.
|
|
296
|
+
*
|
|
297
|
+
* `uf` passes the files it would consult with `--uf-env-file`, one per file, in
|
|
298
|
+
* cascade order, whether or not each exists today — a `.env.local` *created*
|
|
299
|
+
* while the server runs changes the answer exactly as much as an edit to one
|
|
300
|
+
* that was already there, and watching only what was read would have missed
|
|
301
|
+
* it. They are added to Vite's watcher explicitly because they are in no
|
|
302
|
+
* module graph, which is the same reason the RSC manifest is added in
|
|
303
|
+
* `index.js`.
|
|
304
|
+
*
|
|
305
|
+
* What is emitted is "these values are stale", and the Rust side restarts this
|
|
306
|
+
* process with the files re-read. A restart rather than a hot update is the
|
|
307
|
+
* honest granularity: a prefixed value reaches the browser by substitution
|
|
308
|
+
* into the bundle, so a new value has to be substituted again, and every
|
|
309
|
+
* module that read one has to be re-evaluated. Vite's watcher is still the
|
|
310
|
+
* only watcher — a second one over the same tree, in Rust, would be a second
|
|
311
|
+
* answer to "did this file change".
|
|
312
|
+
*/
|
|
313
|
+
function watchEnvFiles(server) {
|
|
314
|
+
const files = argumentAll("--uf-env-file").map((file) => path.resolve(root, file));
|
|
315
|
+
if (files.length === 0) return;
|
|
316
|
+
const watched = new Set(files);
|
|
317
|
+
server.watcher.add(files);
|
|
318
|
+
for (const event of ["add", "change", "unlink"]) {
|
|
319
|
+
server.watcher.on(event, (file) => {
|
|
320
|
+
if (watched.has(path.resolve(file))) emit("env-changed", { file, change: event });
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
354
325
|
/**
|
|
355
326
|
* The preview server: the build, as Vite serves it.
|
|
356
327
|
*
|
|
@@ -375,37 +346,57 @@ async function preview() {
|
|
|
375
346
|
const { preview: startPreview } = await import("vite");
|
|
376
347
|
const config = await loadConfig();
|
|
377
348
|
const inline = await viteConfig(config, argument("--mode") ?? "production");
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
349
|
+
// A build that declared it emits no server has none to mount. `uf` refuses
|
|
350
|
+
// `uf start` for such a project and lets this one through, because a preview
|
|
351
|
+
// of files *is* the deployment: what a static host does with `dist/` is
|
|
352
|
+
// exactly what Vite's preview server does with it, and mounting a request
|
|
353
|
+
// handler behind it would make this preview right about a deployment that is
|
|
354
|
+
// not the one happening. See `uf_cli`'s `commands::serve`.
|
|
355
|
+
const staticBuild = flag("--static-build");
|
|
356
|
+
const build = staticBuild
|
|
357
|
+
? null
|
|
358
|
+
: await loadBuild({
|
|
359
|
+
root,
|
|
360
|
+
outDir: inline.build.outDir,
|
|
361
|
+
serverDir: path.join(".uf", "build", "server"),
|
|
362
|
+
});
|
|
383
363
|
|
|
384
364
|
const server = await startPreview({ ...inline, appType: "custom" });
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
await
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
365
|
+
if (build != null) {
|
|
366
|
+
const handle = createServeHandler({ ...build, cache: config.app?.rendering?.cache });
|
|
367
|
+
server.middlewares.use(async (request, response, next) => {
|
|
368
|
+
try {
|
|
369
|
+
const asRequest = await toRequest(request, server.config);
|
|
370
|
+
// The same lifecycle `uf start` gets from `nodeListener`, spelled out
|
|
371
|
+
// because this door is Vite's connect chain rather than a bare
|
|
372
|
+
// `node:http` server: the whole request runs inside it, and it settles
|
|
373
|
+
// once `send` has returned. A preview whose `after()` fired at a
|
|
374
|
+
// different moment from the production server's would be a preview that
|
|
375
|
+
// is checked and believed and wrong.
|
|
376
|
+
await withRequest(build.entry, asRequest, async () => {
|
|
377
|
+
await send(response, await handle(asRequest));
|
|
378
|
+
});
|
|
379
|
+
} catch (error) {
|
|
380
|
+
next(error);
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
}
|
|
402
384
|
|
|
403
385
|
const urls = server.resolvedUrls ?? { local: [], network: [] };
|
|
404
386
|
emit("listening", {
|
|
405
387
|
local: urls.local,
|
|
406
388
|
network: urls.network,
|
|
407
|
-
|
|
408
|
-
|
|
389
|
+
// From the filesystem when there is no bundle to ask, which is the same
|
|
390
|
+
// scan `dev` reports from. The count is what a reader checks the build
|
|
391
|
+
// against, so answering "0 routes" for a static site that has thirty would
|
|
392
|
+
// be the report being wrong about the thing it exists to report.
|
|
393
|
+
routes:
|
|
394
|
+
build == null
|
|
395
|
+
? scanRoutes(path.resolve(root, config.app?.router?.root ?? "app")).routes.map(
|
|
396
|
+
(route) => route.path,
|
|
397
|
+
)
|
|
398
|
+
: build.entry.routes.map((route) => route.path),
|
|
399
|
+
handlers: build == null ? [] : build.entry.handlers.map((handler) => handler.path),
|
|
409
400
|
});
|
|
410
401
|
|
|
411
402
|
const shutdown = async () => {
|
|
@@ -487,6 +478,15 @@ async function build() {
|
|
|
487
478
|
const inline = await viteConfig(config, mode);
|
|
488
479
|
const outDir = path.resolve(root, inline.build.outDir);
|
|
489
480
|
const serverDir = path.join(root, ".uf", "build", "server");
|
|
481
|
+
// How much of the route table to prerender, and whether the server bundle
|
|
482
|
+
// survives the build. Both are `uf`'s answer rather than this file's: they
|
|
483
|
+
// come from two settings in `uf.config.js` that only mean something read
|
|
484
|
+
// together, and `uf_config`'s `RenderingPlan` is where they are. A driver
|
|
485
|
+
// started by hand gets the behaviour every uf build had before either
|
|
486
|
+
// setting was read.
|
|
487
|
+
const prerender = argument("--prerender") ?? "possible";
|
|
488
|
+
const staticBuild = flag("--static-build");
|
|
489
|
+
const because = argument("--because") ?? "this build prerenders every route";
|
|
490
490
|
|
|
491
491
|
// 1. The client: everything the browser loads, with a manifest so the
|
|
492
492
|
// server render knows which script and stylesheet tags to write.
|
|
@@ -519,11 +519,39 @@ async function build() {
|
|
|
519
519
|
},
|
|
520
520
|
});
|
|
521
521
|
|
|
522
|
-
// 3.
|
|
522
|
+
// 3. Which routes this build renders when, and every route it renders now.
|
|
523
|
+
//
|
|
524
|
+
// The decision comes from `uf.config.js` and is made in Rust — see
|
|
525
|
+
// `uf_config`'s `RenderingPlan` — because `app.rendering.modes` and
|
|
526
|
+
// `build.staticBuild` are two settings that have to be read together. It
|
|
527
|
+
// arrives here as one word, and this is where it meets the route table.
|
|
523
528
|
emit("phase", { name: "prerender" });
|
|
524
529
|
const server = await import(pathToFileURL(path.join(serverDir, "server.js")).href);
|
|
525
530
|
const assets = assetsFromManifest(manifest);
|
|
526
|
-
const
|
|
531
|
+
const plan = await renderingPlan(server, prerender);
|
|
532
|
+
emit("rendering", {
|
|
533
|
+
prerender,
|
|
534
|
+
prerendered: plan.urls.length,
|
|
535
|
+
perRequest: plan.perRequest.map((route) => route.path),
|
|
536
|
+
});
|
|
537
|
+
// A build that has to prerender everything, and a route it cannot: the
|
|
538
|
+
// refusal ubugeeei-prod/uf#336 and ubugeeei-prod/uf#385 are both about.
|
|
539
|
+
// Before the loop below, so no document is written for a build that is not
|
|
540
|
+
// going to be one, and with the whole list rather than the first item — a
|
|
541
|
+
// project that has just narrowed `rendering.modes` wants to see every route
|
|
542
|
+
// the narrowing costs it, not one per rebuild.
|
|
543
|
+
if (prerender === "everything" && plan.perRequest.length > 0) {
|
|
544
|
+
const listed = plan.perRequest.map((entry) => ` ${entry.path} — ${entry.why}`).join("\n");
|
|
545
|
+
emit("error", {
|
|
546
|
+
message:
|
|
547
|
+
`${plan.perRequest.length} ${plural(plan.perRequest.length, "route")} in this project ` +
|
|
548
|
+
`can only be answered by a server, and ${because}\n${listed}\n\n` +
|
|
549
|
+
"Give each page a `generateStaticParams` and take out the handlers and middleware, or " +
|
|
550
|
+
'allow `"ssr"` in `app.rendering.modes` and deploy a server.',
|
|
551
|
+
});
|
|
552
|
+
process.exit(1);
|
|
553
|
+
}
|
|
554
|
+
const pages = plan.urls;
|
|
527
555
|
|
|
528
556
|
// A route that throws fails *that route*, and the rest of the build still
|
|
529
557
|
// happens. This loop had no `try`: the first page to throw rejected out of
|
|
@@ -587,7 +615,11 @@ async function build() {
|
|
|
587
615
|
// host would then serve uf's error page to every visitor who mistyped a URL,
|
|
588
616
|
// and nothing between the throw and the deploy would have mentioned it.
|
|
589
617
|
let attempted = pages.length;
|
|
590
|
-
|
|
618
|
+
// Not for a build that prerenders nothing. `404.html` is a file a static
|
|
619
|
+
// host serves for every path it has no file for, and a project whose
|
|
620
|
+
// `rendering.modes` allows only `ssr` has no such host: its not-found
|
|
621
|
+
// boundary is rendered per request, by the server, with the right status.
|
|
622
|
+
if (prerender !== "nothing" && server.notFound.some((boundary) => boundary.path === "/")) {
|
|
591
623
|
attempted += 1;
|
|
592
624
|
// `/404` rather than `/__uf_not_found__`: the internal path is how the
|
|
593
625
|
// router is asked, and the file the reader is looking for is `404.html`.
|
|
@@ -634,6 +666,16 @@ async function build() {
|
|
|
634
666
|
process.exit(1);
|
|
635
667
|
}
|
|
636
668
|
|
|
669
|
+
// `build.staticBuild` is "prerender everything and emit no server bundle",
|
|
670
|
+
// and this is the second half of it. The bundle is still *built*: the
|
|
671
|
+
// prerender renders through it, so a build with no server bundle at any
|
|
672
|
+
// point would be a build with no documents either. What the declaration is
|
|
673
|
+
// about is what is left behind — so it goes once the last document is
|
|
674
|
+
// written, and `uf start`, `uf preview` and every server adapter then find
|
|
675
|
+
// nothing to serve, which is the honest outcome for a project that said it
|
|
676
|
+
// deploys files.
|
|
677
|
+
if (staticBuild) rmSync(serverDir, { recursive: true, force: true });
|
|
678
|
+
|
|
637
679
|
emit("done", { outDir: path.relative(root, outDir), pages: pages.length });
|
|
638
680
|
process.exit(0);
|
|
639
681
|
}
|
|
@@ -749,7 +791,7 @@ async function compile() {
|
|
|
749
791
|
const ADAPTERS = {
|
|
750
792
|
node: {
|
|
751
793
|
entries: (document, cache) => ({
|
|
752
|
-
handler: handlerEntrySource(document, cache),
|
|
794
|
+
handler: handlerEntrySource(document, cache, NODE_CAPABILITIES),
|
|
753
795
|
server: nodeEntrySource("./handler.js"),
|
|
754
796
|
}),
|
|
755
797
|
},
|
|
@@ -759,13 +801,13 @@ const ADAPTERS = {
|
|
|
759
801
|
// `commands::deploy`.
|
|
760
802
|
container: {
|
|
761
803
|
entries: (document, cache) => ({
|
|
762
|
-
handler: handlerEntrySource(document, cache),
|
|
804
|
+
handler: handlerEntrySource(document, cache, NODE_CAPABILITIES),
|
|
763
805
|
server: nodeEntrySource("./handler.js"),
|
|
764
806
|
}),
|
|
765
807
|
},
|
|
766
808
|
edge: {
|
|
767
809
|
entries: (document, cache) => ({
|
|
768
|
-
handler: handlerEntrySource(document, cache),
|
|
810
|
+
handler: handlerEntrySource(document, cache, EDGE_CAPABILITIES),
|
|
769
811
|
worker: workerEntrySource("./handler.js"),
|
|
770
812
|
}),
|
|
771
813
|
// `workerd` first, so React resolves to the build that has
|
|
@@ -776,12 +818,33 @@ const ADAPTERS = {
|
|
|
776
818
|
},
|
|
777
819
|
serverless: {
|
|
778
820
|
entries: (document, cache) => ({
|
|
779
|
-
handler: handlerEntrySource(document, cache),
|
|
821
|
+
handler: handlerEntrySource(document, cache, SERVERLESS_CAPABILITIES),
|
|
780
822
|
lambda: lambdaEntrySource("./handler.js"),
|
|
781
823
|
}),
|
|
782
824
|
},
|
|
783
825
|
};
|
|
784
826
|
|
|
827
|
+
/**
|
|
828
|
+
* How each target says what it can do: the module, and the name to call.
|
|
829
|
+
*
|
|
830
|
+
* A pair of strings rather than a value, because this is the module that
|
|
831
|
+
* *writes* `handler.js` and never imports what it writes: the capabilities
|
|
832
|
+
* belong to the deployed application's copy of `@uniflowed/server`, not to the
|
|
833
|
+
* driver's. It is the same reason `beginRequest` is re-exported from the
|
|
834
|
+
* generated file rather than reached for here — see `handlerEntrySource`.
|
|
835
|
+
*
|
|
836
|
+
* Nothing is passed for `websocket` or `queue`, and that is not an oversight:
|
|
837
|
+
* uf defines both and implements neither, so a generated file that invented
|
|
838
|
+
* one would be inventing an upgrade for a runtime it cannot see. What the call
|
|
839
|
+
* does supply is the target's name and its two facts, which is what turns "an
|
|
840
|
+
* upgrade is not available" into "the serverless host cannot hold a socket
|
|
841
|
+
* open" and what lets `@uniflowed/server/lambda` refuse a queue that would be
|
|
842
|
+
* dropped.
|
|
843
|
+
*/
|
|
844
|
+
const NODE_CAPABILITIES = { module: "@uniflowed/server/node", name: "nodeCapabilities" };
|
|
845
|
+
const EDGE_CAPABILITIES = { module: "@uniflowed/server/edge", name: "edgeCapabilities" };
|
|
846
|
+
const SERVERLESS_CAPABILITIES = { module: "@uniflowed/server/lambda", name: "lambdaCapabilities" };
|
|
847
|
+
|
|
785
848
|
/**
|
|
786
849
|
* Link the application into a directory that can be copied, for
|
|
787
850
|
* `uf build --adapter`.
|
|
@@ -952,7 +1015,7 @@ async function deploy() {
|
|
|
952
1015
|
* package a bundler happened to give it, and the copy that matters is the one
|
|
953
1016
|
* the application resolved. See ubugeeei-prod/uf#277 and #389.
|
|
954
1017
|
*/
|
|
955
|
-
function handlerEntrySource(document, cache) {
|
|
1018
|
+
function handlerEntrySource(document, cache, capabilities) {
|
|
956
1019
|
const route = cache?.route === true;
|
|
957
1020
|
const fetchCache = cache?.fetch === true;
|
|
958
1021
|
// Nothing at all when both switches are off, so a default project's
|
|
@@ -960,9 +1023,19 @@ function handlerEntrySource(document, cache) {
|
|
|
960
1023
|
// generated output nobody asked for is the second half of the complaint
|
|
961
1024
|
// #277 makes about the first half.
|
|
962
1025
|
const store = route || fetchCache;
|
|
1026
|
+
const options = [
|
|
1027
|
+
"app",
|
|
1028
|
+
`document: ${JSON.stringify(document)}`,
|
|
1029
|
+
...(store ? ["cache"] : []),
|
|
1030
|
+
"capabilities",
|
|
1031
|
+
].join(", ");
|
|
1032
|
+
const cacheImport = store ? 'import { createCacheStore } from "@uniflowed/server/cache";\n' : "";
|
|
1033
|
+
const from = JSON.stringify(capabilities.module);
|
|
1034
|
+
const capabilityImport = `import { ${capabilities.name} } from ${from};`;
|
|
963
1035
|
return `// Generated by \`uf build --adapter\`. Not checked in, not edited.
|
|
964
1036
|
import { createFetchHandler } from "@uniflowed/server/fetch";
|
|
965
|
-
${
|
|
1037
|
+
${cacheImport}${capabilityImport}
|
|
1038
|
+
import * as app from ${JSON.stringify(VIRTUAL.server)};
|
|
966
1039
|
|
|
967
1040
|
${
|
|
968
1041
|
store
|
|
@@ -971,9 +1044,17 @@ ${
|
|
|
971
1044
|
// application. See ubugeeei-prod/uf#277.
|
|
972
1045
|
const cache = { store: createCacheStore(), route: ${String(route)}, fetch: ${String(fetchCache)} };
|
|
973
1046
|
|
|
974
|
-
|
|
975
|
-
:
|
|
976
|
-
}
|
|
1047
|
+
`
|
|
1048
|
+
: ""
|
|
1049
|
+
}// What this target can do, and it is not the same for all four: whether a
|
|
1050
|
+
// response body reaches the client as it is produced, and whether the process
|
|
1051
|
+
// is still there once it has. A route handler that streams events or takes a
|
|
1052
|
+
// socket asks through this rather than finding out in production. Nothing is
|
|
1053
|
+
// passed for the upgrade or the queue — uf defines both and implements
|
|
1054
|
+
// neither. See \`@uniflowed/server/socket\` and \`@uniflowed/server/queue\`.
|
|
1055
|
+
const capabilities = ${capabilities.name}();
|
|
1056
|
+
|
|
1057
|
+
export const fetch = createFetchHandler({ ${options} });
|
|
977
1058
|
export const beginRequest = app.beginRequest;
|
|
978
1059
|
|
|
979
1060
|
export default { fetch, beginRequest };
|
|
@@ -1154,24 +1235,127 @@ function readManifest(outDir) {
|
|
|
1154
1235
|
}
|
|
1155
1236
|
|
|
1156
1237
|
/**
|
|
1157
|
-
*
|
|
1158
|
-
*
|
|
1238
|
+
* What this build renders now, and what it leaves for a server.
|
|
1239
|
+
*
|
|
1240
|
+
* The rendering decision, per route, and it has three answers rather than the
|
|
1241
|
+
* two `staticPaths` used to have:
|
|
1242
|
+
*
|
|
1243
|
+
* * **prerender it** — a route with no parameters, or a route whose page
|
|
1244
|
+
* exports `generateStaticParams`, once per set of parameters it returns;
|
|
1245
|
+
* * **leave it to the server** — a route with parameters and no
|
|
1246
|
+
* `generateStaticParams`, or a page that has said `export const dynamic =
|
|
1247
|
+
* "force-dynamic"`;
|
|
1248
|
+
* * **refuse** — which is not decided here. This function reports what it
|
|
1249
|
+
* found and the caller, which knows whether the project allows a server,
|
|
1250
|
+
* is the one that turns "there is a route here a static host cannot
|
|
1251
|
+
* answer" into an error.
|
|
1252
|
+
*
|
|
1253
|
+
* `dynamic` is the spelling ubugeeei-prod/uf#336 asked for: a route with *no*
|
|
1254
|
+
* parameters whose content depends on the request had no way to say so, and
|
|
1255
|
+
* `generateStaticParams` cannot say it — there are no parameters to generate.
|
|
1256
|
+
* It is Next.js's name for the same declaration, because a person arriving
|
|
1257
|
+
* from `app/` should not have to learn a second word for a decision they have
|
|
1258
|
+
* already made once.
|
|
1259
|
+
*
|
|
1260
|
+
* Two of Next's four values are missing and are not silently accepted:
|
|
1261
|
+
* `"force-static"` and `"error"` are refused by name, because each is a
|
|
1262
|
+
* *constraint* on a page that uf does not yet check, and accepting one would
|
|
1263
|
+
* be reading a declaration and ignoring it — the failure the two issues behind
|
|
1264
|
+
* this function are about.
|
|
1265
|
+
*
|
|
1266
|
+
* Handlers and middleware are in the same list, and they belong there: this is
|
|
1267
|
+
* the list of things that need a process, and a `_uf.route.js` needs one more
|
|
1268
|
+
* obviously than any page does. They carry no per-route render — the build has
|
|
1269
|
+
* never written a file for either — so they appear only when the answer might
|
|
1270
|
+
* be a refusal.
|
|
1271
|
+
*
|
|
1272
|
+
* @param {{routes: Route[], handlers: Handler[], middleware: Middleware[]}} server
|
|
1273
|
+
* @param {"everything" | "possible" | "nothing"} prerender
|
|
1159
1274
|
*/
|
|
1160
|
-
async function
|
|
1275
|
+
async function renderingPlan(server, prerender) {
|
|
1161
1276
|
const urls = [];
|
|
1162
|
-
|
|
1277
|
+
const perRequest = [];
|
|
1278
|
+
|
|
1279
|
+
// Nothing is prerendered and nothing is refused, so no page module is
|
|
1280
|
+
// loaded: a project that renders everything per request should not pay for
|
|
1281
|
+
// a `generateStaticParams` this build will not call.
|
|
1282
|
+
if (prerender === "nothing") {
|
|
1283
|
+
return {
|
|
1284
|
+
urls,
|
|
1285
|
+
perRequest: server.routes.map((route) => ({
|
|
1286
|
+
path: route.path,
|
|
1287
|
+
why: "this build prerenders nothing",
|
|
1288
|
+
})),
|
|
1289
|
+
};
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
for (const route of server.routes) {
|
|
1293
|
+
// Every page module, and not only the parameterised ones: `dynamic` is a
|
|
1294
|
+
// declaration any page can make. A module that cannot be imported at all
|
|
1295
|
+
// is a failure of *that route*, so a route with no parameters goes into
|
|
1296
|
+
// the prerender anyway and the loop below reports it the way it has always
|
|
1297
|
+
// reported a page that throws — named, with the rest of the build still
|
|
1298
|
+
// happening. A parameterised one still rejects out of the build, which is
|
|
1299
|
+
// what it did before there was anything else to load a page module for.
|
|
1300
|
+
let module;
|
|
1301
|
+
try {
|
|
1302
|
+
module = await route.page();
|
|
1303
|
+
} catch (error) {
|
|
1304
|
+
if (route.params.length > 0) throw error;
|
|
1305
|
+
urls.push(route.path);
|
|
1306
|
+
continue;
|
|
1307
|
+
}
|
|
1308
|
+
const declared = module.dynamic ?? "auto";
|
|
1309
|
+
if (declared !== "auto" && declared !== "force-dynamic") {
|
|
1310
|
+
throw new Error(
|
|
1311
|
+
`uf: ${route.file} exports \`dynamic = ${JSON.stringify(declared)}\`, and uf reads ` +
|
|
1312
|
+
'`"auto"` and `"force-dynamic"`. `"force-static"` and `"error"` are Next.js values ' +
|
|
1313
|
+
"for constraints uf does not check yet, and accepting one would be reading a " +
|
|
1314
|
+
"declaration and ignoring it.",
|
|
1315
|
+
);
|
|
1316
|
+
}
|
|
1317
|
+
if (declared === "force-dynamic") {
|
|
1318
|
+
perRequest.push({
|
|
1319
|
+
path: route.path,
|
|
1320
|
+
why: 'its page exports `dynamic = "force-dynamic"`',
|
|
1321
|
+
});
|
|
1322
|
+
continue;
|
|
1323
|
+
}
|
|
1163
1324
|
if (route.params.length === 0) {
|
|
1164
1325
|
urls.push(route.path);
|
|
1165
1326
|
continue;
|
|
1166
1327
|
}
|
|
1167
|
-
const module = await route.page();
|
|
1168
1328
|
const generate = module.generateStaticParams;
|
|
1169
|
-
if (typeof generate !== "function")
|
|
1329
|
+
if (typeof generate !== "function") {
|
|
1330
|
+
perRequest.push({
|
|
1331
|
+
path: route.path,
|
|
1332
|
+
why: "it has parameters and its page exports no `generateStaticParams`",
|
|
1333
|
+
});
|
|
1334
|
+
continue;
|
|
1335
|
+
}
|
|
1170
1336
|
for (const params of await generate()) {
|
|
1171
1337
|
urls.push(fillParams(route.path, params));
|
|
1172
1338
|
}
|
|
1173
1339
|
}
|
|
1174
|
-
|
|
1340
|
+
|
|
1341
|
+
for (const handler of server.handlers ?? []) {
|
|
1342
|
+
perRequest.push({
|
|
1343
|
+
path: handler.path,
|
|
1344
|
+
why: "it is a route handler, and a handler answers a request rather than producing a file",
|
|
1345
|
+
});
|
|
1346
|
+
}
|
|
1347
|
+
for (const entry of server.middleware ?? []) {
|
|
1348
|
+
// A middleware is reported by the path it guards rather than by the route
|
|
1349
|
+
// it guards, which is why it cannot be folded into the loop above: it runs
|
|
1350
|
+
// for a page, for a handler, and for a path under it that is neither, so
|
|
1351
|
+
// "which route is this" has no single answer.
|
|
1352
|
+
perRequest.push({
|
|
1353
|
+
path: `${entry.path === "/" ? "" : entry.path}/*`,
|
|
1354
|
+
why: "a middleware guards it, and a middleware runs once per request",
|
|
1355
|
+
});
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
return { urls, perRequest };
|
|
1175
1359
|
}
|
|
1176
1360
|
|
|
1177
1361
|
function fillParams(routePath, params) {
|