@uniflowed/vite 0.0.0-alpha.6 → 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 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` and `uf preview` spawn.
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 preview --root <dir> [--host <h>] [--port <n>]
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
- envFile: false,
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
- preview: { host, port },
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
- * A Node request as a `Request`.
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
- * The handler contract is the platform's, so the adapter belongs here rather
217
- * than in every handler. The body is read as a stream where the host supports
218
- * it, because a handler that accepts an upload should not need the whole thing
219
- * buffered before it starts.
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 server = await startPreview(await viteConfig(config, "production"));
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", { local: urls.local, network: urls.network, routes: [] });
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
- const result = await server.render(url, assets);
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
- if (server.notFound != null) {
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
- async function printConfig() {
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
- emit("config", { config: projectConfig(config) });
358
- process.exit(0);
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
- function readManifest(outDir) {
362
- const file = path.join(outDir, ".vite", "manifest.json");
363
- if (!existsSync(file)) throw new Error(`uf: the client build wrote no manifest at ${file}`);
364
- return JSON.parse(readFileSync(file, "utf8"));
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
- * Script, stylesheet and preload URLs for the client entry chunk.
578
+ * The source of the module a runtime gets wrapped around.
369
579
  *
370
- * The entry is found by its `isEntry` flag rather than by key, because a
371
- * virtual module's manifest key is an implementation detail of the bundler.
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
- * The tags a prerendered document needs.
604
+ * Refuse a native addon by name instead of by stack trace.
375
605
  *
376
- * Two walks over the manifest, because the two answers are different. A
377
- * `modulepreload` is worth emitting only for a chunk this document will
378
- * certainly load, which is the entry's *static* imports. A stylesheet has to
379
- * be emitted for anything the page might render, and the router loads every
380
- * route module dynamicallyso a stylesheet imported by a layout is reached
381
- * through `dynamicImports` and through nothing else. Following only the static
382
- * graph, as this did, meant a layout could import a stylesheet and the built
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
- * The cost is that a project with per-route stylesheets links all of them on
386
- * every page. Narrowing that needs the route table to say which chunk each
387
- * route came from, which the manifest alone cannot tell us.
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 assetsFromManifest(manifest) {
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
- scripts: [`/${entry.file}`],
420
- styles: [...styles].map((file) => `/${file}`),
421
- preloads: [...preloads].map((file) => `/${file}`),
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.