@uniflowed/vite 0.0.0-alpha.13 → 0.0.0-alpha.15
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 +436 -149
- package/index.js +217 -93
- package/internal/assets.js +266 -18
- package/internal/devtools.js +117 -0
- package/internal/diagnostics.js +369 -0
- package/internal/events.js +5 -5
- package/internal/routes.js +62 -8
- package/internal/serve.js +39 -15
- package/package.json +11 -3
package/driver.js
CHANGED
|
@@ -6,7 +6,12 @@
|
|
|
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>]
|
|
13
|
+
// <host> driver.js library --root <dir> [--mode <m>] [--out-dir <dir>]
|
|
14
|
+
// --entry <file>... --format <es|cjs>... [--external <name>]...
|
|
10
15
|
// <host> driver.js compile --root <dir> [--mode <m>] [--out-dir <dir>] --assets <file> --bundle <dir>
|
|
11
16
|
// <host> driver.js deploy --root <dir> [--mode <m>] [--out-dir <dir>] --adapter <name> --work <dir> --output <dir>
|
|
12
17
|
// <host> driver.js preview --root <dir> [--mode <m>] [--out-dir <dir>] [--host <h>] [--port <n>]
|
|
@@ -19,6 +24,12 @@
|
|
|
19
24
|
// environment — see `viteConfig` below and `crates/uf_config/src/env_files.rs`.
|
|
20
25
|
// `start` has no Vite in it and therefore no mode.
|
|
21
26
|
//
|
|
27
|
+
// `--uf-env-file` names those files, one flag each, so `dev` can watch them and
|
|
28
|
+
// say when one moved; nothing here reads their contents. The prefix is load
|
|
29
|
+
// bearing: node claims `--env-file` for itself and honours it wherever it
|
|
30
|
+
// appears on the command line, script arguments included, so a driver argument
|
|
31
|
+
// by that name is an argument node eats and then exits 9 over.
|
|
32
|
+
//
|
|
22
33
|
// `uf` in Rust owns the terminal; this process owns Vite. They talk over
|
|
23
34
|
// stdout, one JSON event per line (see `./internal/events.js`), and the driver
|
|
24
35
|
// exits when its stdin closes so it cannot outlive the command that started
|
|
@@ -29,12 +40,12 @@
|
|
|
29
40
|
// one host that can evaluate the file evaluates it.
|
|
30
41
|
|
|
31
42
|
import { createServer as createHttpServer } from "node:http";
|
|
32
|
-
import { register } from "node:module";
|
|
43
|
+
import { builtinModules, register } from "node:module";
|
|
33
44
|
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
34
45
|
import path from "node:path";
|
|
35
46
|
import { pathToFileURL } from "node:url";
|
|
36
47
|
|
|
37
|
-
import { emit, errorEvent, eventLogger
|
|
48
|
+
import { emit, errorEvent, eventLogger } from "./internal/events.js";
|
|
38
49
|
import { loadUfConfig, projectConfig } from "./internal/config.js";
|
|
39
50
|
import { send, toRequest } from "./internal/http.js";
|
|
40
51
|
import { withProjectConfig } from "./merge.js";
|
|
@@ -52,6 +63,16 @@ function argument(name) {
|
|
|
52
63
|
return at === -1 ? null : process.argv[at + 1];
|
|
53
64
|
}
|
|
54
65
|
|
|
66
|
+
/** Every value of a repeated argument, in the order they were given. */
|
|
67
|
+
function argumentAll(name) {
|
|
68
|
+
const values = [];
|
|
69
|
+
for (let at = 0; at < process.argv.length; at += 1) {
|
|
70
|
+
if (process.argv[at] === name && process.argv[at + 1] != null)
|
|
71
|
+
values.push(process.argv[at + 1]);
|
|
72
|
+
}
|
|
73
|
+
return values;
|
|
74
|
+
}
|
|
75
|
+
|
|
55
76
|
function flag(name) {
|
|
56
77
|
return process.argv.includes(name);
|
|
57
78
|
}
|
|
@@ -78,7 +99,7 @@ process.stdin.on("end", () => process.exit(0));
|
|
|
78
99
|
process.stdin.on("error", () => process.exit(0));
|
|
79
100
|
process.stdin.resume();
|
|
80
101
|
|
|
81
|
-
const commands = { dev, build, compile, deploy, preview, start, config: printConfig };
|
|
102
|
+
const commands = { dev, build, library, compile, deploy, preview, start, config: printConfig };
|
|
82
103
|
const run = commands[command];
|
|
83
104
|
if (run == null) {
|
|
84
105
|
emit("error", { message: `unknown driver command ${JSON.stringify(command)}` });
|
|
@@ -182,26 +203,21 @@ async function viteConfig(config, mode) {
|
|
|
182
203
|
* Vite in middleware mode serves nothing on its own: with no `index.html` at
|
|
183
204
|
* the project root it answers every navigation with "Cannot GET /", which is
|
|
184
205
|
* 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
|
|
206
|
+
* `index.html` — the document comes from a layout — so the server has to
|
|
207
|
+
* render it.
|
|
187
208
|
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
209
|
+
* That rendering is **not** here. It is one middleware, in `./index.js`'s
|
|
210
|
+
* `configureServer`, and this function installs none of its own. It used to
|
|
211
|
+
* install a second one, and two middlewares rendering the same request is how
|
|
212
|
+
* `uf dev` came to answer a route handler with a page and a redirect without
|
|
213
|
+
* its `Location`: `configureServer`'s post hook runs inside `createServer`,
|
|
214
|
+
* and anything added here runs after it returns, so of the two the plugin's
|
|
215
|
+
* was always the one that decided. See ubugeeei-prod/uf#349 and #338, and the
|
|
216
|
+
* comment above that middleware for what it now has to do.
|
|
195
217
|
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
* have no such hook and stream — see `internal/serve.js` — and it is worth
|
|
200
|
-
* being clear that this is a property of the development server rather than of
|
|
201
|
-
* the renderer. Streaming through the transform is ubugeeei-prod/uf#374.
|
|
202
|
-
*
|
|
203
|
-
* Anything Vite already serves — a module, a public file — never reaches this,
|
|
204
|
-
* because the middleware runs after Vite's own.
|
|
218
|
+
* What is left here is the half that is genuinely the driver's: the Vite
|
|
219
|
+
* config, the socket, the event channel back to `uf`, and the two watchers
|
|
220
|
+
* below.
|
|
205
221
|
*/
|
|
206
222
|
async function dev() {
|
|
207
223
|
const { createServer } = await import("vite");
|
|
@@ -212,100 +228,6 @@ async function dev() {
|
|
|
212
228
|
const inline = await viteConfig(config, argument("--mode") ?? "development");
|
|
213
229
|
const server = await createServer({ ...inline, appType: "custom" });
|
|
214
230
|
|
|
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
231
|
await server.listen();
|
|
310
232
|
const urls = server.resolvedUrls ?? { local: [], network: [] };
|
|
311
233
|
emit("listening", {
|
|
@@ -316,6 +238,7 @@ async function dev() {
|
|
|
316
238
|
),
|
|
317
239
|
});
|
|
318
240
|
watchSources(server);
|
|
241
|
+
watchEnvFiles(server);
|
|
319
242
|
|
|
320
243
|
const shutdown = async () => {
|
|
321
244
|
await server.close();
|
|
@@ -362,6 +285,45 @@ function watchSources(server) {
|
|
|
362
285
|
}
|
|
363
286
|
}
|
|
364
287
|
|
|
288
|
+
/**
|
|
289
|
+
* Restart the server when one of the `.env` files uf read changes.
|
|
290
|
+
*
|
|
291
|
+
* uf reads the `.env` cascade itself, in Rust, before this process starts —
|
|
292
|
+
* one parser, one precedence, one answer for every command (see `viteConfig`
|
|
293
|
+
* above and `crates/uf_config/src/env_files.rs`) — and `envDir: false` turns
|
|
294
|
+
* Vite's own file loading off so there cannot be two answers. The cost of that
|
|
295
|
+
* was that nothing watched them: a value edited while `uf dev` ran changed
|
|
296
|
+
* nothing until somebody restarted the command by hand, and the guide had to
|
|
297
|
+
* document it as a limitation. See ubugeeei-prod/uf#428.
|
|
298
|
+
*
|
|
299
|
+
* `uf` passes the files it would consult with `--uf-env-file`, one per file, in
|
|
300
|
+
* cascade order, whether or not each exists today — a `.env.local` *created*
|
|
301
|
+
* while the server runs changes the answer exactly as much as an edit to one
|
|
302
|
+
* that was already there, and watching only what was read would have missed
|
|
303
|
+
* it. They are added to Vite's watcher explicitly because they are in no
|
|
304
|
+
* module graph, which is the same reason the RSC manifest is added in
|
|
305
|
+
* `index.js`.
|
|
306
|
+
*
|
|
307
|
+
* What is emitted is "these values are stale", and the Rust side restarts this
|
|
308
|
+
* process with the files re-read. A restart rather than a hot update is the
|
|
309
|
+
* honest granularity: a prefixed value reaches the browser by substitution
|
|
310
|
+
* into the bundle, so a new value has to be substituted again, and every
|
|
311
|
+
* module that read one has to be re-evaluated. Vite's watcher is still the
|
|
312
|
+
* only watcher — a second one over the same tree, in Rust, would be a second
|
|
313
|
+
* answer to "did this file change".
|
|
314
|
+
*/
|
|
315
|
+
function watchEnvFiles(server) {
|
|
316
|
+
const files = argumentAll("--uf-env-file").map((file) => path.resolve(root, file));
|
|
317
|
+
if (files.length === 0) return;
|
|
318
|
+
const watched = new Set(files);
|
|
319
|
+
server.watcher.add(files);
|
|
320
|
+
for (const event of ["add", "change", "unlink"]) {
|
|
321
|
+
server.watcher.on(event, (file) => {
|
|
322
|
+
if (watched.has(path.resolve(file))) emit("env-changed", { file, change: event });
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
365
327
|
/**
|
|
366
328
|
* The preview server: the build, as Vite serves it.
|
|
367
329
|
*
|
|
@@ -386,37 +348,57 @@ async function preview() {
|
|
|
386
348
|
const { preview: startPreview } = await import("vite");
|
|
387
349
|
const config = await loadConfig();
|
|
388
350
|
const inline = await viteConfig(config, argument("--mode") ?? "production");
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
351
|
+
// A build that declared it emits no server has none to mount. `uf` refuses
|
|
352
|
+
// `uf start` for such a project and lets this one through, because a preview
|
|
353
|
+
// of files *is* the deployment: what a static host does with `dist/` is
|
|
354
|
+
// exactly what Vite's preview server does with it, and mounting a request
|
|
355
|
+
// handler behind it would make this preview right about a deployment that is
|
|
356
|
+
// not the one happening. See `uf_cli`'s `commands::serve`.
|
|
357
|
+
const staticBuild = flag("--static-build");
|
|
358
|
+
const build = staticBuild
|
|
359
|
+
? null
|
|
360
|
+
: await loadBuild({
|
|
361
|
+
root,
|
|
362
|
+
outDir: inline.build.outDir,
|
|
363
|
+
serverDir: path.join(".uf", "build", "server"),
|
|
364
|
+
});
|
|
394
365
|
|
|
395
366
|
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
|
-
|
|
367
|
+
if (build != null) {
|
|
368
|
+
const handle = createServeHandler({ ...build, cache: config.app?.rendering?.cache });
|
|
369
|
+
server.middlewares.use(async (request, response, next) => {
|
|
370
|
+
try {
|
|
371
|
+
const asRequest = await toRequest(request, server.config);
|
|
372
|
+
// The same lifecycle `uf start` gets from `nodeListener`, spelled out
|
|
373
|
+
// because this door is Vite's connect chain rather than a bare
|
|
374
|
+
// `node:http` server: the whole request runs inside it, and it settles
|
|
375
|
+
// once `send` has returned. A preview whose `after()` fired at a
|
|
376
|
+
// different moment from the production server's would be a preview that
|
|
377
|
+
// is checked and believed and wrong.
|
|
378
|
+
await withRequest(build.entry, asRequest, async () => {
|
|
379
|
+
await send(response, await handle(asRequest));
|
|
380
|
+
});
|
|
381
|
+
} catch (error) {
|
|
382
|
+
next(error);
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
}
|
|
413
386
|
|
|
414
387
|
const urls = server.resolvedUrls ?? { local: [], network: [] };
|
|
415
388
|
emit("listening", {
|
|
416
389
|
local: urls.local,
|
|
417
390
|
network: urls.network,
|
|
418
|
-
|
|
419
|
-
|
|
391
|
+
// From the filesystem when there is no bundle to ask, which is the same
|
|
392
|
+
// scan `dev` reports from. The count is what a reader checks the build
|
|
393
|
+
// against, so answering "0 routes" for a static site that has thirty would
|
|
394
|
+
// be the report being wrong about the thing it exists to report.
|
|
395
|
+
routes:
|
|
396
|
+
build == null
|
|
397
|
+
? scanRoutes(path.resolve(root, config.app?.router?.root ?? "app")).routes.map(
|
|
398
|
+
(route) => route.path,
|
|
399
|
+
)
|
|
400
|
+
: build.entry.routes.map((route) => route.path),
|
|
401
|
+
handlers: build == null ? [] : build.entry.handlers.map((handler) => handler.path),
|
|
420
402
|
});
|
|
421
403
|
|
|
422
404
|
const shutdown = async () => {
|
|
@@ -498,6 +480,15 @@ async function build() {
|
|
|
498
480
|
const inline = await viteConfig(config, mode);
|
|
499
481
|
const outDir = path.resolve(root, inline.build.outDir);
|
|
500
482
|
const serverDir = path.join(root, ".uf", "build", "server");
|
|
483
|
+
// How much of the route table to prerender, and whether the server bundle
|
|
484
|
+
// survives the build. Both are `uf`'s answer rather than this file's: they
|
|
485
|
+
// come from two settings in `uf.config.js` that only mean something read
|
|
486
|
+
// together, and `uf_config`'s `RenderingPlan` is where they are. A driver
|
|
487
|
+
// started by hand gets the behaviour every uf build had before either
|
|
488
|
+
// setting was read.
|
|
489
|
+
const prerender = argument("--prerender") ?? "possible";
|
|
490
|
+
const staticBuild = flag("--static-build");
|
|
491
|
+
const because = argument("--because") ?? "this build prerenders every route";
|
|
501
492
|
|
|
502
493
|
// 1. The client: everything the browser loads, with a manifest so the
|
|
503
494
|
// server render knows which script and stylesheet tags to write.
|
|
@@ -530,11 +521,39 @@ async function build() {
|
|
|
530
521
|
},
|
|
531
522
|
});
|
|
532
523
|
|
|
533
|
-
// 3.
|
|
524
|
+
// 3. Which routes this build renders when, and every route it renders now.
|
|
525
|
+
//
|
|
526
|
+
// The decision comes from `uf.config.js` and is made in Rust — see
|
|
527
|
+
// `uf_config`'s `RenderingPlan` — because `app.rendering.modes` and
|
|
528
|
+
// `build.staticBuild` are two settings that have to be read together. It
|
|
529
|
+
// arrives here as one word, and this is where it meets the route table.
|
|
534
530
|
emit("phase", { name: "prerender" });
|
|
535
531
|
const server = await import(pathToFileURL(path.join(serverDir, "server.js")).href);
|
|
536
532
|
const assets = assetsFromManifest(manifest);
|
|
537
|
-
const
|
|
533
|
+
const plan = await renderingPlan(server, prerender);
|
|
534
|
+
emit("rendering", {
|
|
535
|
+
prerender,
|
|
536
|
+
prerendered: plan.urls.length,
|
|
537
|
+
perRequest: plan.perRequest.map((route) => route.path),
|
|
538
|
+
});
|
|
539
|
+
// A build that has to prerender everything, and a route it cannot: the
|
|
540
|
+
// refusal ubugeeei-prod/uf#336 and ubugeeei-prod/uf#385 are both about.
|
|
541
|
+
// Before the loop below, so no document is written for a build that is not
|
|
542
|
+
// going to be one, and with the whole list rather than the first item — a
|
|
543
|
+
// project that has just narrowed `rendering.modes` wants to see every route
|
|
544
|
+
// the narrowing costs it, not one per rebuild.
|
|
545
|
+
if (prerender === "everything" && plan.perRequest.length > 0) {
|
|
546
|
+
const listed = plan.perRequest.map((entry) => ` ${entry.path} — ${entry.why}`).join("\n");
|
|
547
|
+
emit("error", {
|
|
548
|
+
message:
|
|
549
|
+
`${plan.perRequest.length} ${plural(plan.perRequest.length, "route")} in this project ` +
|
|
550
|
+
`can only be answered by a server, and ${because}\n${listed}\n\n` +
|
|
551
|
+
"Give each page a `generateStaticParams` and take out the handlers and middleware, or " +
|
|
552
|
+
'allow `"ssr"` in `app.rendering.modes` and deploy a server.',
|
|
553
|
+
});
|
|
554
|
+
process.exit(1);
|
|
555
|
+
}
|
|
556
|
+
const pages = plan.urls;
|
|
538
557
|
|
|
539
558
|
// A route that throws fails *that route*, and the rest of the build still
|
|
540
559
|
// happens. This loop had no `try`: the first page to throw rejected out of
|
|
@@ -598,7 +617,11 @@ async function build() {
|
|
|
598
617
|
// host would then serve uf's error page to every visitor who mistyped a URL,
|
|
599
618
|
// and nothing between the throw and the deploy would have mentioned it.
|
|
600
619
|
let attempted = pages.length;
|
|
601
|
-
|
|
620
|
+
// Not for a build that prerenders nothing. `404.html` is a file a static
|
|
621
|
+
// host serves for every path it has no file for, and a project whose
|
|
622
|
+
// `rendering.modes` allows only `ssr` has no such host: its not-found
|
|
623
|
+
// boundary is rendered per request, by the server, with the right status.
|
|
624
|
+
if (prerender !== "nothing" && server.notFound.some((boundary) => boundary.path === "/")) {
|
|
602
625
|
attempted += 1;
|
|
603
626
|
// `/404` rather than `/__uf_not_found__`: the internal path is how the
|
|
604
627
|
// router is asked, and the file the reader is looking for is `404.html`.
|
|
@@ -645,10 +668,161 @@ async function build() {
|
|
|
645
668
|
process.exit(1);
|
|
646
669
|
}
|
|
647
670
|
|
|
671
|
+
// `build.staticBuild` is "prerender everything and emit no server bundle",
|
|
672
|
+
// and this is the second half of it. The bundle is still *built*: the
|
|
673
|
+
// prerender renders through it, so a build with no server bundle at any
|
|
674
|
+
// point would be a build with no documents either. What the declaration is
|
|
675
|
+
// about is what is left behind — so it goes once the last document is
|
|
676
|
+
// written, and `uf start`, `uf preview` and every server adapter then find
|
|
677
|
+
// nothing to serve, which is the honest outcome for a project that said it
|
|
678
|
+
// deploys files.
|
|
679
|
+
if (staticBuild) rmSync(serverDir, { recursive: true, force: true });
|
|
680
|
+
|
|
648
681
|
emit("done", { outDir: path.relative(root, outDir), pages: pages.length });
|
|
649
682
|
process.exit(0);
|
|
650
683
|
}
|
|
651
684
|
|
|
685
|
+
/**
|
|
686
|
+
* The library build, for a project whose `app.router.enabled` is false.
|
|
687
|
+
*
|
|
688
|
+
* `build` above is an application build and has no other mode: it links
|
|
689
|
+
* `virtual:uf/client`, which imports the router and the project's `app.js`.
|
|
690
|
+
* A library has neither, so `uf build` in a project `uf create lib`
|
|
691
|
+
* scaffolded failed at the first pass with `Could not resolve '<root>/app.js'`
|
|
692
|
+
* — a file a library does not have and never had. See ubugeeei-prod/uf#268.
|
|
693
|
+
*
|
|
694
|
+
* This is the fourth thing the driver does, beside `dev`, `build` and
|
|
695
|
+
* `compile`, and it is one pass per format over one input list. Which of the
|
|
696
|
+
* two builds runs is **not decided here**: `uf` resolves it from the config
|
|
697
|
+
* (`uf_config`'s `LibraryPlan`) and spawns this subcommand, the same way
|
|
698
|
+
* `--prerender` arrives as one word rather than as two settings for this file
|
|
699
|
+
* to read together.
|
|
700
|
+
*
|
|
701
|
+
* # The three ways it differs from the application build
|
|
702
|
+
*
|
|
703
|
+
* * **Every dependency stays an import.** `--external` names them, and
|
|
704
|
+
* `uf` computes the list from the project's own manifest —
|
|
705
|
+
* `dependencies`, `peerDependencies`, `optionalDependencies` — so a
|
|
706
|
+
* library ships its own modules and nobody else's. That is the opposite
|
|
707
|
+
* of the application build, which inlines what it can because an
|
|
708
|
+
* application is the end of the line and a library is not: a bundled copy
|
|
709
|
+
* of React inside a library is a second React in every application that
|
|
710
|
+
* installs it.
|
|
711
|
+
* * **One output per entry, named after the entry.** `index.js` becomes
|
|
712
|
+
* `dist/index.js`; `internal/parse.js` becomes `dist/internal/parse.js`.
|
|
713
|
+
* The path rather than the basename, so two entries cannot collide at the
|
|
714
|
+
* moment one would overwrite the other.
|
|
715
|
+
* * **No manifest, no prerender, no server bundle.** There is no document to
|
|
716
|
+
* write and no route table to write it from.
|
|
717
|
+
*
|
|
718
|
+
* Vite's own `build.lib` does the work. uf owns *that* a library is a
|
|
719
|
+
* different build and what goes into it; how this builder performs one is the
|
|
720
|
+
* builder's, which is the same line `build` draws around `rollupOptions`.
|
|
721
|
+
*/
|
|
722
|
+
async function library() {
|
|
723
|
+
const vite = await import("vite");
|
|
724
|
+
const config = await loadConfig();
|
|
725
|
+
const inline = await viteConfig(config, argument("--mode") ?? "production");
|
|
726
|
+
const outDir = path.resolve(root, inline.build.outDir);
|
|
727
|
+
const entries = argumentAll("--entry");
|
|
728
|
+
const formats = argumentAll("--format");
|
|
729
|
+
const external = argumentAll("--external");
|
|
730
|
+
if (entries.length === 0) {
|
|
731
|
+
throw new Error("uf: `driver.js library` needs at least one --entry");
|
|
732
|
+
}
|
|
733
|
+
if (formats.length === 0) {
|
|
734
|
+
throw new Error("uf: `driver.js library` needs at least one --format");
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// Keyed by the entry's path without its extension, which is what Vite's lib
|
|
738
|
+
// mode turns into the output file name.
|
|
739
|
+
const input = {};
|
|
740
|
+
for (const entry of entries) {
|
|
741
|
+
input[entryName(entry)] = path.resolve(root, entry);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
const isExternal = externalTest(external);
|
|
745
|
+
// One pass per format rather than one build with several outputs: Vite's
|
|
746
|
+
// lib mode writes a whole `outDir` per format, and the second pass must not
|
|
747
|
+
// empty what the first wrote. So `emptyOutDir` is true exactly once, on the
|
|
748
|
+
// first, which is also what makes a build that dropped an entry leave no
|
|
749
|
+
// stale copy of it behind.
|
|
750
|
+
let first = true;
|
|
751
|
+
for (const format of formats) {
|
|
752
|
+
emit("phase", { name: `library (${format})` });
|
|
753
|
+
await vite.build({
|
|
754
|
+
...inline,
|
|
755
|
+
build: {
|
|
756
|
+
...inline.build,
|
|
757
|
+
// Vite's `manifest` maps source modules to hashed browser assets. A
|
|
758
|
+
// library has neither — its file names are its API — and writing one
|
|
759
|
+
// would put a `.vite/` directory into a published tarball.
|
|
760
|
+
manifest: false,
|
|
761
|
+
outDir,
|
|
762
|
+
emptyOutDir: first,
|
|
763
|
+
lib: {
|
|
764
|
+
entry: input,
|
|
765
|
+
formats: [format],
|
|
766
|
+
fileName: (_format, name) => `${name}.${format === "cjs" ? "cjs" : "js"}`,
|
|
767
|
+
},
|
|
768
|
+
rollupOptions: { external: isExternal },
|
|
769
|
+
},
|
|
770
|
+
});
|
|
771
|
+
first = false;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
emit("done", { outDir: path.relative(root, outDir), pages: 0 });
|
|
775
|
+
process.exit(0);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
/**
|
|
779
|
+
* The output name for one entry: its path, without the extension.
|
|
780
|
+
*
|
|
781
|
+
* Not the basename. `index.js` and `internal/index.js` are two entries a
|
|
782
|
+
* library can reasonably have, and under a basename they are one file written
|
|
783
|
+
* twice — the second silently winning, which is a published package whose
|
|
784
|
+
* subpath export is somebody else's module.
|
|
785
|
+
*/
|
|
786
|
+
function entryName(entry) {
|
|
787
|
+
const normalised = entry.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
788
|
+
const dot = normalised.lastIndexOf(".");
|
|
789
|
+
const slash = normalised.lastIndexOf("/");
|
|
790
|
+
return dot > slash ? normalised.slice(0, dot) : normalised;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
/**
|
|
794
|
+
* Whether an import is somebody else's module.
|
|
795
|
+
*
|
|
796
|
+
* Three checks, and only the one over `names` is a policy uf decided. `names`
|
|
797
|
+
* is what `uf` read out of the project's manifest and passed as `--external`,
|
|
798
|
+
* and a subpath of one of those names — `@scope/pkg/deep` for `@scope/pkg` —
|
|
799
|
+
* is the same package. The other two are the host's built-in modules, and they
|
|
800
|
+
* are a fact rather than a decision: `node:fs` has no bytes to inline.
|
|
801
|
+
*
|
|
802
|
+
* A bare relative or absolute id is never external, which is the rule that
|
|
803
|
+
* makes this a library build at all: what the author wrote is bundled, and
|
|
804
|
+
* what they installed is imported.
|
|
805
|
+
*/
|
|
806
|
+
function externalTest(names) {
|
|
807
|
+
const declared = new Set(names);
|
|
808
|
+
// The host's built-in module names, unprefixed. `node:`-prefixed ids are
|
|
809
|
+
// caught by the first check whatever the host is; this set is for the bare
|
|
810
|
+
// spellings — `fs`, `path`, `stream` — which a dependency written before the
|
|
811
|
+
// prefix existed still uses. Read from the running host rather than written
|
|
812
|
+
// down, because the list grows and a stale copy of it here would be a
|
|
813
|
+
// bundled `node:worker_threads` that cannot be bundled.
|
|
814
|
+
const builtins = new Set(builtinModules ?? []);
|
|
815
|
+
return (id) => {
|
|
816
|
+
if (id.startsWith("node:")) return true;
|
|
817
|
+
if (builtins.has(id)) return true;
|
|
818
|
+
if (declared.has(id)) return true;
|
|
819
|
+
for (const name of declared) {
|
|
820
|
+
if (id.startsWith(`${name}/`)) return true;
|
|
821
|
+
}
|
|
822
|
+
return false;
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
|
|
652
826
|
/**
|
|
653
827
|
* Link the whole application into one JavaScript file, for `uf build --compile`.
|
|
654
828
|
*
|
|
@@ -856,6 +1030,16 @@ const SERVERLESS_CAPABILITIES = { module: "@uniflowed/server/lambda", name: "lam
|
|
|
856
1030
|
* copying every file in it is bulk work over the whole build, which belongs in
|
|
857
1031
|
* Rust rather than in the host process — the same division `--compile` makes
|
|
858
1032
|
* with its embedded assets.
|
|
1033
|
+
*
|
|
1034
|
+
* # `--adapter static` never reaches this function
|
|
1035
|
+
*
|
|
1036
|
+
* It is the one implemented target with no application to link: a static host
|
|
1037
|
+
* returns files, and `uf build` has already written them. So `uf` copies the
|
|
1038
|
+
* output directory itself and never spawns this driver for it, which is why
|
|
1039
|
+
* [`ADAPTERS`] has four rows and not five. What that target does instead of
|
|
1040
|
+
* linking is refuse a project whose route handlers, middleware, unprerendered
|
|
1041
|
+
* routes or server actions a static host cannot answer — in Rust, because the
|
|
1042
|
+
* facts it needs are the route table and what the prerender reported.
|
|
859
1043
|
*/
|
|
860
1044
|
async function deploy() {
|
|
861
1045
|
const vite = await import("vite");
|
|
@@ -1204,24 +1388,127 @@ function readManifest(outDir) {
|
|
|
1204
1388
|
}
|
|
1205
1389
|
|
|
1206
1390
|
/**
|
|
1207
|
-
*
|
|
1208
|
-
*
|
|
1391
|
+
* What this build renders now, and what it leaves for a server.
|
|
1392
|
+
*
|
|
1393
|
+
* The rendering decision, per route, and it has three answers rather than the
|
|
1394
|
+
* two `staticPaths` used to have:
|
|
1395
|
+
*
|
|
1396
|
+
* * **prerender it** — a route with no parameters, or a route whose page
|
|
1397
|
+
* exports `generateStaticParams`, once per set of parameters it returns;
|
|
1398
|
+
* * **leave it to the server** — a route with parameters and no
|
|
1399
|
+
* `generateStaticParams`, or a page that has said `export const dynamic =
|
|
1400
|
+
* "force-dynamic"`;
|
|
1401
|
+
* * **refuse** — which is not decided here. This function reports what it
|
|
1402
|
+
* found and the caller, which knows whether the project allows a server,
|
|
1403
|
+
* is the one that turns "there is a route here a static host cannot
|
|
1404
|
+
* answer" into an error.
|
|
1405
|
+
*
|
|
1406
|
+
* `dynamic` is the spelling ubugeeei-prod/uf#336 asked for: a route with *no*
|
|
1407
|
+
* parameters whose content depends on the request had no way to say so, and
|
|
1408
|
+
* `generateStaticParams` cannot say it — there are no parameters to generate.
|
|
1409
|
+
* It is Next.js's name for the same declaration, because a person arriving
|
|
1410
|
+
* from `app/` should not have to learn a second word for a decision they have
|
|
1411
|
+
* already made once.
|
|
1412
|
+
*
|
|
1413
|
+
* Two of Next's four values are missing and are not silently accepted:
|
|
1414
|
+
* `"force-static"` and `"error"` are refused by name, because each is a
|
|
1415
|
+
* *constraint* on a page that uf does not yet check, and accepting one would
|
|
1416
|
+
* be reading a declaration and ignoring it — the failure the two issues behind
|
|
1417
|
+
* this function are about.
|
|
1418
|
+
*
|
|
1419
|
+
* Handlers and middleware are in the same list, and they belong there: this is
|
|
1420
|
+
* the list of things that need a process, and a `_uf.route.js` needs one more
|
|
1421
|
+
* obviously than any page does. They carry no per-route render — the build has
|
|
1422
|
+
* never written a file for either — so they appear only when the answer might
|
|
1423
|
+
* be a refusal.
|
|
1424
|
+
*
|
|
1425
|
+
* @param {{routes: Route[], handlers: Handler[], middleware: Middleware[]}} server
|
|
1426
|
+
* @param {"everything" | "possible" | "nothing"} prerender
|
|
1209
1427
|
*/
|
|
1210
|
-
async function
|
|
1428
|
+
async function renderingPlan(server, prerender) {
|
|
1211
1429
|
const urls = [];
|
|
1212
|
-
|
|
1430
|
+
const perRequest = [];
|
|
1431
|
+
|
|
1432
|
+
// Nothing is prerendered and nothing is refused, so no page module is
|
|
1433
|
+
// loaded: a project that renders everything per request should not pay for
|
|
1434
|
+
// a `generateStaticParams` this build will not call.
|
|
1435
|
+
if (prerender === "nothing") {
|
|
1436
|
+
return {
|
|
1437
|
+
urls,
|
|
1438
|
+
perRequest: server.routes.map((route) => ({
|
|
1439
|
+
path: route.path,
|
|
1440
|
+
why: "this build prerenders nothing",
|
|
1441
|
+
})),
|
|
1442
|
+
};
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
for (const route of server.routes) {
|
|
1446
|
+
// Every page module, and not only the parameterised ones: `dynamic` is a
|
|
1447
|
+
// declaration any page can make. A module that cannot be imported at all
|
|
1448
|
+
// is a failure of *that route*, so a route with no parameters goes into
|
|
1449
|
+
// the prerender anyway and the loop below reports it the way it has always
|
|
1450
|
+
// reported a page that throws — named, with the rest of the build still
|
|
1451
|
+
// happening. A parameterised one still rejects out of the build, which is
|
|
1452
|
+
// what it did before there was anything else to load a page module for.
|
|
1453
|
+
let module;
|
|
1454
|
+
try {
|
|
1455
|
+
module = await route.page();
|
|
1456
|
+
} catch (error) {
|
|
1457
|
+
if (route.params.length > 0) throw error;
|
|
1458
|
+
urls.push(route.path);
|
|
1459
|
+
continue;
|
|
1460
|
+
}
|
|
1461
|
+
const declared = module.dynamic ?? "auto";
|
|
1462
|
+
if (declared !== "auto" && declared !== "force-dynamic") {
|
|
1463
|
+
throw new Error(
|
|
1464
|
+
`uf: ${route.file} exports \`dynamic = ${JSON.stringify(declared)}\`, and uf reads ` +
|
|
1465
|
+
'`"auto"` and `"force-dynamic"`. `"force-static"` and `"error"` are Next.js values ' +
|
|
1466
|
+
"for constraints uf does not check yet, and accepting one would be reading a " +
|
|
1467
|
+
"declaration and ignoring it.",
|
|
1468
|
+
);
|
|
1469
|
+
}
|
|
1470
|
+
if (declared === "force-dynamic") {
|
|
1471
|
+
perRequest.push({
|
|
1472
|
+
path: route.path,
|
|
1473
|
+
why: 'its page exports `dynamic = "force-dynamic"`',
|
|
1474
|
+
});
|
|
1475
|
+
continue;
|
|
1476
|
+
}
|
|
1213
1477
|
if (route.params.length === 0) {
|
|
1214
1478
|
urls.push(route.path);
|
|
1215
1479
|
continue;
|
|
1216
1480
|
}
|
|
1217
|
-
const module = await route.page();
|
|
1218
1481
|
const generate = module.generateStaticParams;
|
|
1219
|
-
if (typeof generate !== "function")
|
|
1482
|
+
if (typeof generate !== "function") {
|
|
1483
|
+
perRequest.push({
|
|
1484
|
+
path: route.path,
|
|
1485
|
+
why: "it has parameters and its page exports no `generateStaticParams`",
|
|
1486
|
+
});
|
|
1487
|
+
continue;
|
|
1488
|
+
}
|
|
1220
1489
|
for (const params of await generate()) {
|
|
1221
1490
|
urls.push(fillParams(route.path, params));
|
|
1222
1491
|
}
|
|
1223
1492
|
}
|
|
1224
|
-
|
|
1493
|
+
|
|
1494
|
+
for (const handler of server.handlers ?? []) {
|
|
1495
|
+
perRequest.push({
|
|
1496
|
+
path: handler.path,
|
|
1497
|
+
why: "it is a route handler, and a handler answers a request rather than producing a file",
|
|
1498
|
+
});
|
|
1499
|
+
}
|
|
1500
|
+
for (const entry of server.middleware ?? []) {
|
|
1501
|
+
// A middleware is reported by the path it guards rather than by the route
|
|
1502
|
+
// it guards, which is why it cannot be folded into the loop above: it runs
|
|
1503
|
+
// for a page, for a handler, and for a path under it that is neither, so
|
|
1504
|
+
// "which route is this" has no single answer.
|
|
1505
|
+
perRequest.push({
|
|
1506
|
+
path: `${entry.path === "/" ? "" : entry.path}/*`,
|
|
1507
|
+
why: "a middleware guards it, and a middleware runs once per request",
|
|
1508
|
+
});
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
return { urls, perRequest };
|
|
1225
1512
|
}
|
|
1226
1513
|
|
|
1227
1514
|
function fillParams(routePath, params) {
|