@uniflowed/vite 0.0.0-alpha.5 → 0.0.0-alpha.7
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 +349 -118
- package/index.js +177 -8
- package/internal/events.js +20 -0
- package/internal/routes.js +112 -22
- package/internal/serve.js +386 -0
- package/package.json +2 -2
package/driver.js
CHANGED
|
@@ -2,11 +2,14 @@
|
|
|
2
2
|
//
|
|
3
3
|
// Plain JavaScript: the host runs this file directly.
|
|
4
4
|
//
|
|
5
|
-
// The driver `uf dev`, `uf build`
|
|
5
|
+
// The driver `uf dev`, `uf build`, `uf build --compile`, `uf preview` and
|
|
6
|
+
// `uf start` spawn.
|
|
6
7
|
//
|
|
7
8
|
// <host> driver.js dev --root <dir> [--host <h>] [--port <n>] [--strict-port]
|
|
8
9
|
// <host> driver.js build --root <dir> [--out-dir <dir>] [--mode <m>]
|
|
9
|
-
// <host> driver.js
|
|
10
|
+
// <host> driver.js compile --root <dir> [--out-dir <dir>] --assets <file> --bundle <dir>
|
|
11
|
+
// <host> driver.js preview --root <dir> [--out-dir <dir>] [--host <h>] [--port <n>]
|
|
12
|
+
// <host> driver.js start --root <dir> [--out-dir <dir>] [--host <h>] [--port <n>]
|
|
10
13
|
// <host> driver.js config --root <dir>
|
|
11
14
|
//
|
|
12
15
|
// `uf` in Rust owns the terminal; this process owns Vite. They talk over
|
|
@@ -18,15 +21,24 @@
|
|
|
18
21
|
// Rust side reads a config that may hold functions and plugin instances: the
|
|
19
22
|
// one host that can evaluate the file evaluates it.
|
|
20
23
|
|
|
24
|
+
import { createServer as createHttpServer } from "node:http";
|
|
21
25
|
import { register } from "node:module";
|
|
22
26
|
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
23
27
|
import path from "node:path";
|
|
24
28
|
import { pathToFileURL } from "node:url";
|
|
25
29
|
|
|
26
|
-
import { emit, errorEvent, eventLogger } from "./internal/events.js";
|
|
30
|
+
import { emit, errorEvent, eventLogger, reportRenderError } from "./internal/events.js";
|
|
27
31
|
import { loadUfConfig, projectConfig } from "./internal/config.js";
|
|
28
32
|
import { withProjectConfig } from "./merge.js";
|
|
29
33
|
import { VIRTUAL, scanRoutes } from "./internal/routes.js";
|
|
34
|
+
import {
|
|
35
|
+
assetsFromManifest,
|
|
36
|
+
createServeHandler,
|
|
37
|
+
loadBuild,
|
|
38
|
+
nodeListener,
|
|
39
|
+
send,
|
|
40
|
+
toRequest,
|
|
41
|
+
} from "./internal/serve.js";
|
|
30
42
|
|
|
31
43
|
function argument(name) {
|
|
32
44
|
const at = process.argv.indexOf(name);
|
|
@@ -59,7 +71,7 @@ process.stdin.on("end", () => process.exit(0));
|
|
|
59
71
|
process.stdin.on("error", () => process.exit(0));
|
|
60
72
|
process.stdin.resume();
|
|
61
73
|
|
|
62
|
-
const commands = { dev, build, preview, config: printConfig };
|
|
74
|
+
const commands = { dev, build, compile, preview, start, config: printConfig };
|
|
63
75
|
const run = commands[command];
|
|
64
76
|
if (run == null) {
|
|
65
77
|
emit("error", { message: `unknown driver command ${JSON.stringify(command)}` });
|
|
@@ -93,10 +105,17 @@ async function viteConfig(config, mode) {
|
|
|
93
105
|
// plugins make Flow compile, and the few settings uf enforces rather than
|
|
94
106
|
// merely passes on — `allowedHosts` gates binding a routable address, and
|
|
95
107
|
// `manifest` is how the prerender finds its assets.
|
|
108
|
+
//
|
|
109
|
+
// `envDir: false` rather than `envFile: false`: Vite 8 deprecated the second
|
|
110
|
+
// spelling and prints a line saying so on every dev server and every build,
|
|
111
|
+
// twice in the docs site's. A project turns the loader back on with
|
|
112
|
+
// `vite: { envDir: "." }`, which is the default directory — the project's own
|
|
113
|
+
// configuration is merged over this one, so it wins. See #259 for why uf
|
|
114
|
+
// switches it off at all.
|
|
96
115
|
const generated = {
|
|
97
116
|
root,
|
|
98
117
|
configFile: false,
|
|
99
|
-
|
|
118
|
+
envDir: false,
|
|
100
119
|
mode,
|
|
101
120
|
clearScreen: false,
|
|
102
121
|
customLogger: eventLogger(argument("--log-level") ?? "info"),
|
|
@@ -111,7 +130,17 @@ async function viteConfig(config, mode) {
|
|
|
111
130
|
deny: dev.fs?.deny,
|
|
112
131
|
},
|
|
113
132
|
},
|
|
114
|
-
|
|
133
|
+
// Not `server`, and not `dev.port` either. Vite's own default for a
|
|
134
|
+
// preview is 4173 rather than 5173, and the reason is the case this
|
|
135
|
+
// command exists for: somebody comparing a build against the dev server
|
|
136
|
+
// they left running. Taking `dev.port` would have made the two collide,
|
|
137
|
+
// and Vite would have moved the preview to the next free port and served
|
|
138
|
+
// it somewhere nobody was looking.
|
|
139
|
+
preview: {
|
|
140
|
+
host: argument("--host") ?? "127.0.0.1",
|
|
141
|
+
port: Number(argument("--port") ?? 4173),
|
|
142
|
+
strictPort: flag("--strict-port"),
|
|
143
|
+
},
|
|
115
144
|
build: {
|
|
116
145
|
outDir: argument("--out-dir") ?? build.outDir ?? "dist",
|
|
117
146
|
sourcemap: build.sourcemap ?? true,
|
|
@@ -181,6 +210,7 @@ async function dev() {
|
|
|
181
210
|
}
|
|
182
211
|
|
|
183
212
|
const result = await entry.render(url, assets);
|
|
213
|
+
if (result.error != null) reportRenderError(server, url, result.error);
|
|
184
214
|
const html = await server.transformIndexHtml(url, result.html);
|
|
185
215
|
response.statusCode = result.status ?? 200;
|
|
186
216
|
response.setHeader("content-type", "text/html; charset=utf-8");
|
|
@@ -211,67 +241,53 @@ async function dev() {
|
|
|
211
241
|
}
|
|
212
242
|
|
|
213
243
|
/**
|
|
214
|
-
*
|
|
244
|
+
* The preview server: the build, as Vite serves it.
|
|
245
|
+
*
|
|
246
|
+
* Vite's `preview()` is a static file server, and a uf build is not only
|
|
247
|
+
* static files — a route handler answers a `POST` and a route with parameters
|
|
248
|
+
* and no `generateStaticParams` was never prerendered. On its own it would
|
|
249
|
+
* therefore 404 every request the interesting half of an application exists to
|
|
250
|
+
* answer, which is worse than having no preview at all, because a preview is
|
|
251
|
+
* checked and believed.
|
|
215
252
|
*
|
|
216
|
-
*
|
|
217
|
-
*
|
|
218
|
-
*
|
|
219
|
-
*
|
|
253
|
+
* So the application handler is mounted behind it, and `appType: "custom"` is
|
|
254
|
+
* what makes that reachable: with Vite's default `spa` it inserts an
|
|
255
|
+
* index.html fallback and a 404 middleware of its own, so every unmatched path
|
|
256
|
+
* would have been answered with the home page — a 200 for a path that does not
|
|
257
|
+
* exist — before anything of uf's ran.
|
|
258
|
+
*
|
|
259
|
+
* The static middleware still runs first, and that is deliberate rather than
|
|
260
|
+
* incidental; see `internal/serve.js` for why `uf start` orders itself the
|
|
261
|
+
* same way.
|
|
220
262
|
*/
|
|
221
|
-
async function toRequest(incoming, config) {
|
|
222
|
-
const host = incoming.headers.host ?? "localhost";
|
|
223
|
-
const protocol = config?.server?.https == null ? "http" : "https";
|
|
224
|
-
const url = new URL(incoming.originalUrl ?? incoming.url ?? "/", `${protocol}://${host}`);
|
|
225
|
-
|
|
226
|
-
const headers = new Headers();
|
|
227
|
-
for (const [name, value] of Object.entries(incoming.headers)) {
|
|
228
|
-
if (value == null) continue;
|
|
229
|
-
for (const entry of Array.isArray(value) ? value : [value]) {
|
|
230
|
-
headers.append(name, entry);
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
const method = (incoming.method ?? "GET").toUpperCase();
|
|
235
|
-
const init = { method, headers };
|
|
236
|
-
if (method !== "GET" && method !== "HEAD") {
|
|
237
|
-
// `duplex` is required by the specification whenever a body is a stream,
|
|
238
|
-
// and Node throws without it.
|
|
239
|
-
init.body = incoming;
|
|
240
|
-
init.duplex = "half";
|
|
241
|
-
}
|
|
242
|
-
return new Request(url, init);
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
/** Write a `Response` to a Node response. */
|
|
246
|
-
async function send(outgoing, result) {
|
|
247
|
-
outgoing.statusCode = result.status;
|
|
248
|
-
if (result.statusText !== "") {
|
|
249
|
-
outgoing.statusMessage = result.statusText;
|
|
250
|
-
}
|
|
251
|
-
for (const [name, value] of result.headers) {
|
|
252
|
-
outgoing.setHeader(name, value);
|
|
253
|
-
}
|
|
254
|
-
if (result.body == null) {
|
|
255
|
-
outgoing.end();
|
|
256
|
-
return;
|
|
257
|
-
}
|
|
258
|
-
// Streamed rather than buffered, so a handler returning a large or
|
|
259
|
-
// open-ended body is not read into memory first.
|
|
260
|
-
const reader = result.body.getReader();
|
|
261
|
-
while (true) {
|
|
262
|
-
const { done, value } = await reader.read();
|
|
263
|
-
if (done) break;
|
|
264
|
-
outgoing.write(value);
|
|
265
|
-
}
|
|
266
|
-
outgoing.end();
|
|
267
|
-
}
|
|
268
|
-
|
|
269
263
|
async function preview() {
|
|
270
264
|
const { preview: startPreview } = await import("vite");
|
|
271
265
|
const config = await loadConfig();
|
|
272
|
-
const
|
|
266
|
+
const inline = await viteConfig(config, "production");
|
|
267
|
+
const build = await loadBuild({
|
|
268
|
+
root,
|
|
269
|
+
outDir: inline.build.outDir,
|
|
270
|
+
serverDir: path.join(".uf", "build", "server"),
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
const server = await startPreview({ ...inline, appType: "custom" });
|
|
274
|
+
const handle = createServeHandler(build);
|
|
275
|
+
server.middlewares.use(async (request, response, next) => {
|
|
276
|
+
try {
|
|
277
|
+
await send(response, await handle(await toRequest(request, server.config)));
|
|
278
|
+
} catch (error) {
|
|
279
|
+
next(error);
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
|
|
273
283
|
const urls = server.resolvedUrls ?? { local: [], network: [] };
|
|
274
|
-
emit("listening", {
|
|
284
|
+
emit("listening", {
|
|
285
|
+
local: urls.local,
|
|
286
|
+
network: urls.network,
|
|
287
|
+
routes: build.entry.routes.map((route) => route.path),
|
|
288
|
+
handlers: build.entry.handlers.map((handler) => handler.path),
|
|
289
|
+
});
|
|
290
|
+
|
|
275
291
|
const shutdown = async () => {
|
|
276
292
|
await server.close();
|
|
277
293
|
process.exit(0);
|
|
@@ -280,6 +296,65 @@ async function preview() {
|
|
|
280
296
|
process.on("SIGTERM", shutdown);
|
|
281
297
|
}
|
|
282
298
|
|
|
299
|
+
/**
|
|
300
|
+
* The production server: the build, with no bundler in the process.
|
|
301
|
+
*
|
|
302
|
+
* `preview` proves the build works through Vite. This is the thing that is
|
|
303
|
+
* actually deployed, and it imports `vite` nowhere — a host running a built
|
|
304
|
+
* application should not need the bundler that produced it, and the moment it
|
|
305
|
+
* does, "portable output" is a claim rather than a property.
|
|
306
|
+
*
|
|
307
|
+
* There is no `--strict-port` here and there is nothing to add: this server
|
|
308
|
+
* binds the port it was given or fails, where Vite's would have quietly moved
|
|
309
|
+
* to the next free one. `PORT` and `HOST` are read from the environment
|
|
310
|
+
* because that is how every process manager and container platform says which
|
|
311
|
+
* socket to take, and a production server that could only be told on the
|
|
312
|
+
* command line would need a wrapper script everywhere it ran.
|
|
313
|
+
*
|
|
314
|
+
* It is not the only thing that can be deployed. `uf build --compile` puts
|
|
315
|
+
* this same application behind this same resolution order inside a single
|
|
316
|
+
* executable, for a host that should not have to have a JavaScript runtime
|
|
317
|
+
* installed at all; see [`compile`] for what that costs and what it shares.
|
|
318
|
+
*/
|
|
319
|
+
async function start() {
|
|
320
|
+
const config = await loadConfig();
|
|
321
|
+
const outDir = argument("--out-dir") ?? config.build?.outDir ?? "dist";
|
|
322
|
+
const build = await loadBuild({
|
|
323
|
+
root,
|
|
324
|
+
outDir,
|
|
325
|
+
serverDir: path.join(".uf", "build", "server"),
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
const host = argument("--host") ?? process.env.HOST ?? "0.0.0.0";
|
|
329
|
+
const port = Number(argument("--port") ?? process.env.PORT ?? 3000);
|
|
330
|
+
const server = createHttpServer(nodeListener(createServeHandler(build)));
|
|
331
|
+
|
|
332
|
+
await new Promise((resolve, reject) => {
|
|
333
|
+
server.once("error", reject);
|
|
334
|
+
server.listen(port, host, resolve);
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
const bound = server.address();
|
|
338
|
+
// `0.0.0.0` is not a URL anybody can open, so the loopback spelling is what
|
|
339
|
+
// is printed as `local` and the bound address is reported as the network
|
|
340
|
+
// one — the same split `uf dev` prints, and for the same reason: one of the
|
|
341
|
+
// two is a link and the other is a fact about the socket.
|
|
342
|
+
const shown = `${bound.address}:${bound.port}`;
|
|
343
|
+
const wildcard = bound.address === "0.0.0.0" || bound.address === "::";
|
|
344
|
+
emit("listening", {
|
|
345
|
+
local: [`http://${wildcard ? `localhost:${bound.port}` : shown}/`],
|
|
346
|
+
network: wildcard ? [`http://${shown}/`] : [],
|
|
347
|
+
routes: build.entry.routes.map((route) => route.path),
|
|
348
|
+
handlers: build.entry.handlers.map((handler) => handler.path),
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
const shutdown = () => {
|
|
352
|
+
server.close(() => process.exit(0));
|
|
353
|
+
};
|
|
354
|
+
process.on("SIGINT", shutdown);
|
|
355
|
+
process.on("SIGTERM", shutdown);
|
|
356
|
+
}
|
|
357
|
+
|
|
283
358
|
async function build() {
|
|
284
359
|
const vite = await import("vite");
|
|
285
360
|
const config = await loadConfig();
|
|
@@ -324,8 +399,34 @@ async function build() {
|
|
|
324
399
|
const server = await import(pathToFileURL(path.join(serverDir, "server.js")).href);
|
|
325
400
|
const assets = assetsFromManifest(manifest);
|
|
326
401
|
const pages = await staticPaths(server.routes);
|
|
402
|
+
|
|
403
|
+
// A route that throws fails *that route*, and the rest of the build still
|
|
404
|
+
// happens. This loop had no `try`: the first page to throw rejected out of
|
|
405
|
+
// `build()`, `run().catch` reported the exception, and which URL was being
|
|
406
|
+
// rendered was a local variable nobody could see. One broken page was the
|
|
407
|
+
// whole build, and the message named a stack rather than a route.
|
|
408
|
+
//
|
|
409
|
+
// The render itself no longer throws for an ordinary component failure —
|
|
410
|
+
// `createRenderer` renders the error boundary and reports the exception on
|
|
411
|
+
// the result — so both are checked here. Neither writes a file: an error
|
|
412
|
+
// page written into `dist/` is a build that shipped its own failure.
|
|
413
|
+
const failures = [];
|
|
414
|
+
const failed = (url, error) => {
|
|
415
|
+
failures.push(url);
|
|
416
|
+
emit("page-failed", { url, ...errorEvent(error) });
|
|
417
|
+
};
|
|
327
418
|
for (const url of pages) {
|
|
328
|
-
|
|
419
|
+
let result;
|
|
420
|
+
try {
|
|
421
|
+
result = await server.render(url, assets);
|
|
422
|
+
} catch (error) {
|
|
423
|
+
failed(url, error);
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
if (result.error != null) {
|
|
427
|
+
failed(url, result.error);
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
329
430
|
const file = htmlPathFor(outDir, url);
|
|
330
431
|
mkdirSync(path.dirname(file), { recursive: true });
|
|
331
432
|
writeFileSync(file, result.html);
|
|
@@ -336,7 +437,17 @@ async function build() {
|
|
|
336
437
|
bytes: Buffer.byteLength(result.html),
|
|
337
438
|
});
|
|
338
439
|
}
|
|
339
|
-
|
|
440
|
+
// One `404.html`, from the boundary at the router root: a static host serves
|
|
441
|
+
// a single error document for the whole site, so the nested boundaries a
|
|
442
|
+
// project declares are the server's and the client's to render, not
|
|
443
|
+
// something this loop can write a file for.
|
|
444
|
+
//
|
|
445
|
+
// The condition is "there is a root boundary", not "there is any boundary",
|
|
446
|
+
// because `/__uf_not_found__` is a path at the root: a project whose only
|
|
447
|
+
// `_uf.not-found.js` is in `app/guide/` would otherwise get a `404.html`
|
|
448
|
+
// rendered from the framework's bare default, which is worse than the file
|
|
449
|
+
// it used to write, which was none.
|
|
450
|
+
if (server.notFound.some((boundary) => boundary.path === "/")) {
|
|
340
451
|
const result = await server.render("/__uf_not_found__", assets);
|
|
341
452
|
const file = path.join(outDir, "404.html");
|
|
342
453
|
writeFileSync(file, result.html);
|
|
@@ -348,80 +459,200 @@ async function build() {
|
|
|
348
459
|
});
|
|
349
460
|
}
|
|
350
461
|
|
|
462
|
+
if (failures.length > 0) {
|
|
463
|
+
// Emitted rather than thrown, so the message is the routes and not the
|
|
464
|
+
// last exception: each one has already been reported with its own frame.
|
|
465
|
+
//
|
|
466
|
+
// The first line stands on its own, because it is the one `uf build` uses
|
|
467
|
+
// as the headline and the one a CI log's last line will be. It read
|
|
468
|
+
// `... failed:` with the routes below it, and the headline was then a
|
|
469
|
+
// sentence ending in a colon and nothing.
|
|
470
|
+
emit("error", {
|
|
471
|
+
message: `${failures.length} of ${pages.length} prerendered ${plural(
|
|
472
|
+
pages.length,
|
|
473
|
+
"route",
|
|
474
|
+
)} failed\n${failures.map((url) => ` ${url}`).join("\n")}`,
|
|
475
|
+
});
|
|
476
|
+
process.exit(1);
|
|
477
|
+
}
|
|
478
|
+
|
|
351
479
|
emit("done", { outDir: path.relative(root, outDir), pages: pages.length });
|
|
352
480
|
process.exit(0);
|
|
353
481
|
}
|
|
354
482
|
|
|
355
|
-
|
|
483
|
+
/**
|
|
484
|
+
* Link the whole application into one JavaScript file, for `uf build --compile`.
|
|
485
|
+
*
|
|
486
|
+
* This runs after `build`, on a `dist/` that is already complete, and produces
|
|
487
|
+
* the module a runtime is wrapped around. It differs from the server build in
|
|
488
|
+
* `build()` in exactly three ways, and each of them is what "one file" means:
|
|
489
|
+
*
|
|
490
|
+
* * `ssr.noExternal: true` — the server build leaves `react`, `react-dom`
|
|
491
|
+
* and every other dependency as bare imports, because the host it runs on
|
|
492
|
+
* has `node_modules` beside it. A binary does not, so they come in.
|
|
493
|
+
* * `codeSplitting: false` — a route is a lazy `import()` so that the browser
|
|
494
|
+
* can fetch one chunk per page. On the server that split buys nothing and
|
|
495
|
+
* costs everything: chunks are separate files, and separate files are the
|
|
496
|
+
* one thing this output may not have.
|
|
497
|
+
* * the native-addon guard below, which turns "cannot resolve" into a
|
|
498
|
+
* sentence naming the package that cannot be compiled.
|
|
499
|
+
*
|
|
500
|
+
* The embedded copy of `dist/` is *not* built here. `uf` writes it (see
|
|
501
|
+
* `uf_bundle::embed`) and passes its path in `--assets`, because walking an
|
|
502
|
+
* output directory and encoding every file in it is bulk work over the whole
|
|
503
|
+
* build, which belongs in Rust rather than in the host process.
|
|
504
|
+
*
|
|
505
|
+
* # Three front doors onto one build, and why they are not one function
|
|
506
|
+
*
|
|
507
|
+
* `preview` and `start` above serve `dist/` from disk, and they share a single
|
|
508
|
+
* handler in `./internal/serve.js` for the express purpose of being unable to
|
|
509
|
+
* answer differently. What is linked here is a third front door onto the same
|
|
510
|
+
* build, and it deliberately does *not* import that module. Two reasons, and
|
|
511
|
+
* either would be enough: `internal/serve.js` answers by opening files under
|
|
512
|
+
* `dist/`, and a compiled binary has no `dist/` to open — it carries the bytes
|
|
513
|
+
* — so the half that reads a request would arrive with a half that cannot run;
|
|
514
|
+
* and it lives in `@uniflowed/vite`, so linking it would put the package named
|
|
515
|
+
* after the bundler inside the artefact a deployment runs, which is the one
|
|
516
|
+
* thing `start` exists to avoid.
|
|
517
|
+
*
|
|
518
|
+
* What a binary uses instead is `@uniflowed/server/standalone`, and the thing
|
|
519
|
+
* that is shared between the three is not code but the *answer*: an asset or a
|
|
520
|
+
* prerendered document first, then a route handler, then a render for whatever
|
|
521
|
+
* is left. That order is not a preference. `preview` cannot deviate from it —
|
|
522
|
+
* Vite's preview server runs its own file middleware before anything uf mounts
|
|
523
|
+
* behind it — so `start` matches Vite, and the binary matches `start`. A
|
|
524
|
+
* compiled application that resolved a collision the other way would be the
|
|
525
|
+
* trap `preview` exists to prevent, one deployment further along, and the only
|
|
526
|
+
* copy nobody can check with `uf preview` first.
|
|
527
|
+
*/
|
|
528
|
+
async function compile() {
|
|
529
|
+
const vite = await import("vite");
|
|
356
530
|
const config = await loadConfig();
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
531
|
+
const inline = await viteConfig(config, argument("--mode") ?? "production");
|
|
532
|
+
const outDir = path.resolve(root, inline.build.outDir);
|
|
533
|
+
const assetsArgument = argument("--assets");
|
|
534
|
+
const bundleArgument = argument("--bundle");
|
|
535
|
+
if (assetsArgument == null || bundleArgument == null) {
|
|
536
|
+
throw new Error("uf: `driver.js compile` needs both --assets and --bundle");
|
|
537
|
+
}
|
|
538
|
+
const assets = path.resolve(root, assetsArgument);
|
|
539
|
+
const bundleDir = path.resolve(root, bundleArgument);
|
|
360
540
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
541
|
+
emit("phase", { name: "standalone" });
|
|
542
|
+
|
|
543
|
+
// The entry is written to disk rather than served as another virtual module:
|
|
544
|
+
// it is generated per build (it names this build's asset file), and a real
|
|
545
|
+
// file is the version a person can open when a compiled binary misbehaves.
|
|
546
|
+
const entry = path.join(bundleDir, "entry.js");
|
|
547
|
+
mkdirSync(bundleDir, { recursive: true });
|
|
548
|
+
const specifier = `./${path.relative(bundleDir, assets)}`;
|
|
549
|
+
writeFileSync(entry, entrySource(specifier, assetsFromManifest(readManifest(outDir))));
|
|
550
|
+
|
|
551
|
+
await vite.build({
|
|
552
|
+
...inline,
|
|
553
|
+
customLogger: eventLogger("warn"),
|
|
554
|
+
plugins: [...inline.plugins, nativeAddonGuard()],
|
|
555
|
+
ssr: { ...(inline.ssr ?? {}), noExternal: true },
|
|
556
|
+
build: {
|
|
557
|
+
...inline.build,
|
|
558
|
+
manifest: false,
|
|
559
|
+
// The map would describe this intermediate bundle rather than the
|
|
560
|
+
// binary, and nothing downstream reads it. Turning it off is a smaller
|
|
561
|
+
// `.uf/` and one less file to explain.
|
|
562
|
+
sourcemap: false,
|
|
563
|
+
ssr: true,
|
|
564
|
+
outDir: bundleDir,
|
|
565
|
+
emptyOutDir: false,
|
|
566
|
+
rollupOptions: {
|
|
567
|
+
input: { server: entry },
|
|
568
|
+
output: { entryFileNames: "server.js", format: "es", codeSplitting: false },
|
|
569
|
+
},
|
|
570
|
+
},
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
emit("done", { outDir: path.relative(root, bundleDir), pages: 0 });
|
|
574
|
+
process.exit(0);
|
|
365
575
|
}
|
|
366
576
|
|
|
367
577
|
/**
|
|
368
|
-
*
|
|
578
|
+
* The source of the module a runtime gets wrapped around.
|
|
369
579
|
*
|
|
370
|
-
*
|
|
371
|
-
*
|
|
580
|
+
* Three imports and one call: the shim that serves, the application, and the
|
|
581
|
+
* bytes of `dist/`. The document's script and stylesheet URLs are baked in
|
|
582
|
+
* here because they come from the client manifest, which exists at this moment
|
|
583
|
+
* and not inside the binary.
|
|
372
584
|
*/
|
|
585
|
+
function entrySource(assetsSpecifier, document) {
|
|
586
|
+
// Not `await serve(...)` at the top level. Node runs top-level `await`
|
|
587
|
+
// happily and the Flow parser uf vendors does not parse it (ubugeeei-prod/uf#204),
|
|
588
|
+
// so the generated entry would fail its own transform. `.catch` is the better
|
|
589
|
+
// spelling anyway: a binary that cannot take its port should say which port
|
|
590
|
+
// and exit non-zero, rather than die as an unhandled rejection.
|
|
591
|
+
return `// Generated by \`uf build --compile\`. Not checked in, not edited.
|
|
592
|
+
import { serve } from "@uniflowed/server/standalone";
|
|
593
|
+
import { assets } from ${JSON.stringify(assetsSpecifier)};
|
|
594
|
+
import * as app from ${JSON.stringify(VIRTUAL.server)};
|
|
595
|
+
|
|
596
|
+
serve({ app, assets, document: ${JSON.stringify(document)} }).catch((error) => {
|
|
597
|
+
process.stderr.write(\`uf: \${error?.message ?? String(error)}\n\`);
|
|
598
|
+
process.exit(1);
|
|
599
|
+
});
|
|
600
|
+
`;
|
|
601
|
+
}
|
|
602
|
+
|
|
373
603
|
/**
|
|
374
|
-
*
|
|
604
|
+
* Refuse a native addon by name instead of by stack trace.
|
|
375
605
|
*
|
|
376
|
-
*
|
|
377
|
-
*
|
|
378
|
-
*
|
|
379
|
-
*
|
|
380
|
-
*
|
|
381
|
-
*
|
|
382
|
-
*
|
|
383
|
-
* HTML would silently ship without it.
|
|
606
|
+
* A `.node` file is a compiled shared object for one platform: it cannot be
|
|
607
|
+
* inlined into a JavaScript bundle, and a binary that carried one would stop
|
|
608
|
+
* being a single file. Without this, `ssr.noExternal: true` hands the addon to
|
|
609
|
+
* Rolldown and the build fails somewhere inside the bundler with a message
|
|
610
|
+
* about an unexpected character — which is true, and useless. Failing here
|
|
611
|
+
* with the addon's path and the importer that reached it is the difference
|
|
612
|
+
* between a feature and a trap.
|
|
384
613
|
*
|
|
385
|
-
*
|
|
386
|
-
*
|
|
387
|
-
*
|
|
614
|
+
* It catches what can be caught: a static `import` or `require` that resolves
|
|
615
|
+
* to a `.node` file. An addon loaded through a runtime string — `process.dlopen`,
|
|
616
|
+
* or `require(variable)` — is not visible to any bundler, so such a project
|
|
617
|
+
* still compiles and still fails on the first request that reaches the addon.
|
|
618
|
+
* That limit is real, it is not fixable from inside a bundler, and it is
|
|
619
|
+
* written down in the CLI reference rather than papered over.
|
|
388
620
|
*/
|
|
389
|
-
function
|
|
390
|
-
const entry = Object.values(manifest).find((chunk) => chunk.isEntry);
|
|
391
|
-
if (entry == null) throw new Error("uf: the client manifest has no entry chunk");
|
|
392
|
-
|
|
393
|
-
const styles = new Set(entry.css ?? []);
|
|
394
|
-
const seen = new Set();
|
|
395
|
-
const collectStyles = (chunk) => {
|
|
396
|
-
for (const imported of [...(chunk.imports ?? []), ...(chunk.dynamicImports ?? [])]) {
|
|
397
|
-
if (seen.has(imported)) continue;
|
|
398
|
-
seen.add(imported);
|
|
399
|
-
const dependency = manifest[imported];
|
|
400
|
-
if (dependency == null) continue;
|
|
401
|
-
for (const css of dependency.css ?? []) styles.add(css);
|
|
402
|
-
collectStyles(dependency);
|
|
403
|
-
}
|
|
404
|
-
};
|
|
405
|
-
collectStyles(entry);
|
|
406
|
-
|
|
407
|
-
const preloads = new Set();
|
|
408
|
-
const collectPreloads = (chunk) => {
|
|
409
|
-
for (const imported of chunk.imports ?? []) {
|
|
410
|
-
const dependency = manifest[imported];
|
|
411
|
-
if (dependency == null || preloads.has(dependency.file)) continue;
|
|
412
|
-
preloads.add(dependency.file);
|
|
413
|
-
collectPreloads(dependency);
|
|
414
|
-
}
|
|
415
|
-
};
|
|
416
|
-
collectPreloads(entry);
|
|
417
|
-
|
|
621
|
+
function nativeAddonGuard() {
|
|
418
622
|
return {
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
623
|
+
name: "uf:no-native-addons",
|
|
624
|
+
enforce: "pre",
|
|
625
|
+
resolveId(source, importer) {
|
|
626
|
+
if (!source.endsWith(".node")) return null;
|
|
627
|
+
const from = importer == null ? "the application" : path.relative(root, importer);
|
|
628
|
+
throw new Error(
|
|
629
|
+
`${from} loads the native addon ${source}, and \`uf build --compile\` cannot put one ` +
|
|
630
|
+
"inside a single executable: a `.node` file is a shared object built for one " +
|
|
631
|
+
"platform, and embedding it would make the output two files rather than one. " +
|
|
632
|
+
"Build without `--compile` and deploy `dist/` with a runtime, or replace the " +
|
|
633
|
+
"dependency with one that has no native addon.",
|
|
634
|
+
);
|
|
635
|
+
},
|
|
422
636
|
};
|
|
423
637
|
}
|
|
424
638
|
|
|
639
|
+
/** `word`, pluralised for `count`. */
|
|
640
|
+
function plural(count, word) {
|
|
641
|
+
return count === 1 ? word : `${word}s`;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
async function printConfig() {
|
|
645
|
+
const config = await loadConfig();
|
|
646
|
+
emit("config", { config: projectConfig(config) });
|
|
647
|
+
process.exit(0);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function readManifest(outDir) {
|
|
651
|
+
const file = path.join(outDir, ".vite", "manifest.json");
|
|
652
|
+
if (!existsSync(file)) throw new Error(`uf: the client build wrote no manifest at ${file}`);
|
|
653
|
+
return JSON.parse(readFileSync(file, "utf8"));
|
|
654
|
+
}
|
|
655
|
+
|
|
425
656
|
/**
|
|
426
657
|
* The URLs to prerender: every route without parameters, plus every set of
|
|
427
658
|
* parameters a page's `generateStaticParams` returns.
|