@uniflowed/vite 0.0.0-alpha.13 → 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 +281 -147
- package/index.js +184 -90
- package/internal/diagnostics.js +366 -0
- package/internal/events.js +5 -5
- package/internal/routes.js +44 -6
- package/internal/serve.js +39 -15
- 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,100 +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
|
-
// A server action next, below the guard and above the handlers. It
|
|
250
|
-
// declines every request that carries no action id, so this costs a
|
|
251
|
-
// page request one header lookup; and it answers every request that
|
|
252
|
-
// carries one, refusals included, so an action can never fall through
|
|
253
|
-
// to a route handler that happens to sit at the URL it was posted to.
|
|
254
|
-
const acted = await entry.callAction(asRequest);
|
|
255
|
-
if (acted != null) {
|
|
256
|
-
await send(response, acted);
|
|
257
|
-
return true;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
// Route handlers next, and for every method: a handler is the only
|
|
261
|
-
// thing that answers a POST, and it may also answer a GET for a path
|
|
262
|
-
// that has no page.
|
|
263
|
-
const handled = await entry.dispatch(asRequest);
|
|
264
|
-
if (handled != null) {
|
|
265
|
-
await send(response, handled);
|
|
266
|
-
return true;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
// Only a navigation reaches the renderer. A page cannot answer a POST,
|
|
270
|
-
// and letting one try would turn a missing handler into a rendered page
|
|
271
|
-
// with a 200 rather than a 404.
|
|
272
|
-
if (request.method !== "GET" && request.method !== "HEAD") {
|
|
273
|
-
return false;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
const result = await entry.render(url, assets, {
|
|
277
|
-
// A boundary that threw after the shell went out. `result.error` cannot
|
|
278
|
-
// carry it — the caller already has the result by then — so the
|
|
279
|
-
// terminal hears about it here or not at all.
|
|
280
|
-
onError: (error) => reportRenderError(server, url, error),
|
|
281
|
-
});
|
|
282
|
-
if (result.error != null) reportRenderError(server, url, result.error);
|
|
283
|
-
const html = await server.transformIndexHtml(url, await result.text());
|
|
284
|
-
response.statusCode = result.status ?? 200;
|
|
285
|
-
response.setHeader("content-type", "text/html; charset=utf-8");
|
|
286
|
-
response.end(html);
|
|
287
|
-
return true;
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
if (!answered) {
|
|
291
|
-
// The one path where uf is not the one writing the response: a
|
|
292
|
-
// non-navigation nothing claimed goes back to Vite's chain. The guard
|
|
293
|
-
// has still run and may have deferred work, so `close` — the socket
|
|
294
|
-
// saying the response is over, however it ended — is the only honest
|
|
295
|
-
// signal left that the bytes are out.
|
|
296
|
-
response.once("close", lifecycle.settle);
|
|
297
|
-
next();
|
|
298
|
-
return;
|
|
299
|
-
}
|
|
300
|
-
await lifecycle.settle();
|
|
301
|
-
} catch (error) {
|
|
302
|
-
if (lifecycle != null) await lifecycle.settle();
|
|
303
|
-
// Map the stack back onto the Flow source before it reaches the overlay.
|
|
304
|
-
if (error instanceof Error) server.ssrFixStacktrace(error);
|
|
305
|
-
next(error);
|
|
306
|
-
}
|
|
307
|
-
});
|
|
308
|
-
|
|
309
229
|
await server.listen();
|
|
310
230
|
const urls = server.resolvedUrls ?? { local: [], network: [] };
|
|
311
231
|
emit("listening", {
|
|
@@ -316,6 +236,7 @@ async function dev() {
|
|
|
316
236
|
),
|
|
317
237
|
});
|
|
318
238
|
watchSources(server);
|
|
239
|
+
watchEnvFiles(server);
|
|
319
240
|
|
|
320
241
|
const shutdown = async () => {
|
|
321
242
|
await server.close();
|
|
@@ -362,6 +283,45 @@ function watchSources(server) {
|
|
|
362
283
|
}
|
|
363
284
|
}
|
|
364
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
|
+
|
|
365
325
|
/**
|
|
366
326
|
* The preview server: the build, as Vite serves it.
|
|
367
327
|
*
|
|
@@ -386,37 +346,57 @@ async function preview() {
|
|
|
386
346
|
const { preview: startPreview } = await import("vite");
|
|
387
347
|
const config = await loadConfig();
|
|
388
348
|
const inline = await viteConfig(config, argument("--mode") ?? "production");
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
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
|
+
});
|
|
394
363
|
|
|
395
364
|
const server = await startPreview({ ...inline, appType: "custom" });
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
await
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
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
|
+
}
|
|
413
384
|
|
|
414
385
|
const urls = server.resolvedUrls ?? { local: [], network: [] };
|
|
415
386
|
emit("listening", {
|
|
416
387
|
local: urls.local,
|
|
417
388
|
network: urls.network,
|
|
418
|
-
|
|
419
|
-
|
|
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),
|
|
420
400
|
});
|
|
421
401
|
|
|
422
402
|
const shutdown = async () => {
|
|
@@ -498,6 +478,15 @@ async function build() {
|
|
|
498
478
|
const inline = await viteConfig(config, mode);
|
|
499
479
|
const outDir = path.resolve(root, inline.build.outDir);
|
|
500
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";
|
|
501
490
|
|
|
502
491
|
// 1. The client: everything the browser loads, with a manifest so the
|
|
503
492
|
// server render knows which script and stylesheet tags to write.
|
|
@@ -530,11 +519,39 @@ async function build() {
|
|
|
530
519
|
},
|
|
531
520
|
});
|
|
532
521
|
|
|
533
|
-
// 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.
|
|
534
528
|
emit("phase", { name: "prerender" });
|
|
535
529
|
const server = await import(pathToFileURL(path.join(serverDir, "server.js")).href);
|
|
536
530
|
const assets = assetsFromManifest(manifest);
|
|
537
|
-
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;
|
|
538
555
|
|
|
539
556
|
// A route that throws fails *that route*, and the rest of the build still
|
|
540
557
|
// happens. This loop had no `try`: the first page to throw rejected out of
|
|
@@ -598,7 +615,11 @@ async function build() {
|
|
|
598
615
|
// host would then serve uf's error page to every visitor who mistyped a URL,
|
|
599
616
|
// and nothing between the throw and the deploy would have mentioned it.
|
|
600
617
|
let attempted = pages.length;
|
|
601
|
-
|
|
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 === "/")) {
|
|
602
623
|
attempted += 1;
|
|
603
624
|
// `/404` rather than `/__uf_not_found__`: the internal path is how the
|
|
604
625
|
// router is asked, and the file the reader is looking for is `404.html`.
|
|
@@ -645,6 +666,16 @@ async function build() {
|
|
|
645
666
|
process.exit(1);
|
|
646
667
|
}
|
|
647
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
|
+
|
|
648
679
|
emit("done", { outDir: path.relative(root, outDir), pages: pages.length });
|
|
649
680
|
process.exit(0);
|
|
650
681
|
}
|
|
@@ -1204,24 +1235,127 @@ function readManifest(outDir) {
|
|
|
1204
1235
|
}
|
|
1205
1236
|
|
|
1206
1237
|
/**
|
|
1207
|
-
*
|
|
1208
|
-
*
|
|
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
|
|
1209
1274
|
*/
|
|
1210
|
-
async function
|
|
1275
|
+
async function renderingPlan(server, prerender) {
|
|
1211
1276
|
const urls = [];
|
|
1212
|
-
|
|
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
|
+
}
|
|
1213
1324
|
if (route.params.length === 0) {
|
|
1214
1325
|
urls.push(route.path);
|
|
1215
1326
|
continue;
|
|
1216
1327
|
}
|
|
1217
|
-
const module = await route.page();
|
|
1218
1328
|
const generate = module.generateStaticParams;
|
|
1219
|
-
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
|
+
}
|
|
1220
1336
|
for (const params of await generate()) {
|
|
1221
1337
|
urls.push(fillParams(route.path, params));
|
|
1222
1338
|
}
|
|
1223
1339
|
}
|
|
1224
|
-
|
|
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 };
|
|
1225
1359
|
}
|
|
1226
1360
|
|
|
1227
1361
|
function fillParams(routePath, params) {
|