@uniflowed/vite 0.0.0-alpha.34 → 0.0.0-alpha.37
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 +537 -48
- package/index.js +327 -24
- package/internal/barrel-imports.js +459 -0
- package/internal/flight.js +736 -0
- package/internal/frontmatter.js +33 -0
- package/internal/http.js +21 -2
- package/internal/module-graph.js +134 -0
- package/internal/routes.js +55 -2
- package/internal/serve.js +206 -14
- package/internal/server-components.js +109 -0
- package/internal/worker-builtins.js +170 -0
- package/package.json +9 -7
package/driver.js
CHANGED
|
@@ -42,26 +42,42 @@
|
|
|
42
42
|
import { randomUUID } from "node:crypto";
|
|
43
43
|
import { createServer as createHttpServer } from "node:http";
|
|
44
44
|
import { builtinModules, register } from "node:module";
|
|
45
|
-
import {
|
|
45
|
+
import { installFlowHooks } from "@uniflowed/host/internal/sync-hooks.js";
|
|
46
|
+
import { cpSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
46
47
|
import path from "node:path";
|
|
47
48
|
import { pathToFileURL } from "node:url";
|
|
48
49
|
|
|
49
50
|
import { COMPILE_ASSETS_ID, compileAssetsPlugin } from "./internal/compile-assets.js";
|
|
51
|
+
import {
|
|
52
|
+
survivingImports,
|
|
53
|
+
unavailableOnWorkers,
|
|
54
|
+
workerBuiltinWarnings,
|
|
55
|
+
} from "./internal/worker-builtins.js";
|
|
50
56
|
import { emit, errorEvent, eventLogger } from "./internal/events.js";
|
|
51
57
|
import { loadUfConfig, projectConfig } from "./internal/config.js";
|
|
52
|
-
import { send, toRequest } from "./internal/http.js";
|
|
58
|
+
import { send, toAddressRequest, toRequest } from "./internal/http.js";
|
|
53
59
|
import { createOpenApiDocument } from "./internal/openapi.js";
|
|
54
60
|
import { withProjectConfig } from "./merge.js";
|
|
55
|
-
import {
|
|
61
|
+
import { FLIGHT_VIRTUAL, RSC_ENVIRONMENT } from "./internal/flight.js";
|
|
62
|
+
import { MODULE_GRAPH_FILE, createModuleGraphCollector } from "./internal/module-graph.js";
|
|
63
|
+
import { VIRTUAL, resolveRouteTarget, routingRulesOf, scanRoutes } from "./internal/routes.js";
|
|
56
64
|
import {
|
|
57
65
|
BUILD_ID_FILE,
|
|
66
|
+
DOCUMENT_ASSETS_FILE,
|
|
67
|
+
REGENERATED_DIRECTORY,
|
|
68
|
+
REGENERATION_FILE,
|
|
69
|
+
answerRouting,
|
|
70
|
+
answersInFrontOfFiles,
|
|
58
71
|
assetsFromManifest,
|
|
59
72
|
buildIdentity,
|
|
60
73
|
createPrerenderGate,
|
|
61
74
|
createServeHandler,
|
|
75
|
+
documentAssetsFor,
|
|
76
|
+
forViteBase,
|
|
62
77
|
loadBuild,
|
|
63
78
|
nodeListener,
|
|
64
79
|
providerSpecifier,
|
|
80
|
+
readRegeneration,
|
|
65
81
|
withRequest,
|
|
66
82
|
} from "./internal/serve.js";
|
|
67
83
|
|
|
@@ -94,11 +110,23 @@ process.env.UF_PROJECT_ROOT = root;
|
|
|
94
110
|
// hooks for that; Bun is started with `--preload` on the same package's
|
|
95
111
|
// preload instead, and has no `register`.
|
|
96
112
|
//
|
|
113
|
+
// Deno has no `register` either, and it gets its hooks *here* rather than from
|
|
114
|
+
// a preload, for a reason that is about order. While a Deno `load` hook is
|
|
115
|
+
// registered, `require()` of a native addon fails — and Vite requires one,
|
|
116
|
+
// Rolldown's binding. The static imports above have already loaded Vite by
|
|
117
|
+
// this line, binding included, so hooks installed now see only what is
|
|
118
|
+
// imported after them: the config, and the `@uniflowed/*` modules this driver
|
|
119
|
+
// reaches dynamically. They are the in-thread hooks a new enough Node takes
|
|
120
|
+
// through `@uniflowed/host/register` too; see
|
|
121
|
+
// `@uniflowed/host/internal/sync-hooks.js`.
|
|
122
|
+
//
|
|
97
123
|
// The hooks live in `@uniflowed/host` rather than here: they are how Flow runs
|
|
98
124
|
// on a Capability JS Host, and nothing in them is Vite's. `uf test` reaches for
|
|
99
125
|
// the same package, which is what stopped a test run from depending on a
|
|
100
126
|
// bundler it never loads.
|
|
101
|
-
if (typeof
|
|
127
|
+
if (typeof Deno !== "undefined") {
|
|
128
|
+
installFlowHooks(root);
|
|
129
|
+
} else if (typeof Bun === "undefined") {
|
|
102
130
|
register("@uniflowed/host/internal/node-hooks.js", import.meta.url, { data: { root } });
|
|
103
131
|
}
|
|
104
132
|
|
|
@@ -152,6 +180,15 @@ async function loadConfig() {
|
|
|
152
180
|
return config;
|
|
153
181
|
}
|
|
154
182
|
|
|
183
|
+
/**
|
|
184
|
+
* `app.router.basePath` as the driver uses it: `""` at the root, `"/docs"`
|
|
185
|
+
* otherwise. Read where the build writes asset URLs and file names, because a
|
|
186
|
+
* prerendered document is written outside Vite's HTML transform.
|
|
187
|
+
*/
|
|
188
|
+
function basePathOf(config) {
|
|
189
|
+
return routingRulesOf(config.app?.router).basePath;
|
|
190
|
+
}
|
|
191
|
+
|
|
155
192
|
/** The Vite inline config a uf config describes. */
|
|
156
193
|
async function viteConfig(config, mode) {
|
|
157
194
|
const { default: uniflowed } = await import("./index.js");
|
|
@@ -191,6 +228,9 @@ async function viteConfig(config, mode) {
|
|
|
191
228
|
configFile: false,
|
|
192
229
|
envDir: false,
|
|
193
230
|
mode,
|
|
231
|
+
// `app.router.basePath`: where Vite serves the modules in development, and
|
|
232
|
+
// what it puts in front of every asset URL a build writes.
|
|
233
|
+
base: basePathOf(config) === "" ? "/" : `${basePathOf(config)}/`,
|
|
194
234
|
clearScreen: false,
|
|
195
235
|
customLogger: eventLogger(argument("--log-level") ?? "info"),
|
|
196
236
|
plugins: [uniflowed({ root, config, target: routeTarget })],
|
|
@@ -437,6 +477,37 @@ async function preview() {
|
|
|
437
477
|
const draftFirst = {
|
|
438
478
|
name: "uf:draft-before-files",
|
|
439
479
|
configurePreviewServer(previewServer) {
|
|
480
|
+
// `app.router.headers` and `redirects`, first of all: a redirect answers
|
|
481
|
+
// before a file is looked for, and a header is pinned on the response so
|
|
482
|
+
// it survives the `writeHead` Vite's file middleware writes its own
|
|
483
|
+
// with. `uf start` and every adapter put the same two in front of their
|
|
484
|
+
// static half; the application's own answers get them from
|
|
485
|
+
// `createServeHandler` behind. See `internal/serve.js`'s `answerRouting`.
|
|
486
|
+
const routing = build?.entry?.routing;
|
|
487
|
+
if (answersInFrontOfFiles(routing)) {
|
|
488
|
+
previewServer.middlewares.use((request, response, next) => {
|
|
489
|
+
answerRouting(routing, toAddressRequest(request), response)
|
|
490
|
+
.then((answered) => {
|
|
491
|
+
if (answered) return;
|
|
492
|
+
// The bare base path is the root, which Vite only knows as
|
|
493
|
+
// `/docs/`; see `forViteBase`.
|
|
494
|
+
forViteBase(routing, request);
|
|
495
|
+
next();
|
|
496
|
+
})
|
|
497
|
+
.catch(next);
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
// A prerendered payload is a file whose extension Vite's static middleware
|
|
501
|
+
// knows no type for, and the router hands bytes to React only when they
|
|
502
|
+
// are answered as a payload — so a navigation on a preview would silently
|
|
503
|
+
// become a document load. Set here, in front of the file server, which
|
|
504
|
+
// keeps a type it did not choose.
|
|
505
|
+
previewServer.middlewares.use((request, response, next) => {
|
|
506
|
+
if ((request.url ?? "").split("?")[0].endsWith("/__uf.flight")) {
|
|
507
|
+
response.setHeader("content-type", "text/x-component");
|
|
508
|
+
}
|
|
509
|
+
next();
|
|
510
|
+
});
|
|
440
511
|
if (handle == null) return;
|
|
441
512
|
const run = answer(previewServer);
|
|
442
513
|
previewServer.middlewares.use((request, response, next) => {
|
|
@@ -589,17 +660,60 @@ async function build() {
|
|
|
589
660
|
const staticBuild = flag("--static-build");
|
|
590
661
|
const because = argument("--because") ?? "this build prerenders every route";
|
|
591
662
|
|
|
663
|
+
// 0. The rsc graph, for an application React Server Components render: the
|
|
664
|
+
// route table and every server component, resolved under `react-server`.
|
|
665
|
+
// First, because it is what finds the client modules the next pass has
|
|
666
|
+
// to build; `./internal/flight.js` has the order and the reason for it.
|
|
667
|
+
const flight = flightStateOf(inline);
|
|
668
|
+
// `uf build --analyze`: the graph every bundle below is built from, for uf
|
|
669
|
+
// to attribute to routes; see `./internal/module-graph.js`. A client module
|
|
670
|
+
// is an entry the browser loads because a server component names it, not on
|
|
671
|
+
// every page, so it is not one of the client bundle's shared entries.
|
|
672
|
+
const graph = flag("--analyze")
|
|
673
|
+
? createModuleGraphCollector(root, {
|
|
674
|
+
isReference: (file) => flight?.clientModules.has(file) ?? false,
|
|
675
|
+
})
|
|
676
|
+
: null;
|
|
677
|
+
if (graph != null) {
|
|
678
|
+
inline.plugins = [...(inline.plugins ?? []), graph.plugin];
|
|
679
|
+
}
|
|
680
|
+
const rscDir = path.join(root, ".uf", "build", "rsc");
|
|
681
|
+
if (flight != null) {
|
|
682
|
+
emit("phase", { name: "rsc" });
|
|
683
|
+
await buildRscGraph(vite, inline, flight, { outDir: rscDir, conditions: null });
|
|
684
|
+
}
|
|
685
|
+
|
|
592
686
|
// 1. The client: everything the browser loads, with a manifest so the
|
|
593
|
-
// server render knows which script and stylesheet tags to write.
|
|
687
|
+
// server render knows which script and stylesheet tags to write. Under
|
|
688
|
+
// React Server Components that is the entry and one entry per client
|
|
689
|
+
// module, each keeping its export names, because a payload asks for a
|
|
690
|
+
// chunk by its URL and for a component by its export.
|
|
594
691
|
emit("phase", { name: "client" });
|
|
692
|
+
const references = flight == null ? [] : [...flight.clientModules].sort();
|
|
693
|
+
const input = { client: VIRTUAL.client };
|
|
694
|
+
references.forEach((file, index) => {
|
|
695
|
+
input[`client-reference-${index}`] = file;
|
|
696
|
+
});
|
|
595
697
|
await vite.build({
|
|
596
698
|
...inline,
|
|
597
699
|
build: {
|
|
598
700
|
...inline.build,
|
|
599
|
-
rollupOptions:
|
|
701
|
+
rollupOptions:
|
|
702
|
+
flight == null ? { input } : { input, preserveEntrySignatures: "exports-only" },
|
|
600
703
|
},
|
|
601
704
|
});
|
|
602
705
|
const manifest = readManifest(outDir);
|
|
706
|
+
if (flight != null) {
|
|
707
|
+
recordClientChunks(flight, manifest, references, rscDir, basePathOf(config));
|
|
708
|
+
// What the summary's "pages in the client bundle" reads. None: a browser
|
|
709
|
+
// that hydrates a payload imports no page, whichever route it is on.
|
|
710
|
+
emit("rsc-split", {
|
|
711
|
+
pages: 0,
|
|
712
|
+
routes: scanRoutes(path.resolve(root, config.app?.router?.root ?? "app"), {
|
|
713
|
+
target: resolveRouteTarget(config, argument("--target")),
|
|
714
|
+
}).routes.length,
|
|
715
|
+
});
|
|
716
|
+
}
|
|
603
717
|
|
|
604
718
|
// 2. The server entry, bundled for the host, outside `dist/` so it is never
|
|
605
719
|
// deployed by accident.
|
|
@@ -627,6 +741,9 @@ async function build() {
|
|
|
627
741
|
// `internal/serve.js`'s `buildIdentity`, which is what reads this.
|
|
628
742
|
writeFileSync(path.join(serverDir, BUILD_ID_FILE), `${mintBuildId()}\n`);
|
|
629
743
|
|
|
744
|
+
// Every bundle is built by here, and the prerender below builds none.
|
|
745
|
+
graph?.write(path.join(root, ".uf", "build", "meta", MODULE_GRAPH_FILE));
|
|
746
|
+
|
|
630
747
|
// 3. Which routes this build renders when, and every route it renders now.
|
|
631
748
|
//
|
|
632
749
|
// The decision comes from `uf.config.js` and is made in Rust — see
|
|
@@ -635,7 +752,14 @@ async function build() {
|
|
|
635
752
|
// arrives here as one word, and this is where it meets the route table.
|
|
636
753
|
emit("phase", { name: "prerender" });
|
|
637
754
|
const server = await import(pathToFileURL(path.join(serverDir, "server.js")).href);
|
|
638
|
-
const assets =
|
|
755
|
+
const assets =
|
|
756
|
+
flight == null
|
|
757
|
+
? assetsFromManifest(manifest, basePathOf(config))
|
|
758
|
+
: flightAssets(manifest, references, rscDir, outDir, basePathOf(config));
|
|
759
|
+
// Recorded beside the server bundle, because whatever serves this build
|
|
760
|
+
// later cannot recompute them from the client manifest alone; see
|
|
761
|
+
// `documentAssetsFor`.
|
|
762
|
+
writeFileSync(path.join(serverDir, DOCUMENT_ASSETS_FILE), `${JSON.stringify(assets, null, 2)}\n`);
|
|
639
763
|
const openapi = await createOpenApiDocument(server.handlers);
|
|
640
764
|
const openapiFile = path.join(root, ".uf", "build", "meta", "openapi.json");
|
|
641
765
|
mkdirSync(path.dirname(openapiFile), { recursive: true });
|
|
@@ -686,28 +810,86 @@ async function build() {
|
|
|
686
810
|
failures.push(url);
|
|
687
811
|
emit("page-failed", { url, ...errorEvent(error) });
|
|
688
812
|
};
|
|
813
|
+
//
|
|
814
|
+
// Each prerender runs inside a fill that stores nothing, so what a page states
|
|
815
|
+
// with `cacheLife`, `cacheTag` and `noStore` is something this loop can read
|
|
816
|
+
// rather than a call that throws for want of a scope.
|
|
817
|
+
//
|
|
818
|
+
// A page is written for regeneration when `uf` passed `--regenerate` — `isr`
|
|
819
|
+
// allowed in `app.rendering.modes`, `rendering.cache.route` on, and a server
|
|
820
|
+
// deployed to regenerate it — and the page stated a lifetime, called no
|
|
821
|
+
// `noStore`, and answered 200. Its document goes under
|
|
822
|
+
// `REGENERATED_DIRECTORY` rather than at its own URL, so no static half
|
|
823
|
+
// answers the page: the server does, starting from this document, until the
|
|
824
|
+
// lifetime has passed. What it stated goes into `REGENERATION_FILE` beside the
|
|
825
|
+
// server bundle. Every other page is the document it has always been.
|
|
826
|
+
//
|
|
827
|
+
// A tag without a lifetime is not enough, for the route cache's own reason:
|
|
828
|
+
// an entry with no end is one another process could serve from its memory
|
|
829
|
+
// for ever after `revalidateTag` took it out of the shared store.
|
|
830
|
+
const { collectCacheDeclarations } = await import("@uniflowed/server/cache");
|
|
831
|
+
const regenerate = flag("--regenerate");
|
|
832
|
+
const regenerated = {};
|
|
689
833
|
for (const url of pages) {
|
|
690
|
-
let
|
|
834
|
+
let declared;
|
|
835
|
+
const renderedAt = Date.now();
|
|
691
836
|
try {
|
|
692
|
-
|
|
837
|
+
declared = await collectCacheDeclarations(() => server.prerender(url, assets));
|
|
693
838
|
} catch (error) {
|
|
694
839
|
failed(url, error);
|
|
695
840
|
continue;
|
|
696
841
|
}
|
|
842
|
+
const result = declared.value;
|
|
697
843
|
if (result.error != null) {
|
|
698
844
|
failed(url, result.error);
|
|
699
845
|
continue;
|
|
700
846
|
}
|
|
701
|
-
const
|
|
847
|
+
const lifetime = declared.lifetime;
|
|
848
|
+
const regenerates =
|
|
849
|
+
regenerate && result.status === 200 && declared.denied == null && lifetime != null;
|
|
850
|
+
// The trailing-slash policy decides a served page's file name; a page the
|
|
851
|
+
// build regenerates keeps the one layout the regeneration reads.
|
|
852
|
+
const file = regenerates
|
|
853
|
+
? htmlPathFor(path.join(outDir, REGENERATED_DIRECTORY), url)
|
|
854
|
+
: htmlPathFor(outDir, url, routingRulesOf(config.app?.router).trailingSlash);
|
|
702
855
|
mkdirSync(path.dirname(file), { recursive: true });
|
|
703
856
|
writeFileSync(file, result.html);
|
|
857
|
+
// The payload the document was rendered from, beside it: what a browser
|
|
858
|
+
// navigating to this route fetches, from whatever serves the files. Beside
|
|
859
|
+
// the document wherever the document went, so a page written for
|
|
860
|
+
// regeneration leaves no file at its route's own payload URL answering with
|
|
861
|
+
// the build's copy for ever; the server answers that URL instead.
|
|
862
|
+
if (result.payload != null) {
|
|
863
|
+
// Beside the route's directory whatever the document is called:
|
|
864
|
+
// `guide/index.html` and `guide.html` both put it at `guide/__uf.flight`.
|
|
865
|
+
const payloadDirectory =
|
|
866
|
+
path.basename(file) === "index.html" ? path.dirname(file) : file.slice(0, -".html".length);
|
|
867
|
+
mkdirSync(payloadDirectory, { recursive: true });
|
|
868
|
+
writeFileSync(path.join(payloadDirectory, "__uf.flight"), result.payload);
|
|
869
|
+
}
|
|
870
|
+
if (regenerates) {
|
|
871
|
+
regenerated[url] = {
|
|
872
|
+
document: regeneratedDocumentUrl(url),
|
|
873
|
+
renderedAt,
|
|
874
|
+
revalidate: lifetime.revalidate,
|
|
875
|
+
expire: lifetime.expire ?? null,
|
|
876
|
+
tags: declared.tags,
|
|
877
|
+
};
|
|
878
|
+
}
|
|
704
879
|
emit("page", {
|
|
705
880
|
url,
|
|
706
881
|
file: path.relative(root, file),
|
|
707
882
|
status: result.status,
|
|
708
883
|
bytes: Buffer.byteLength(result.html),
|
|
884
|
+
regenerates,
|
|
709
885
|
});
|
|
710
886
|
}
|
|
887
|
+
if (Object.keys(regenerated).length > 0) {
|
|
888
|
+
writeFileSync(
|
|
889
|
+
path.join(serverDir, REGENERATION_FILE),
|
|
890
|
+
`${JSON.stringify({ pages: regenerated }, null, 2)}\n`,
|
|
891
|
+
);
|
|
892
|
+
}
|
|
711
893
|
// One `404.html`, from the boundary at the router root: a static host serves
|
|
712
894
|
// a single error document for the whole site, so the nested boundaries a
|
|
713
895
|
// project declares are the server's and the client's to render, not
|
|
@@ -1039,9 +1221,16 @@ async function compile() {
|
|
|
1039
1221
|
mkdirSync(bundleDir, { recursive: true });
|
|
1040
1222
|
writeFileSync(
|
|
1041
1223
|
entry,
|
|
1042
|
-
entrySource(
|
|
1224
|
+
entrySource(
|
|
1225
|
+
path.relative(root, assets),
|
|
1226
|
+
await documentAssetsFor(path.join(root, ".uf", "build", "server"), readManifest(outDir)),
|
|
1227
|
+
),
|
|
1043
1228
|
);
|
|
1044
1229
|
|
|
1230
|
+
// The rsc graph `uf build` built, and the client chunks its references name.
|
|
1231
|
+
const flight = flightStateOf(inline);
|
|
1232
|
+
if (flight != null) loadFlightBuild(flight, path.join(root, ".uf", "build", "rsc"));
|
|
1233
|
+
|
|
1045
1234
|
await vite.build({
|
|
1046
1235
|
...inline,
|
|
1047
1236
|
customLogger: eventLogger("warn"),
|
|
@@ -1081,13 +1270,21 @@ async function compile() {
|
|
|
1081
1270
|
* `static` is deliberately absent; `uf_config`'s
|
|
1082
1271
|
* `DeployAdapter::is_implemented` is the other half of that fact and
|
|
1083
1272
|
* `docs/app/reference/cli/$page.mdx` says why.
|
|
1273
|
+
*
|
|
1274
|
+
* `regenerationStore` is where a target keeps the pages a build regenerates
|
|
1275
|
+
* when `rendering.cache.store` names nothing: a disk for a process with one,
|
|
1276
|
+
* Workers KV for a Worker, and `null` for a Lambda, which keeps nothing between
|
|
1277
|
+
* invocations and is refused by name instead. A regenerated page kept only in
|
|
1278
|
+
* memory would go back to the build's copy on every restart, which is a page
|
|
1279
|
+
* that travels back in time. See [`deploy`].
|
|
1084
1280
|
*/
|
|
1085
1281
|
const ADAPTERS = {
|
|
1086
1282
|
node: {
|
|
1087
|
-
entries: (document, cache, build, schedules) => ({
|
|
1088
|
-
handler: handlerEntrySource(document, cache, NODE_CAPABILITIES, build),
|
|
1283
|
+
entries: (document, cache, build, schedules, regeneration) => ({
|
|
1284
|
+
handler: handlerEntrySource(document, cache, NODE_CAPABILITIES, build, regeneration),
|
|
1089
1285
|
server: nodeEntrySource("./handler.js", schedules),
|
|
1090
1286
|
}),
|
|
1287
|
+
regenerationStore: "filesystem",
|
|
1091
1288
|
},
|
|
1092
1289
|
// The same two files as `node`, with `@uniflowed/server/bun` in place of
|
|
1093
1290
|
// `@uniflowed/server/node`. That module is `./internal/static.js` for every
|
|
@@ -1104,32 +1301,40 @@ const ADAPTERS = {
|
|
|
1104
1301
|
// nobody here. `edge` pays that price because it must: there is no
|
|
1105
1302
|
// `node:stream` in a Worker.
|
|
1106
1303
|
bun: {
|
|
1107
|
-
entries: (document, cache, build, schedules) => ({
|
|
1108
|
-
handler: handlerEntrySource(document, cache, BUN_CAPABILITIES, build),
|
|
1304
|
+
entries: (document, cache, build, schedules, regeneration) => ({
|
|
1305
|
+
handler: handlerEntrySource(document, cache, BUN_CAPABILITIES, build, regeneration),
|
|
1109
1306
|
server: bunEntrySource("./handler.js", schedules),
|
|
1110
1307
|
}),
|
|
1308
|
+
regenerationStore: "filesystem",
|
|
1111
1309
|
},
|
|
1112
1310
|
deno: {
|
|
1113
|
-
entries: (document, cache, build, schedules) => ({
|
|
1114
|
-
handler: handlerEntrySource(document, cache, DENO_CAPABILITIES, build),
|
|
1311
|
+
entries: (document, cache, build, schedules, regeneration) => ({
|
|
1312
|
+
handler: handlerEntrySource(document, cache, DENO_CAPABILITIES, build, regeneration),
|
|
1115
1313
|
server: denoEntrySource("./handler.js", schedules),
|
|
1116
1314
|
}),
|
|
1315
|
+
regenerationStore: "filesystem",
|
|
1117
1316
|
},
|
|
1118
1317
|
// The same two files. What `--adapter container` adds is a `Dockerfile` and
|
|
1119
1318
|
// a `.dockerignore`, and both are plain text that `uf` writes beside this
|
|
1120
1319
|
// output rather than anything the bundler produces — see `uf_cli`'s
|
|
1121
1320
|
// `commands::deploy`.
|
|
1122
1321
|
container: {
|
|
1123
|
-
entries: (document, cache, build, schedules) => ({
|
|
1124
|
-
handler: handlerEntrySource(document, cache, NODE_CAPABILITIES, build),
|
|
1322
|
+
entries: (document, cache, build, schedules, regeneration) => ({
|
|
1323
|
+
handler: handlerEntrySource(document, cache, NODE_CAPABILITIES, build, regeneration),
|
|
1125
1324
|
server: nodeEntrySource("./handler.js", schedules),
|
|
1126
1325
|
}),
|
|
1326
|
+
regenerationStore: "filesystem",
|
|
1127
1327
|
},
|
|
1128
1328
|
edge: {
|
|
1129
|
-
entries: (document, cache, build, schedules) => ({
|
|
1130
|
-
handler: handlerEntrySource(document, cache, EDGE_CAPABILITIES, build),
|
|
1329
|
+
entries: (document, cache, build, schedules, regeneration) => ({
|
|
1330
|
+
handler: handlerEntrySource(document, cache, EDGE_CAPABILITIES, build, regeneration),
|
|
1131
1331
|
worker: workerEntrySource("./handler.js", schedules),
|
|
1132
1332
|
}),
|
|
1333
|
+
// Workers KV, through the same module seam a project's own provider goes
|
|
1334
|
+
// through. `packages/server/cache-kv.js` argues KV over the Cache API: the
|
|
1335
|
+
// seam has to find every entry under a tag, and the Cache API cannot list
|
|
1336
|
+
// what it holds.
|
|
1337
|
+
regenerationStore: "@uniflowed/server/cache/kv",
|
|
1133
1338
|
// `workerd` first, so React resolves to the build that has
|
|
1134
1339
|
// `renderToReadableStream` and no `node:stream`. `browser` and `module`
|
|
1135
1340
|
// after it are Vite's own SSR defaults, kept so a dependency with no
|
|
@@ -1141,12 +1346,19 @@ const ADAPTERS = {
|
|
|
1141
1346
|
// target which cannot provide a durable store says so, and this is the one
|
|
1142
1347
|
// target that cannot.
|
|
1143
1348
|
filesystem: false,
|
|
1349
|
+
// And the Node built-ins it has only as stubs, which the link reports by
|
|
1350
|
+
// module and importer. See `./internal/worker-builtins.js`.
|
|
1351
|
+
workerBuiltins: true,
|
|
1144
1352
|
},
|
|
1145
1353
|
serverless: {
|
|
1146
|
-
entries: (document, cache, build) => ({
|
|
1147
|
-
handler: handlerEntrySource(document, cache, SERVERLESS_CAPABILITIES, build),
|
|
1354
|
+
entries: (document, cache, build, _schedules, regeneration) => ({
|
|
1355
|
+
handler: handlerEntrySource(document, cache, SERVERLESS_CAPABILITIES, build, regeneration),
|
|
1148
1356
|
lambda: lambdaEntrySource("./handler.js"),
|
|
1149
1357
|
}),
|
|
1358
|
+
// A Lambda's memory lasts one instance and its disk is that instance's
|
|
1359
|
+
// `/tmp`, so neither is somewhere a regenerated page survives. A build that
|
|
1360
|
+
// regenerates pages has to name a provider module; see [`deploy`].
|
|
1361
|
+
regenerationStore: null,
|
|
1150
1362
|
},
|
|
1151
1363
|
};
|
|
1152
1364
|
|
|
@@ -1267,15 +1479,18 @@ async function deploy() {
|
|
|
1267
1479
|
// file is the version a person can open when a deployed directory
|
|
1268
1480
|
// misbehaves.
|
|
1269
1481
|
mkdirSync(work, { recursive: true });
|
|
1270
|
-
const document =
|
|
1482
|
+
const document = await documentAssetsFor(
|
|
1483
|
+
path.join(root, ".uf", "build", "server"),
|
|
1484
|
+
readManifest(outDir),
|
|
1485
|
+
);
|
|
1271
1486
|
// Whatever `build` above minted, so a durable cache in the deployed artefact
|
|
1272
1487
|
// is keyed by the build that produced it and not by the moment it was
|
|
1273
1488
|
// packaged. Read rather than minted again for exactly that reason: a second
|
|
1274
1489
|
// `randomUUID()` here would key the adapter's copy differently from the one
|
|
1275
1490
|
// `uf start` serves out of `.uf/build/`, which is two caches for one build.
|
|
1276
1491
|
const buildId = await buildIdentity(root, path.join(".uf", "build", "server"));
|
|
1277
|
-
const
|
|
1278
|
-
if (shape.filesystem === false &&
|
|
1492
|
+
const declaredCache = config.app?.rendering?.cache;
|
|
1493
|
+
if (shape.filesystem === false && declaredCache?.store === "filesystem") {
|
|
1279
1494
|
throw new Error(
|
|
1280
1495
|
`uf: rendering.cache.store is "filesystem" and \`--adapter ${adapter}\` has no ` +
|
|
1281
1496
|
"filesystem. Name a module exporting `createCacheProvider` instead — a KV " +
|
|
@@ -1283,13 +1498,49 @@ async function deploy() {
|
|
|
1283
1498
|
"docs/app/guide/cache.",
|
|
1284
1499
|
);
|
|
1285
1500
|
}
|
|
1286
|
-
|
|
1501
|
+
// The pages this build regenerates, and where this target keeps what it
|
|
1502
|
+
// regenerates when the project named no store: the adapter's
|
|
1503
|
+
// `regenerationStore`. A target with nowhere refuses by name and lists the
|
|
1504
|
+
// pages, rather than deploying pages that every cold start takes back to the
|
|
1505
|
+
// build's copy.
|
|
1506
|
+
const regeneration = await readRegeneration(path.join(root, ".uf", "build", "server"));
|
|
1507
|
+
let cacheConfig = declaredCache;
|
|
1508
|
+
if (regeneration != null && declaredCache?.store == null) {
|
|
1509
|
+
if (shape.regenerationStore == null) {
|
|
1510
|
+
const pages = Object.keys(regeneration.pages);
|
|
1511
|
+
throw new Error(
|
|
1512
|
+
`uf: this build regenerates ${pages.length} ${plural(pages.length, "page")} ` +
|
|
1513
|
+
`(${pages.join(", ")}), and \`--adapter ${adapter}\` has nowhere of its own to keep ` +
|
|
1514
|
+
"a regenerated page: an instance's memory and its /tmp both go with the instance. " +
|
|
1515
|
+
"Name a module exporting `createCacheProvider` in rendering.cache.store, or leave " +
|
|
1516
|
+
"`isr` out of app.rendering.modes to prerender those pages as documents that do not " +
|
|
1517
|
+
"change. See docs/app/guide/rendering.",
|
|
1518
|
+
);
|
|
1519
|
+
}
|
|
1520
|
+
cacheConfig = { ...declaredCache, store: shape.regenerationStore };
|
|
1521
|
+
}
|
|
1522
|
+
const entries = shape.entries(document, cacheConfig, buildId, schedules, regeneration);
|
|
1287
1523
|
const input = {};
|
|
1288
1524
|
for (const name of Object.keys(entries)) {
|
|
1289
1525
|
writeFileSync(path.join(work, `${name}.js`), entries[name]);
|
|
1290
1526
|
input[name] = path.join(work, `${name}.js`);
|
|
1291
1527
|
}
|
|
1292
1528
|
|
|
1529
|
+
// An application React Server Components render bundles the rsc graph into
|
|
1530
|
+
// its server, and a target with export conditions of its own needs that graph
|
|
1531
|
+
// resolved under them as well: React's Flight server has a Node build and a
|
|
1532
|
+
// worker build, exactly as its HTML renderer does.
|
|
1533
|
+
const flight = flightStateOf(inline);
|
|
1534
|
+
if (flight != null) {
|
|
1535
|
+
loadFlightBuild(flight, path.join(root, ".uf", "build", "rsc"));
|
|
1536
|
+
if (shape.conditions != null) {
|
|
1537
|
+
await buildRscGraph(vite, inline, flight, {
|
|
1538
|
+
outDir: path.join(work, "rsc"),
|
|
1539
|
+
conditions: shape.conditions,
|
|
1540
|
+
});
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1293
1544
|
const ssr = { ...(inline.ssr ?? {}), noExternal: true };
|
|
1294
1545
|
if (shape.conditions != null) {
|
|
1295
1546
|
// Which build of a dependency this target gets, and it is the difference
|
|
@@ -1304,7 +1555,24 @@ async function deploy() {
|
|
|
1304
1555
|
await vite.build({
|
|
1305
1556
|
...inline,
|
|
1306
1557
|
customLogger: eventLogger("warn"),
|
|
1307
|
-
plugins: [
|
|
1558
|
+
plugins: [
|
|
1559
|
+
...inline.plugins,
|
|
1560
|
+
nativeAddonGuard(),
|
|
1561
|
+
...(shape.workerBuiltins === true ? [workerBuiltinGuard()] : []),
|
|
1562
|
+
],
|
|
1563
|
+
// Fixed, because this bundle inlines every dependency and so both of each
|
|
1564
|
+
// React package's builds, and a runtime lookup of `NODE_ENV` in a worker
|
|
1565
|
+
// finds nothing and picks the development one. React's Flight client's
|
|
1566
|
+
// development build constructs a `WeakRef` for every response, which
|
|
1567
|
+
// workerd does not have: every document the edge artefact rendered was a
|
|
1568
|
+
// `ReferenceError`. The production build has none, and is the one a
|
|
1569
|
+
// deployment means.
|
|
1570
|
+
define: {
|
|
1571
|
+
...(inline.define ?? {}),
|
|
1572
|
+
"process.env.NODE_ENV": JSON.stringify(
|
|
1573
|
+
inline.mode === "development" ? "development" : "production",
|
|
1574
|
+
),
|
|
1575
|
+
},
|
|
1308
1576
|
ssr,
|
|
1309
1577
|
build: {
|
|
1310
1578
|
...inline.build,
|
|
@@ -1388,7 +1656,7 @@ async function deploy() {
|
|
|
1388
1656
|
* `buildIdentity` is where it came from and
|
|
1389
1657
|
* `packages/server/internal/cache-key.js` is why it exists.
|
|
1390
1658
|
*/
|
|
1391
|
-
function handlerEntrySource(document, cache, capabilities, build) {
|
|
1659
|
+
function handlerEntrySource(document, cache, capabilities, build, regeneration) {
|
|
1392
1660
|
const route = cache?.route === true;
|
|
1393
1661
|
const fetchCache = cache?.fetch === true;
|
|
1394
1662
|
// Nothing at all when both switches are off, so a default project's
|
|
@@ -1402,6 +1670,10 @@ function handlerEntrySource(document, cache, capabilities, build) {
|
|
|
1402
1670
|
`document: ${JSON.stringify(document)}`,
|
|
1403
1671
|
...(store ? ["cache"] : []),
|
|
1404
1672
|
"capabilities",
|
|
1673
|
+
// The pages this build regenerates, baked in beside the document and for
|
|
1674
|
+
// the same reason: the manifest exists on the machine doing the build, and
|
|
1675
|
+
// the deployed directory has only what this file carries.
|
|
1676
|
+
...(store && regeneration != null ? [`regeneration: ${JSON.stringify(regeneration)}`] : []),
|
|
1405
1677
|
].join(", ");
|
|
1406
1678
|
const cacheImport = store ? 'import { createCacheStore } from "@uniflowed/server/cache";\n' : "";
|
|
1407
1679
|
const providerImport = durable == null ? "" : `${durable.import}\n`;
|
|
@@ -1440,8 +1712,11 @@ const capabilities = ${capabilities.name}();
|
|
|
1440
1712
|
|
|
1441
1713
|
export const fetch = createFetchHandler({ ${options} });
|
|
1442
1714
|
export const beginRequest = app.beginRequest;
|
|
1715
|
+
// \`app.router\`'s redirects and headers, for the entry beside this file to put
|
|
1716
|
+
// in front of its static half. Rewrites are \`fetch\`'s own.
|
|
1717
|
+
export const routing = app.routing;
|
|
1443
1718
|
|
|
1444
|
-
export default { fetch, beginRequest };
|
|
1719
|
+
export default { fetch, beginRequest, routing };
|
|
1445
1720
|
`;
|
|
1446
1721
|
}
|
|
1447
1722
|
|
|
@@ -1549,7 +1824,7 @@ ${cron.imports}
|
|
|
1549
1824
|
// \`@uniflowed/server/node\` above, because the request has to be established in
|
|
1550
1825
|
// the storage the *application* reads, which is the copy bundled into
|
|
1551
1826
|
// \`handler.js\`. See ubugeeei-prod/uf#389.
|
|
1552
|
-
import { beginRequest, fetch } from ${JSON.stringify(handlerSpecifier)};
|
|
1827
|
+
import { beginRequest, fetch, routing } from ${JSON.stringify(handlerSpecifier)};
|
|
1553
1828
|
|
|
1554
1829
|
// Resolved from this file and not from the working directory: a process
|
|
1555
1830
|
// manager, a container entrypoint and a person in a shell each start a server
|
|
@@ -1562,7 +1837,7 @@ ${cron.declarations}
|
|
|
1562
1837
|
// (ubugeeei-prod/uf#204) and this entry is a module, so it would work; \`.catch\`
|
|
1563
1838
|
// is the better spelling regardless — a server that cannot take its port should
|
|
1564
1839
|
// say so and exit non-zero, rather than die as an unhandled rejection.
|
|
1565
|
-
serve({ handle: fetch, staticDir, beginRequest${cron.option} }).catch((error) => {
|
|
1840
|
+
serve({ handle: fetch, staticDir, beginRequest, routing${cron.option} }).catch((error) => {
|
|
1566
1841
|
process.stderr.write(\`uf: \${error?.message ?? String(error)}\\n\`);
|
|
1567
1842
|
process.exit(1);
|
|
1568
1843
|
});
|
|
@@ -1589,7 +1864,7 @@ ${cron.imports}
|
|
|
1589
1864
|
// \`@uniflowed/server/bun\` above, because the request has to be established in
|
|
1590
1865
|
// the storage the *application* reads, which is the copy bundled into
|
|
1591
1866
|
// \`handler.js\`. See ubugeeei-prod/uf#389.
|
|
1592
|
-
import { beginRequest, fetch } from ${JSON.stringify(handlerSpecifier)};
|
|
1867
|
+
import { beginRequest, fetch, routing } from ${JSON.stringify(handlerSpecifier)};
|
|
1593
1868
|
|
|
1594
1869
|
// Resolved from this file and not from the working directory: a process
|
|
1595
1870
|
// manager, a container entrypoint and a person in a shell each start a server
|
|
@@ -1598,7 +1873,7 @@ import { beginRequest, fetch } from ${JSON.stringify(handlerSpecifier)};
|
|
|
1598
1873
|
// trap in it.
|
|
1599
1874
|
const staticDir = path.join(path.dirname(fileURLToPath(import.meta.url)), "static");
|
|
1600
1875
|
${cron.declarations}
|
|
1601
|
-
serve({ handle: fetch, staticDir, beginRequest${cron.option} }).catch((error) => {
|
|
1876
|
+
serve({ handle: fetch, staticDir, beginRequest, routing${cron.option} }).catch((error) => {
|
|
1602
1877
|
process.stderr.write(\`uf: \${error?.message ?? String(error)}\\n\`);
|
|
1603
1878
|
process.exit(1);
|
|
1604
1879
|
});
|
|
@@ -1636,7 +1911,7 @@ globalThis.Buffer ??= {
|
|
|
1636
1911
|
};
|
|
1637
1912
|
globalThis.setImmediate ??= (callback, ...args) => setTimeout(callback, 0, ...args);
|
|
1638
1913
|
globalThis.clearImmediate ??= (handle) => clearTimeout(handle);
|
|
1639
|
-
const { beginRequest, fetch } = await import(${JSON.stringify(handlerSpecifier)});
|
|
1914
|
+
const { beginRequest, fetch, routing } = await import(${JSON.stringify(handlerSpecifier)});
|
|
1640
1915
|
|
|
1641
1916
|
// Resolved from this file and not from the working directory: a process
|
|
1642
1917
|
// manager and a person in a shell each start a server from wherever they
|
|
@@ -1644,7 +1919,7 @@ const { beginRequest, fetch } = await import(${JSON.stringify(handlerSpecifier)}
|
|
|
1644
1919
|
// started from inside itself would be a deployment with a trap in it.
|
|
1645
1920
|
const staticDir = decodeURIComponent(new URL("./static", import.meta.url).pathname);
|
|
1646
1921
|
${cron.declarations}
|
|
1647
|
-
serve({ handle: fetch, staticDir, beginRequest${cron.option} }).catch((error) => {
|
|
1922
|
+
serve({ handle: fetch, staticDir, beginRequest, routing${cron.option} }).catch((error) => {
|
|
1648
1923
|
console.error(\`uf: \${error?.message ?? String(error)}\`);
|
|
1649
1924
|
Deno.exit(1);
|
|
1650
1925
|
});
|
|
@@ -1671,11 +1946,15 @@ function workerEntrySource(handlerSpecifier, schedules) {
|
|
|
1671
1946
|
// would not be there.
|
|
1672
1947
|
if (declared.length === 0) {
|
|
1673
1948
|
return `// Generated by \`uf build --adapter edge\`. Not checked in, not edited.
|
|
1674
|
-
import { createWorkerFetch } from "@uniflowed/server/edge";
|
|
1949
|
+
import { createWorkerFetch, installWorkerLogger } from "@uniflowed/server/edge";
|
|
1950
|
+
|
|
1951
|
+
import { beginRequest, fetch as handle, routing } from ${JSON.stringify(handlerSpecifier)};
|
|
1675
1952
|
|
|
1676
|
-
|
|
1953
|
+
// After the imports, so a logger the application installed while it loaded is
|
|
1954
|
+
// the one that stays; see \`installWorkerLogger\`.
|
|
1955
|
+
installWorkerLogger();
|
|
1677
1956
|
|
|
1678
|
-
export default { fetch: createWorkerFetch({ handle, beginRequest }) };
|
|
1957
|
+
export default { fetch: createWorkerFetch({ handle, beginRequest, routing }) };
|
|
1679
1958
|
`;
|
|
1680
1959
|
}
|
|
1681
1960
|
|
|
@@ -1684,16 +1963,20 @@ export default { fetch: createWorkerFetch({ handle, beginRequest }) };
|
|
|
1684
1963
|
// `uf` writes both from one list, so the two cannot disagree.
|
|
1685
1964
|
const routes = Object.fromEntries(declared.map((schedule) => [schedule.cron, schedule.path]));
|
|
1686
1965
|
return `// Generated by \`uf build --adapter edge\`. Not checked in, not edited.
|
|
1687
|
-
import { createWorkerFetch, createWorkerScheduled } from "@uniflowed/server/edge";
|
|
1966
|
+
import { createWorkerFetch, createWorkerScheduled, installWorkerLogger } from "@uniflowed/server/edge";
|
|
1688
1967
|
|
|
1689
|
-
import { beginRequest, fetch as handle } from ${JSON.stringify(handlerSpecifier)};
|
|
1968
|
+
import { beginRequest, fetch as handle, routing } from ${JSON.stringify(handlerSpecifier)};
|
|
1969
|
+
|
|
1970
|
+
// After the imports, so a logger the application installed while it loaded is
|
|
1971
|
+
// the one that stays; see \`installWorkerLogger\`.
|
|
1972
|
+
installWorkerLogger();
|
|
1690
1973
|
|
|
1691
1974
|
// \`triggers.crons\` in the wrangler.json beside this file names these same
|
|
1692
1975
|
// expressions. See ubugeeei-prod/uf#531.
|
|
1693
1976
|
const routes = ${JSON.stringify(routes, null, 2)};
|
|
1694
1977
|
|
|
1695
1978
|
export default {
|
|
1696
|
-
fetch: createWorkerFetch({ handle, beginRequest }),
|
|
1979
|
+
fetch: createWorkerFetch({ handle, beginRequest, routing }),
|
|
1697
1980
|
scheduled: createWorkerScheduled({ handle, beginRequest, routes }),
|
|
1698
1981
|
};
|
|
1699
1982
|
`;
|
|
@@ -1719,7 +2002,7 @@ import { fileURLToPath } from "node:url";
|
|
|
1719
2002
|
|
|
1720
2003
|
import { createLambdaHandler } from "@uniflowed/server/lambda";
|
|
1721
2004
|
|
|
1722
|
-
import { beginRequest, fetch as handle } from ${JSON.stringify(handlerSpecifier)};
|
|
2005
|
+
import { beginRequest, fetch as handle, routing } from ${JSON.stringify(handlerSpecifier)};
|
|
1723
2006
|
|
|
1724
2007
|
// Resolved from this file and not from the working directory: Lambda sets the
|
|
1725
2008
|
// working directory to the task root today and is under no obligation to keep
|
|
@@ -1727,7 +2010,7 @@ import { beginRequest, fetch as handle } from ${JSON.stringify(handlerSpecifier)
|
|
|
1727
2010
|
// deployment with a trap in it.
|
|
1728
2011
|
const staticDir = path.join(path.dirname(fileURLToPath(import.meta.url)), "static");
|
|
1729
2012
|
|
|
1730
|
-
export const handler = createLambdaHandler({ handle, beginRequest, staticDir });
|
|
2013
|
+
export const handler = createLambdaHandler({ handle, beginRequest, staticDir, routing });
|
|
1731
2014
|
`;
|
|
1732
2015
|
}
|
|
1733
2016
|
|
|
@@ -1803,6 +2086,38 @@ function nativeAddonGuard() {
|
|
|
1803
2086
|
};
|
|
1804
2087
|
}
|
|
1805
2088
|
|
|
2089
|
+
/**
|
|
2090
|
+
* Say which Node built-ins the Worker being linked reaches and does not have.
|
|
2091
|
+
*
|
|
2092
|
+
* `--adapter edge` only. Every import the bundler resolves passes through here,
|
|
2093
|
+
* and one naming a module `./internal/worker-builtins.js` measured as a stub at
|
|
2094
|
+
* the compatibility date uf writes is kept with the file that imported it. Only
|
|
2095
|
+
* the ones still in the output are reported — see `survivingImports` for the
|
|
2096
|
+
* fixture that showed why — and as warnings rather than a refusal, for the
|
|
2097
|
+
* reason that module gives: a deployment at a newer date may have the module,
|
|
2098
|
+
* and uf cannot see that deployment. What it can do is say, before anything is
|
|
2099
|
+
* uploaded, which request is going to answer 500 and why.
|
|
2100
|
+
*/
|
|
2101
|
+
function workerBuiltinGuard() {
|
|
2102
|
+
const reached = [];
|
|
2103
|
+
return {
|
|
2104
|
+
name: "uf:worker-builtins",
|
|
2105
|
+
enforce: "pre",
|
|
2106
|
+
resolveId(source, importer) {
|
|
2107
|
+
if (importer != null && unavailableOnWorkers(source) != null) {
|
|
2108
|
+
reached.push({ specifier: source, importer });
|
|
2109
|
+
}
|
|
2110
|
+
return null;
|
|
2111
|
+
},
|
|
2112
|
+
generateBundle(_options, bundle) {
|
|
2113
|
+
const chunks = Object.values(bundle).filter((output) => output.type === "chunk");
|
|
2114
|
+
for (const warning of workerBuiltinWarnings(survivingImports(reached, chunks), root)) {
|
|
2115
|
+
this.warn(warning);
|
|
2116
|
+
}
|
|
2117
|
+
},
|
|
2118
|
+
};
|
|
2119
|
+
}
|
|
2120
|
+
|
|
1806
2121
|
/** `word`, pluralised for `count`. */
|
|
1807
2122
|
function plural(count, word) {
|
|
1808
2123
|
return count === 1 ? word : `${word}s`;
|
|
@@ -1814,6 +2129,138 @@ async function printConfig() {
|
|
|
1814
2129
|
process.exit(0);
|
|
1815
2130
|
}
|
|
1816
2131
|
|
|
2132
|
+
/**
|
|
2133
|
+
* The React Server Components state `@uniflowed/vite` shares with this driver,
|
|
2134
|
+
* or `null` for an application rendered from its modules.
|
|
2135
|
+
*
|
|
2136
|
+
* Read off the plugin rather than decided again here: `rendersFlight` in
|
|
2137
|
+
* `./internal/flight.js` decides from the same `uf.config.js`, and a second
|
|
2138
|
+
* reading of it would be the one that drifts.
|
|
2139
|
+
*/
|
|
2140
|
+
function flightStateOf(inline) {
|
|
2141
|
+
const plugins = (inline.plugins ?? []).flat(Number.POSITIVE_INFINITY);
|
|
2142
|
+
return plugins.find((plugin) => plugin?.name === "uf:flow")?.api?.flight ?? null;
|
|
2143
|
+
}
|
|
2144
|
+
|
|
2145
|
+
/** What a later command needs of the rsc build, written beside its output. */
|
|
2146
|
+
const FLIGHT_BUILD_FILE = "uf-flight.json";
|
|
2147
|
+
|
|
2148
|
+
/**
|
|
2149
|
+
* Build the rsc graph into `outDir`.
|
|
2150
|
+
*
|
|
2151
|
+
* `conditions` are a deploy target's own, added to `react-server`; `null` is
|
|
2152
|
+
* the Node server `uf build` writes. Vite's builder rather than `vite.build`,
|
|
2153
|
+
* because the rsc graph is an environment of its own and `vite.build` builds
|
|
2154
|
+
* the two Vite always has.
|
|
2155
|
+
*/
|
|
2156
|
+
async function buildRscGraph(vite, inline, state, { outDir, conditions }) {
|
|
2157
|
+
const environment = {
|
|
2158
|
+
build: {
|
|
2159
|
+
outDir,
|
|
2160
|
+
emptyOutDir: true,
|
|
2161
|
+
rollupOptions: {
|
|
2162
|
+
input: { index: FLIGHT_VIRTUAL.entry },
|
|
2163
|
+
output: { entryFileNames: "[name].js", format: "es" },
|
|
2164
|
+
},
|
|
2165
|
+
},
|
|
2166
|
+
};
|
|
2167
|
+
if (conditions != null) {
|
|
2168
|
+
environment.resolve = {
|
|
2169
|
+
conditions: ["react-server", ...conditions],
|
|
2170
|
+
externalConditions: ["react-server", ...conditions],
|
|
2171
|
+
};
|
|
2172
|
+
}
|
|
2173
|
+
const builder = await vite.createBuilder({
|
|
2174
|
+
...inline,
|
|
2175
|
+
customLogger: eventLogger("warn"),
|
|
2176
|
+
environments: { [RSC_ENVIRONMENT]: environment },
|
|
2177
|
+
});
|
|
2178
|
+
await builder.build(builder.environments[RSC_ENVIRONMENT]);
|
|
2179
|
+
state.rscOutput = path.join(outDir, "index.js");
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2182
|
+
/**
|
|
2183
|
+
* Record the chunk each client module was built into, and write it down.
|
|
2184
|
+
*
|
|
2185
|
+
* Written down because `uf build --adapter` and `uf build --compile` bundle
|
|
2186
|
+
* the server again in a process of their own, and a reference in the rsc
|
|
2187
|
+
* output names its module by path, which only this build's manifest turns
|
|
2188
|
+
* into a URL.
|
|
2189
|
+
*/
|
|
2190
|
+
function recordClientChunks(state, manifest, references, rscDir, base = "") {
|
|
2191
|
+
for (const file of references) {
|
|
2192
|
+
const key = path.relative(root, file).split(path.sep).join("/");
|
|
2193
|
+
const chunk = manifest[key];
|
|
2194
|
+
if (chunk == null) {
|
|
2195
|
+
throw new Error(
|
|
2196
|
+
`uf: the client build wrote no chunk for ${key}, which a server component renders as a ` +
|
|
2197
|
+
"client component",
|
|
2198
|
+
);
|
|
2199
|
+
}
|
|
2200
|
+
state.chunkUrls.set(file, `${base}/${chunk.file}`);
|
|
2201
|
+
}
|
|
2202
|
+
writeFileSync(
|
|
2203
|
+
path.join(rscDir, FLIGHT_BUILD_FILE),
|
|
2204
|
+
`${JSON.stringify({ chunkUrls: [...state.chunkUrls] }, null, 2)}\n`,
|
|
2205
|
+
);
|
|
2206
|
+
}
|
|
2207
|
+
|
|
2208
|
+
/** What `recordClientChunks` wrote, for a command that runs after `uf build`. */
|
|
2209
|
+
function loadFlightBuild(state, rscDir) {
|
|
2210
|
+
const file = path.join(rscDir, FLIGHT_BUILD_FILE);
|
|
2211
|
+
if (!existsSync(file)) {
|
|
2212
|
+
throw new Error(
|
|
2213
|
+
`uf: ${path.relative(root, file)} is missing, so there is no rsc graph to render routes ` +
|
|
2214
|
+
"with; run `uf build` first",
|
|
2215
|
+
);
|
|
2216
|
+
}
|
|
2217
|
+
state.chunkUrls = new Map(JSON.parse(readFileSync(file, "utf8")).chunkUrls);
|
|
2218
|
+
state.rscOutput = path.join(rscDir, "index.js");
|
|
2219
|
+
}
|
|
2220
|
+
|
|
2221
|
+
/**
|
|
2222
|
+
* The tags a document React Server Components render needs.
|
|
2223
|
+
*
|
|
2224
|
+
* `assetsFromManifest`'s, with stylesheets from three places in the order they
|
|
2225
|
+
* cascade: the rsc graph's first — every layout's and every server component's
|
|
2226
|
+
* — then the client entry's, then each client module's own, which the client
|
|
2227
|
+
* build emits beside that module's chunk and no import from the entry reaches.
|
|
2228
|
+
*
|
|
2229
|
+
* The rsc build's emitted files are copied under `dist/` so those URLs resolve,
|
|
2230
|
+
* and only its assets: a server bundle's JavaScript is never a deployable file.
|
|
2231
|
+
*/
|
|
2232
|
+
function flightAssets(manifest, references, rscDir, outDir, base = "") {
|
|
2233
|
+
const assets = assetsFromManifest(manifest, base);
|
|
2234
|
+
const styles = new Set();
|
|
2235
|
+
const rscManifest = path.join(rscDir, ".vite", "manifest.json");
|
|
2236
|
+
if (existsSync(rscManifest)) {
|
|
2237
|
+
const rscStyles = assetsFromManifest(
|
|
2238
|
+
JSON.parse(readFileSync(rscManifest, "utf8")),
|
|
2239
|
+
base,
|
|
2240
|
+
).styles;
|
|
2241
|
+
for (const href of rscStyles) styles.add(href);
|
|
2242
|
+
}
|
|
2243
|
+
const rscAssets = path.join(rscDir, "assets");
|
|
2244
|
+
if (existsSync(rscAssets)) {
|
|
2245
|
+
cpSync(rscAssets, path.join(outDir, "assets"), {
|
|
2246
|
+
recursive: true,
|
|
2247
|
+
filter: (from) => !/\.(?:[cm]?js|map)$/.test(from),
|
|
2248
|
+
});
|
|
2249
|
+
}
|
|
2250
|
+
for (const href of assets.styles) styles.add(href);
|
|
2251
|
+
const seen = new Set();
|
|
2252
|
+
const visit = (key) => {
|
|
2253
|
+
if (seen.has(key)) return;
|
|
2254
|
+
seen.add(key);
|
|
2255
|
+
const chunk = manifest[key];
|
|
2256
|
+
if (chunk == null) return;
|
|
2257
|
+
for (const css of chunk.css ?? []) styles.add(`${base}/${css}`);
|
|
2258
|
+
for (const imported of chunk.imports ?? []) visit(imported);
|
|
2259
|
+
};
|
|
2260
|
+
for (const file of references) visit(path.relative(root, file).split(path.sep).join("/"));
|
|
2261
|
+
return { ...assets, styles: [...styles] };
|
|
2262
|
+
}
|
|
2263
|
+
|
|
1817
2264
|
function readManifest(outDir) {
|
|
1818
2265
|
const file = path.join(outDir, ".vite", "manifest.json");
|
|
1819
2266
|
if (!existsSync(file)) throw new Error(`uf: the client build wrote no manifest at ${file}`);
|
|
@@ -1949,6 +2396,21 @@ async function renderingPlan(server, prerender) {
|
|
|
1949
2396
|
why: "a middleware guards it, and a middleware runs once per request",
|
|
1950
2397
|
});
|
|
1951
2398
|
}
|
|
2399
|
+
// And `app.router`'s three lists, by the source each rule matches: a file
|
|
2400
|
+
// can be neither a redirect nor a rewrite, and a header a file is served
|
|
2401
|
+
// with is the host's to add rather than the file's.
|
|
2402
|
+
for (const [key, what] of [
|
|
2403
|
+
["redirects", "a redirect"],
|
|
2404
|
+
["rewrites", "a rewrite"],
|
|
2405
|
+
["headers", "a response header"],
|
|
2406
|
+
]) {
|
|
2407
|
+
for (const rule of server.routing?.[key] ?? []) {
|
|
2408
|
+
perRequest.push({
|
|
2409
|
+
path: rule.source,
|
|
2410
|
+
why: `\`app.router.${key}\` names it, and ${what} is answered when a request arrives`,
|
|
2411
|
+
});
|
|
2412
|
+
}
|
|
2413
|
+
}
|
|
1952
2414
|
|
|
1953
2415
|
return { urls, perRequest };
|
|
1954
2416
|
}
|
|
@@ -1970,9 +2432,36 @@ function fillParams(routePath, params) {
|
|
|
1970
2432
|
.join("/");
|
|
1971
2433
|
}
|
|
1972
2434
|
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
2435
|
+
/**
|
|
2436
|
+
* The file a prerendered page is written to.
|
|
2437
|
+
*
|
|
2438
|
+
* `guide/index.html`, which every static host serves at `/guide/` and most at
|
|
2439
|
+
* `/guide`, unless `app.router.trailingSlash` is `"never"` — then `guide.html`,
|
|
2440
|
+
* which the same hosts serve at `/guide` without a redirect to the slash.
|
|
2441
|
+
* Next.js's static export makes the same choice from the same setting.
|
|
2442
|
+
*/
|
|
2443
|
+
function htmlPathFor(outDir, url, trailingSlash = "ignore") {
|
|
2444
|
+
const pathname = url.split("?")[0].replace(/^\/+/, "").replace(/\/+$/, "");
|
|
2445
|
+
if (pathname === "") return path.join(outDir, "index.html");
|
|
2446
|
+
return trailingSlash === "never"
|
|
2447
|
+
? path.join(outDir, `${pathname}.html`)
|
|
1977
2448
|
: path.join(outDir, pathname, "index.html");
|
|
1978
2449
|
}
|
|
2450
|
+
|
|
2451
|
+
/**
|
|
2452
|
+
* The URL path at which a static half answers the document `htmlPathFor`
|
|
2453
|
+
* wrote for `url` under `REGENERATED_DIRECTORY`.
|
|
2454
|
+
*
|
|
2455
|
+
* Ending in a slash, so every host answers it with that directory's
|
|
2456
|
+
* `index.html` in the same way: a Node static half tries `index.html` for such
|
|
2457
|
+
* a path, and a Worker's assets binding serves it without the redirect it
|
|
2458
|
+
* answers `…/index.html` with.
|
|
2459
|
+
*/
|
|
2460
|
+
function regeneratedDocumentUrl(url) {
|
|
2461
|
+
const pathname = url.split("?")[0].replace(/^\/+/, "").replace(/\/+$/, "");
|
|
2462
|
+
const encoded = pathname
|
|
2463
|
+
.split("/")
|
|
2464
|
+
.map((segment) => encodeURIComponent(segment))
|
|
2465
|
+
.join("/");
|
|
2466
|
+
return pathname === "" ? `/${REGENERATED_DIRECTORY}/` : `/${REGENERATED_DIRECTORY}/${encoded}/`;
|
|
2467
|
+
}
|