@uniflowed/vite 0.0.0-alpha.35 → 0.0.0-alpha.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/driver.js +281 -50
- package/index.js +146 -15
- package/internal/barrel-imports.js +459 -0
- package/internal/flight.js +128 -11
- package/internal/http.js +21 -2
- package/internal/module-graph.js +134 -0
- package/internal/routes.js +58 -2
- package/internal/serve.js +158 -12
- package/internal/server-components.js +109 -0
- package/package.json +5 -5
package/driver.js
CHANGED
|
@@ -42,6 +42,7 @@
|
|
|
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 { installFlowHooks } from "@uniflowed/host/internal/sync-hooks.js";
|
|
45
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";
|
|
@@ -54,22 +55,29 @@ import {
|
|
|
54
55
|
} from "./internal/worker-builtins.js";
|
|
55
56
|
import { emit, errorEvent, eventLogger } from "./internal/events.js";
|
|
56
57
|
import { loadUfConfig, projectConfig } from "./internal/config.js";
|
|
57
|
-
import { send, toRequest } from "./internal/http.js";
|
|
58
|
+
import { send, toAddressRequest, toRequest } from "./internal/http.js";
|
|
58
59
|
import { createOpenApiDocument } from "./internal/openapi.js";
|
|
59
60
|
import { withProjectConfig } from "./merge.js";
|
|
60
61
|
import { FLIGHT_VIRTUAL, RSC_ENVIRONMENT } from "./internal/flight.js";
|
|
61
|
-
import {
|
|
62
|
+
import { MODULE_GRAPH_FILE, createModuleGraphCollector } from "./internal/module-graph.js";
|
|
63
|
+
import { VIRTUAL, resolveRouteTarget, routingRulesOf, scanRoutes } from "./internal/routes.js";
|
|
62
64
|
import {
|
|
63
65
|
BUILD_ID_FILE,
|
|
64
66
|
DOCUMENT_ASSETS_FILE,
|
|
67
|
+
REGENERATED_DIRECTORY,
|
|
68
|
+
REGENERATION_FILE,
|
|
69
|
+
answerRouting,
|
|
70
|
+
answersInFrontOfFiles,
|
|
65
71
|
assetsFromManifest,
|
|
66
72
|
buildIdentity,
|
|
67
73
|
createPrerenderGate,
|
|
68
74
|
createServeHandler,
|
|
69
75
|
documentAssetsFor,
|
|
76
|
+
forViteBase,
|
|
70
77
|
loadBuild,
|
|
71
78
|
nodeListener,
|
|
72
79
|
providerSpecifier,
|
|
80
|
+
readRegeneration,
|
|
73
81
|
withRequest,
|
|
74
82
|
} from "./internal/serve.js";
|
|
75
83
|
|
|
@@ -102,11 +110,23 @@ process.env.UF_PROJECT_ROOT = root;
|
|
|
102
110
|
// hooks for that; Bun is started with `--preload` on the same package's
|
|
103
111
|
// preload instead, and has no `register`.
|
|
104
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
|
+
//
|
|
105
123
|
// The hooks live in `@uniflowed/host` rather than here: they are how Flow runs
|
|
106
124
|
// on a Capability JS Host, and nothing in them is Vite's. `uf test` reaches for
|
|
107
125
|
// the same package, which is what stopped a test run from depending on a
|
|
108
126
|
// bundler it never loads.
|
|
109
|
-
if (typeof
|
|
127
|
+
if (typeof Deno !== "undefined") {
|
|
128
|
+
installFlowHooks(root);
|
|
129
|
+
} else if (typeof Bun === "undefined") {
|
|
110
130
|
register("@uniflowed/host/internal/node-hooks.js", import.meta.url, { data: { root } });
|
|
111
131
|
}
|
|
112
132
|
|
|
@@ -160,6 +180,15 @@ async function loadConfig() {
|
|
|
160
180
|
return config;
|
|
161
181
|
}
|
|
162
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
|
+
|
|
163
192
|
/** The Vite inline config a uf config describes. */
|
|
164
193
|
async function viteConfig(config, mode) {
|
|
165
194
|
const { default: uniflowed } = await import("./index.js");
|
|
@@ -199,6 +228,27 @@ async function viteConfig(config, mode) {
|
|
|
199
228
|
configFile: false,
|
|
200
229
|
envDir: false,
|
|
201
230
|
mode,
|
|
231
|
+
// Vite's dependency cache, kept in the project whatever is above it.
|
|
232
|
+
// Vite's own default is the nearest `package.json`'s `node_modules/.vite`,
|
|
233
|
+
// and a uf project needs no `package.json`. One inside another repository
|
|
234
|
+
// pre-bundled into that repository's `node_modules`, in one directory
|
|
235
|
+
// shared with every other such project there and emptied by each dev
|
|
236
|
+
// server that started — which is how two `uf dev` servers came to answer
|
|
237
|
+
// each other's pre-bundled files with `504 Outdated Optimize Dep`
|
|
238
|
+
// (ubugeeei-prod/uf#1141).
|
|
239
|
+
//
|
|
240
|
+
// This directory rather than `.vite` or `.uf/cache/vite`, because it is
|
|
241
|
+
// where Vite already puts the cache for a project with its own
|
|
242
|
+
// `package.json`, so nothing moves for one: the URLs stay
|
|
243
|
+
// `/node_modules/.vite/deps/…`, and the files stay under `node_modules`,
|
|
244
|
+
// which git ignores, Vite's watcher skips and Vite's sourcemap ignore list
|
|
245
|
+
// treats as somebody else's code. `cacheDir` is shared config, so every
|
|
246
|
+
// environment's optimizer follows it — `deps`, `deps_ssr`, `deps_rsc` —
|
|
247
|
+
// and a project's own `vite.cacheDir` is merged over it like any option.
|
|
248
|
+
cacheDir: path.join(root, "node_modules", ".vite"),
|
|
249
|
+
// `app.router.basePath`: where Vite serves the modules in development, and
|
|
250
|
+
// what it puts in front of every asset URL a build writes.
|
|
251
|
+
base: basePathOf(config) === "" ? "/" : `${basePathOf(config)}/`,
|
|
202
252
|
clearScreen: false,
|
|
203
253
|
customLogger: eventLogger(argument("--log-level") ?? "info"),
|
|
204
254
|
plugins: [uniflowed({ root, config, target: routeTarget })],
|
|
@@ -445,6 +495,26 @@ async function preview() {
|
|
|
445
495
|
const draftFirst = {
|
|
446
496
|
name: "uf:draft-before-files",
|
|
447
497
|
configurePreviewServer(previewServer) {
|
|
498
|
+
// `app.router.headers` and `redirects`, first of all: a redirect answers
|
|
499
|
+
// before a file is looked for, and a header is pinned on the response so
|
|
500
|
+
// it survives the `writeHead` Vite's file middleware writes its own
|
|
501
|
+
// with. `uf start` and every adapter put the same two in front of their
|
|
502
|
+
// static half; the application's own answers get them from
|
|
503
|
+
// `createServeHandler` behind. See `internal/serve.js`'s `answerRouting`.
|
|
504
|
+
const routing = build?.entry?.routing;
|
|
505
|
+
if (answersInFrontOfFiles(routing)) {
|
|
506
|
+
previewServer.middlewares.use((request, response, next) => {
|
|
507
|
+
answerRouting(routing, toAddressRequest(request), response)
|
|
508
|
+
.then((answered) => {
|
|
509
|
+
if (answered) return;
|
|
510
|
+
// The bare base path is the root, which Vite only knows as
|
|
511
|
+
// `/docs/`; see `forViteBase`.
|
|
512
|
+
forViteBase(routing, request);
|
|
513
|
+
next();
|
|
514
|
+
})
|
|
515
|
+
.catch(next);
|
|
516
|
+
});
|
|
517
|
+
}
|
|
448
518
|
// A prerendered payload is a file whose extension Vite's static middleware
|
|
449
519
|
// knows no type for, and the router hands bytes to React only when they
|
|
450
520
|
// are answered as a payload — so a navigation on a preview would silently
|
|
@@ -613,6 +683,18 @@ async function build() {
|
|
|
613
683
|
// First, because it is what finds the client modules the next pass has
|
|
614
684
|
// to build; `./internal/flight.js` has the order and the reason for it.
|
|
615
685
|
const flight = flightStateOf(inline);
|
|
686
|
+
// `uf build --analyze`: the graph every bundle below is built from, for uf
|
|
687
|
+
// to attribute to routes; see `./internal/module-graph.js`. A client module
|
|
688
|
+
// is an entry the browser loads because a server component names it, not on
|
|
689
|
+
// every page, so it is not one of the client bundle's shared entries.
|
|
690
|
+
const graph = flag("--analyze")
|
|
691
|
+
? createModuleGraphCollector(root, {
|
|
692
|
+
isReference: (file) => flight?.clientModules.has(file) ?? false,
|
|
693
|
+
})
|
|
694
|
+
: null;
|
|
695
|
+
if (graph != null) {
|
|
696
|
+
inline.plugins = [...(inline.plugins ?? []), graph.plugin];
|
|
697
|
+
}
|
|
616
698
|
const rscDir = path.join(root, ".uf", "build", "rsc");
|
|
617
699
|
if (flight != null) {
|
|
618
700
|
emit("phase", { name: "rsc" });
|
|
@@ -640,7 +722,7 @@ async function build() {
|
|
|
640
722
|
});
|
|
641
723
|
const manifest = readManifest(outDir);
|
|
642
724
|
if (flight != null) {
|
|
643
|
-
recordClientChunks(flight, manifest, references, rscDir);
|
|
725
|
+
recordClientChunks(flight, manifest, references, rscDir, basePathOf(config));
|
|
644
726
|
// What the summary's "pages in the client bundle" reads. None: a browser
|
|
645
727
|
// that hydrates a payload imports no page, whichever route it is on.
|
|
646
728
|
emit("rsc-split", {
|
|
@@ -677,6 +759,9 @@ async function build() {
|
|
|
677
759
|
// `internal/serve.js`'s `buildIdentity`, which is what reads this.
|
|
678
760
|
writeFileSync(path.join(serverDir, BUILD_ID_FILE), `${mintBuildId()}\n`);
|
|
679
761
|
|
|
762
|
+
// Every bundle is built by here, and the prerender below builds none.
|
|
763
|
+
graph?.write(path.join(root, ".uf", "build", "meta", MODULE_GRAPH_FILE));
|
|
764
|
+
|
|
680
765
|
// 3. Which routes this build renders when, and every route it renders now.
|
|
681
766
|
//
|
|
682
767
|
// The decision comes from `uf.config.js` and is made in Rust — see
|
|
@@ -687,8 +772,8 @@ async function build() {
|
|
|
687
772
|
const server = await import(pathToFileURL(path.join(serverDir, "server.js")).href);
|
|
688
773
|
const assets =
|
|
689
774
|
flight == null
|
|
690
|
-
? assetsFromManifest(manifest)
|
|
691
|
-
: flightAssets(manifest, references, rscDir, outDir);
|
|
775
|
+
? assetsFromManifest(manifest, basePathOf(config))
|
|
776
|
+
: flightAssets(manifest, references, rscDir, outDir, basePathOf(config));
|
|
692
777
|
// Recorded beside the server bundle, because whatever serves this build
|
|
693
778
|
// later cannot recompute them from the client manifest alone; see
|
|
694
779
|
// `documentAssetsFor`.
|
|
@@ -743,33 +828,86 @@ async function build() {
|
|
|
743
828
|
failures.push(url);
|
|
744
829
|
emit("page-failed", { url, ...errorEvent(error) });
|
|
745
830
|
};
|
|
831
|
+
//
|
|
832
|
+
// Each prerender runs inside a fill that stores nothing, so what a page states
|
|
833
|
+
// with `cacheLife`, `cacheTag` and `noStore` is something this loop can read
|
|
834
|
+
// rather than a call that throws for want of a scope.
|
|
835
|
+
//
|
|
836
|
+
// A page is written for regeneration when `uf` passed `--regenerate` — `isr`
|
|
837
|
+
// allowed in `app.rendering.modes`, `rendering.cache.route` on, and a server
|
|
838
|
+
// deployed to regenerate it — and the page stated a lifetime, called no
|
|
839
|
+
// `noStore`, and answered 200. Its document goes under
|
|
840
|
+
// `REGENERATED_DIRECTORY` rather than at its own URL, so no static half
|
|
841
|
+
// answers the page: the server does, starting from this document, until the
|
|
842
|
+
// lifetime has passed. What it stated goes into `REGENERATION_FILE` beside the
|
|
843
|
+
// server bundle. Every other page is the document it has always been.
|
|
844
|
+
//
|
|
845
|
+
// A tag without a lifetime is not enough, for the route cache's own reason:
|
|
846
|
+
// an entry with no end is one another process could serve from its memory
|
|
847
|
+
// for ever after `revalidateTag` took it out of the shared store.
|
|
848
|
+
const { collectCacheDeclarations } = await import("@uniflowed/server/cache");
|
|
849
|
+
const regenerate = flag("--regenerate");
|
|
850
|
+
const regenerated = {};
|
|
746
851
|
for (const url of pages) {
|
|
747
|
-
let
|
|
852
|
+
let declared;
|
|
853
|
+
const renderedAt = Date.now();
|
|
748
854
|
try {
|
|
749
|
-
|
|
855
|
+
declared = await collectCacheDeclarations(() => server.prerender(url, assets));
|
|
750
856
|
} catch (error) {
|
|
751
857
|
failed(url, error);
|
|
752
858
|
continue;
|
|
753
859
|
}
|
|
860
|
+
const result = declared.value;
|
|
754
861
|
if (result.error != null) {
|
|
755
862
|
failed(url, result.error);
|
|
756
863
|
continue;
|
|
757
864
|
}
|
|
758
|
-
const
|
|
865
|
+
const lifetime = declared.lifetime;
|
|
866
|
+
const regenerates =
|
|
867
|
+
regenerate && result.status === 200 && declared.denied == null && lifetime != null;
|
|
868
|
+
// The trailing-slash policy decides a served page's file name; a page the
|
|
869
|
+
// build regenerates keeps the one layout the regeneration reads.
|
|
870
|
+
const file = regenerates
|
|
871
|
+
? htmlPathFor(path.join(outDir, REGENERATED_DIRECTORY), url)
|
|
872
|
+
: htmlPathFor(outDir, url, routingRulesOf(config.app?.router).trailingSlash);
|
|
759
873
|
mkdirSync(path.dirname(file), { recursive: true });
|
|
760
874
|
writeFileSync(file, result.html);
|
|
761
875
|
// The payload the document was rendered from, beside it: what a browser
|
|
762
|
-
// navigating to this route fetches, from whatever serves the files.
|
|
876
|
+
// navigating to this route fetches, from whatever serves the files. Beside
|
|
877
|
+
// the document wherever the document went, so a page written for
|
|
878
|
+
// regeneration leaves no file at its route's own payload URL answering with
|
|
879
|
+
// the build's copy for ever; the server answers that URL instead.
|
|
763
880
|
if (result.payload != null) {
|
|
764
|
-
|
|
881
|
+
// Beside the route's directory whatever the document is called:
|
|
882
|
+
// `guide/index.html` and `guide.html` both put it at `guide/__uf.flight`.
|
|
883
|
+
const payloadDirectory =
|
|
884
|
+
path.basename(file) === "index.html" ? path.dirname(file) : file.slice(0, -".html".length);
|
|
885
|
+
mkdirSync(payloadDirectory, { recursive: true });
|
|
886
|
+
writeFileSync(path.join(payloadDirectory, "__uf.flight"), result.payload);
|
|
887
|
+
}
|
|
888
|
+
if (regenerates) {
|
|
889
|
+
regenerated[url] = {
|
|
890
|
+
document: regeneratedDocumentUrl(url),
|
|
891
|
+
renderedAt,
|
|
892
|
+
revalidate: lifetime.revalidate,
|
|
893
|
+
expire: lifetime.expire ?? null,
|
|
894
|
+
tags: declared.tags,
|
|
895
|
+
};
|
|
765
896
|
}
|
|
766
897
|
emit("page", {
|
|
767
898
|
url,
|
|
768
899
|
file: path.relative(root, file),
|
|
769
900
|
status: result.status,
|
|
770
901
|
bytes: Buffer.byteLength(result.html),
|
|
902
|
+
regenerates,
|
|
771
903
|
});
|
|
772
904
|
}
|
|
905
|
+
if (Object.keys(regenerated).length > 0) {
|
|
906
|
+
writeFileSync(
|
|
907
|
+
path.join(serverDir, REGENERATION_FILE),
|
|
908
|
+
`${JSON.stringify({ pages: regenerated }, null, 2)}\n`,
|
|
909
|
+
);
|
|
910
|
+
}
|
|
773
911
|
// One `404.html`, from the boundary at the router root: a static host serves
|
|
774
912
|
// a single error document for the whole site, so the nested boundaries a
|
|
775
913
|
// project declares are the server's and the client's to render, not
|
|
@@ -1150,13 +1288,21 @@ async function compile() {
|
|
|
1150
1288
|
* `static` is deliberately absent; `uf_config`'s
|
|
1151
1289
|
* `DeployAdapter::is_implemented` is the other half of that fact and
|
|
1152
1290
|
* `docs/app/reference/cli/$page.mdx` says why.
|
|
1291
|
+
*
|
|
1292
|
+
* `regenerationStore` is where a target keeps the pages a build regenerates
|
|
1293
|
+
* when `rendering.cache.store` names nothing: a disk for a process with one,
|
|
1294
|
+
* Workers KV for a Worker, and `null` for a Lambda, which keeps nothing between
|
|
1295
|
+
* invocations and is refused by name instead. A regenerated page kept only in
|
|
1296
|
+
* memory would go back to the build's copy on every restart, which is a page
|
|
1297
|
+
* that travels back in time. See [`deploy`].
|
|
1153
1298
|
*/
|
|
1154
1299
|
const ADAPTERS = {
|
|
1155
1300
|
node: {
|
|
1156
|
-
entries: (document, cache, build, schedules) => ({
|
|
1157
|
-
handler: handlerEntrySource(document, cache, NODE_CAPABILITIES, build),
|
|
1301
|
+
entries: (document, cache, build, schedules, regeneration) => ({
|
|
1302
|
+
handler: handlerEntrySource(document, cache, NODE_CAPABILITIES, build, regeneration),
|
|
1158
1303
|
server: nodeEntrySource("./handler.js", schedules),
|
|
1159
1304
|
}),
|
|
1305
|
+
regenerationStore: "filesystem",
|
|
1160
1306
|
},
|
|
1161
1307
|
// The same two files as `node`, with `@uniflowed/server/bun` in place of
|
|
1162
1308
|
// `@uniflowed/server/node`. That module is `./internal/static.js` for every
|
|
@@ -1173,32 +1319,40 @@ const ADAPTERS = {
|
|
|
1173
1319
|
// nobody here. `edge` pays that price because it must: there is no
|
|
1174
1320
|
// `node:stream` in a Worker.
|
|
1175
1321
|
bun: {
|
|
1176
|
-
entries: (document, cache, build, schedules) => ({
|
|
1177
|
-
handler: handlerEntrySource(document, cache, BUN_CAPABILITIES, build),
|
|
1322
|
+
entries: (document, cache, build, schedules, regeneration) => ({
|
|
1323
|
+
handler: handlerEntrySource(document, cache, BUN_CAPABILITIES, build, regeneration),
|
|
1178
1324
|
server: bunEntrySource("./handler.js", schedules),
|
|
1179
1325
|
}),
|
|
1326
|
+
regenerationStore: "filesystem",
|
|
1180
1327
|
},
|
|
1181
1328
|
deno: {
|
|
1182
|
-
entries: (document, cache, build, schedules) => ({
|
|
1183
|
-
handler: handlerEntrySource(document, cache, DENO_CAPABILITIES, build),
|
|
1329
|
+
entries: (document, cache, build, schedules, regeneration) => ({
|
|
1330
|
+
handler: handlerEntrySource(document, cache, DENO_CAPABILITIES, build, regeneration),
|
|
1184
1331
|
server: denoEntrySource("./handler.js", schedules),
|
|
1185
1332
|
}),
|
|
1333
|
+
regenerationStore: "filesystem",
|
|
1186
1334
|
},
|
|
1187
1335
|
// The same two files. What `--adapter container` adds is a `Dockerfile` and
|
|
1188
1336
|
// a `.dockerignore`, and both are plain text that `uf` writes beside this
|
|
1189
1337
|
// output rather than anything the bundler produces — see `uf_cli`'s
|
|
1190
1338
|
// `commands::deploy`.
|
|
1191
1339
|
container: {
|
|
1192
|
-
entries: (document, cache, build, schedules) => ({
|
|
1193
|
-
handler: handlerEntrySource(document, cache, NODE_CAPABILITIES, build),
|
|
1340
|
+
entries: (document, cache, build, schedules, regeneration) => ({
|
|
1341
|
+
handler: handlerEntrySource(document, cache, NODE_CAPABILITIES, build, regeneration),
|
|
1194
1342
|
server: nodeEntrySource("./handler.js", schedules),
|
|
1195
1343
|
}),
|
|
1344
|
+
regenerationStore: "filesystem",
|
|
1196
1345
|
},
|
|
1197
1346
|
edge: {
|
|
1198
|
-
entries: (document, cache, build, schedules) => ({
|
|
1199
|
-
handler: handlerEntrySource(document, cache, EDGE_CAPABILITIES, build),
|
|
1347
|
+
entries: (document, cache, build, schedules, regeneration) => ({
|
|
1348
|
+
handler: handlerEntrySource(document, cache, EDGE_CAPABILITIES, build, regeneration),
|
|
1200
1349
|
worker: workerEntrySource("./handler.js", schedules),
|
|
1201
1350
|
}),
|
|
1351
|
+
// Workers KV, through the same module seam a project's own provider goes
|
|
1352
|
+
// through. `packages/server/cache-kv.js` argues KV over the Cache API: the
|
|
1353
|
+
// seam has to find every entry under a tag, and the Cache API cannot list
|
|
1354
|
+
// what it holds.
|
|
1355
|
+
regenerationStore: "@uniflowed/server/cache/kv",
|
|
1202
1356
|
// `workerd` first, so React resolves to the build that has
|
|
1203
1357
|
// `renderToReadableStream` and no `node:stream`. `browser` and `module`
|
|
1204
1358
|
// after it are Vite's own SSR defaults, kept so a dependency with no
|
|
@@ -1215,10 +1369,14 @@ const ADAPTERS = {
|
|
|
1215
1369
|
workerBuiltins: true,
|
|
1216
1370
|
},
|
|
1217
1371
|
serverless: {
|
|
1218
|
-
entries: (document, cache, build) => ({
|
|
1219
|
-
handler: handlerEntrySource(document, cache, SERVERLESS_CAPABILITIES, build),
|
|
1372
|
+
entries: (document, cache, build, _schedules, regeneration) => ({
|
|
1373
|
+
handler: handlerEntrySource(document, cache, SERVERLESS_CAPABILITIES, build, regeneration),
|
|
1220
1374
|
lambda: lambdaEntrySource("./handler.js"),
|
|
1221
1375
|
}),
|
|
1376
|
+
// A Lambda's memory lasts one instance and its disk is that instance's
|
|
1377
|
+
// `/tmp`, so neither is somewhere a regenerated page survives. A build that
|
|
1378
|
+
// regenerates pages has to name a provider module; see [`deploy`].
|
|
1379
|
+
regenerationStore: null,
|
|
1222
1380
|
},
|
|
1223
1381
|
};
|
|
1224
1382
|
|
|
@@ -1349,8 +1507,8 @@ async function deploy() {
|
|
|
1349
1507
|
// `randomUUID()` here would key the adapter's copy differently from the one
|
|
1350
1508
|
// `uf start` serves out of `.uf/build/`, which is two caches for one build.
|
|
1351
1509
|
const buildId = await buildIdentity(root, path.join(".uf", "build", "server"));
|
|
1352
|
-
const
|
|
1353
|
-
if (shape.filesystem === false &&
|
|
1510
|
+
const declaredCache = config.app?.rendering?.cache;
|
|
1511
|
+
if (shape.filesystem === false && declaredCache?.store === "filesystem") {
|
|
1354
1512
|
throw new Error(
|
|
1355
1513
|
`uf: rendering.cache.store is "filesystem" and \`--adapter ${adapter}\` has no ` +
|
|
1356
1514
|
"filesystem. Name a module exporting `createCacheProvider` instead — a KV " +
|
|
@@ -1358,7 +1516,28 @@ async function deploy() {
|
|
|
1358
1516
|
"docs/app/guide/cache.",
|
|
1359
1517
|
);
|
|
1360
1518
|
}
|
|
1361
|
-
|
|
1519
|
+
// The pages this build regenerates, and where this target keeps what it
|
|
1520
|
+
// regenerates when the project named no store: the adapter's
|
|
1521
|
+
// `regenerationStore`. A target with nowhere refuses by name and lists the
|
|
1522
|
+
// pages, rather than deploying pages that every cold start takes back to the
|
|
1523
|
+
// build's copy.
|
|
1524
|
+
const regeneration = await readRegeneration(path.join(root, ".uf", "build", "server"));
|
|
1525
|
+
let cacheConfig = declaredCache;
|
|
1526
|
+
if (regeneration != null && declaredCache?.store == null) {
|
|
1527
|
+
if (shape.regenerationStore == null) {
|
|
1528
|
+
const pages = Object.keys(regeneration.pages);
|
|
1529
|
+
throw new Error(
|
|
1530
|
+
`uf: this build regenerates ${pages.length} ${plural(pages.length, "page")} ` +
|
|
1531
|
+
`(${pages.join(", ")}), and \`--adapter ${adapter}\` has nowhere of its own to keep ` +
|
|
1532
|
+
"a regenerated page: an instance's memory and its /tmp both go with the instance. " +
|
|
1533
|
+
"Name a module exporting `createCacheProvider` in rendering.cache.store, or leave " +
|
|
1534
|
+
"`isr` out of app.rendering.modes to prerender those pages as documents that do not " +
|
|
1535
|
+
"change. See docs/app/guide/rendering.",
|
|
1536
|
+
);
|
|
1537
|
+
}
|
|
1538
|
+
cacheConfig = { ...declaredCache, store: shape.regenerationStore };
|
|
1539
|
+
}
|
|
1540
|
+
const entries = shape.entries(document, cacheConfig, buildId, schedules, regeneration);
|
|
1362
1541
|
const input = {};
|
|
1363
1542
|
for (const name of Object.keys(entries)) {
|
|
1364
1543
|
writeFileSync(path.join(work, `${name}.js`), entries[name]);
|
|
@@ -1495,7 +1674,7 @@ async function deploy() {
|
|
|
1495
1674
|
* `buildIdentity` is where it came from and
|
|
1496
1675
|
* `packages/server/internal/cache-key.js` is why it exists.
|
|
1497
1676
|
*/
|
|
1498
|
-
function handlerEntrySource(document, cache, capabilities, build) {
|
|
1677
|
+
function handlerEntrySource(document, cache, capabilities, build, regeneration) {
|
|
1499
1678
|
const route = cache?.route === true;
|
|
1500
1679
|
const fetchCache = cache?.fetch === true;
|
|
1501
1680
|
// Nothing at all when both switches are off, so a default project's
|
|
@@ -1509,6 +1688,10 @@ function handlerEntrySource(document, cache, capabilities, build) {
|
|
|
1509
1688
|
`document: ${JSON.stringify(document)}`,
|
|
1510
1689
|
...(store ? ["cache"] : []),
|
|
1511
1690
|
"capabilities",
|
|
1691
|
+
// The pages this build regenerates, baked in beside the document and for
|
|
1692
|
+
// the same reason: the manifest exists on the machine doing the build, and
|
|
1693
|
+
// the deployed directory has only what this file carries.
|
|
1694
|
+
...(store && regeneration != null ? [`regeneration: ${JSON.stringify(regeneration)}`] : []),
|
|
1512
1695
|
].join(", ");
|
|
1513
1696
|
const cacheImport = store ? 'import { createCacheStore } from "@uniflowed/server/cache";\n' : "";
|
|
1514
1697
|
const providerImport = durable == null ? "" : `${durable.import}\n`;
|
|
@@ -1547,8 +1730,11 @@ const capabilities = ${capabilities.name}();
|
|
|
1547
1730
|
|
|
1548
1731
|
export const fetch = createFetchHandler({ ${options} });
|
|
1549
1732
|
export const beginRequest = app.beginRequest;
|
|
1733
|
+
// \`app.router\`'s redirects and headers, for the entry beside this file to put
|
|
1734
|
+
// in front of its static half. Rewrites are \`fetch\`'s own.
|
|
1735
|
+
export const routing = app.routing;
|
|
1550
1736
|
|
|
1551
|
-
export default { fetch, beginRequest };
|
|
1737
|
+
export default { fetch, beginRequest, routing };
|
|
1552
1738
|
`;
|
|
1553
1739
|
}
|
|
1554
1740
|
|
|
@@ -1656,7 +1842,7 @@ ${cron.imports}
|
|
|
1656
1842
|
// \`@uniflowed/server/node\` above, because the request has to be established in
|
|
1657
1843
|
// the storage the *application* reads, which is the copy bundled into
|
|
1658
1844
|
// \`handler.js\`. See ubugeeei-prod/uf#389.
|
|
1659
|
-
import { beginRequest, fetch } from ${JSON.stringify(handlerSpecifier)};
|
|
1845
|
+
import { beginRequest, fetch, routing } from ${JSON.stringify(handlerSpecifier)};
|
|
1660
1846
|
|
|
1661
1847
|
// Resolved from this file and not from the working directory: a process
|
|
1662
1848
|
// manager, a container entrypoint and a person in a shell each start a server
|
|
@@ -1669,7 +1855,7 @@ ${cron.declarations}
|
|
|
1669
1855
|
// (ubugeeei-prod/uf#204) and this entry is a module, so it would work; \`.catch\`
|
|
1670
1856
|
// is the better spelling regardless — a server that cannot take its port should
|
|
1671
1857
|
// say so and exit non-zero, rather than die as an unhandled rejection.
|
|
1672
|
-
serve({ handle: fetch, staticDir, beginRequest${cron.option} }).catch((error) => {
|
|
1858
|
+
serve({ handle: fetch, staticDir, beginRequest, routing${cron.option} }).catch((error) => {
|
|
1673
1859
|
process.stderr.write(\`uf: \${error?.message ?? String(error)}\\n\`);
|
|
1674
1860
|
process.exit(1);
|
|
1675
1861
|
});
|
|
@@ -1696,7 +1882,7 @@ ${cron.imports}
|
|
|
1696
1882
|
// \`@uniflowed/server/bun\` above, because the request has to be established in
|
|
1697
1883
|
// the storage the *application* reads, which is the copy bundled into
|
|
1698
1884
|
// \`handler.js\`. See ubugeeei-prod/uf#389.
|
|
1699
|
-
import { beginRequest, fetch } from ${JSON.stringify(handlerSpecifier)};
|
|
1885
|
+
import { beginRequest, fetch, routing } from ${JSON.stringify(handlerSpecifier)};
|
|
1700
1886
|
|
|
1701
1887
|
// Resolved from this file and not from the working directory: a process
|
|
1702
1888
|
// manager, a container entrypoint and a person in a shell each start a server
|
|
@@ -1705,7 +1891,7 @@ import { beginRequest, fetch } from ${JSON.stringify(handlerSpecifier)};
|
|
|
1705
1891
|
// trap in it.
|
|
1706
1892
|
const staticDir = path.join(path.dirname(fileURLToPath(import.meta.url)), "static");
|
|
1707
1893
|
${cron.declarations}
|
|
1708
|
-
serve({ handle: fetch, staticDir, beginRequest${cron.option} }).catch((error) => {
|
|
1894
|
+
serve({ handle: fetch, staticDir, beginRequest, routing${cron.option} }).catch((error) => {
|
|
1709
1895
|
process.stderr.write(\`uf: \${error?.message ?? String(error)}\\n\`);
|
|
1710
1896
|
process.exit(1);
|
|
1711
1897
|
});
|
|
@@ -1743,7 +1929,7 @@ globalThis.Buffer ??= {
|
|
|
1743
1929
|
};
|
|
1744
1930
|
globalThis.setImmediate ??= (callback, ...args) => setTimeout(callback, 0, ...args);
|
|
1745
1931
|
globalThis.clearImmediate ??= (handle) => clearTimeout(handle);
|
|
1746
|
-
const { beginRequest, fetch } = await import(${JSON.stringify(handlerSpecifier)});
|
|
1932
|
+
const { beginRequest, fetch, routing } = await import(${JSON.stringify(handlerSpecifier)});
|
|
1747
1933
|
|
|
1748
1934
|
// Resolved from this file and not from the working directory: a process
|
|
1749
1935
|
// manager and a person in a shell each start a server from wherever they
|
|
@@ -1751,7 +1937,7 @@ const { beginRequest, fetch } = await import(${JSON.stringify(handlerSpecifier)}
|
|
|
1751
1937
|
// started from inside itself would be a deployment with a trap in it.
|
|
1752
1938
|
const staticDir = decodeURIComponent(new URL("./static", import.meta.url).pathname);
|
|
1753
1939
|
${cron.declarations}
|
|
1754
|
-
serve({ handle: fetch, staticDir, beginRequest${cron.option} }).catch((error) => {
|
|
1940
|
+
serve({ handle: fetch, staticDir, beginRequest, routing${cron.option} }).catch((error) => {
|
|
1755
1941
|
console.error(\`uf: \${error?.message ?? String(error)}\`);
|
|
1756
1942
|
Deno.exit(1);
|
|
1757
1943
|
});
|
|
@@ -1780,13 +1966,13 @@ function workerEntrySource(handlerSpecifier, schedules) {
|
|
|
1780
1966
|
return `// Generated by \`uf build --adapter edge\`. Not checked in, not edited.
|
|
1781
1967
|
import { createWorkerFetch, installWorkerLogger } from "@uniflowed/server/edge";
|
|
1782
1968
|
|
|
1783
|
-
import { beginRequest, fetch as handle } from ${JSON.stringify(handlerSpecifier)};
|
|
1969
|
+
import { beginRequest, fetch as handle, routing } from ${JSON.stringify(handlerSpecifier)};
|
|
1784
1970
|
|
|
1785
1971
|
// After the imports, so a logger the application installed while it loaded is
|
|
1786
1972
|
// the one that stays; see \`installWorkerLogger\`.
|
|
1787
1973
|
installWorkerLogger();
|
|
1788
1974
|
|
|
1789
|
-
export default { fetch: createWorkerFetch({ handle, beginRequest }) };
|
|
1975
|
+
export default { fetch: createWorkerFetch({ handle, beginRequest, routing }) };
|
|
1790
1976
|
`;
|
|
1791
1977
|
}
|
|
1792
1978
|
|
|
@@ -1797,7 +1983,7 @@ export default { fetch: createWorkerFetch({ handle, beginRequest }) };
|
|
|
1797
1983
|
return `// Generated by \`uf build --adapter edge\`. Not checked in, not edited.
|
|
1798
1984
|
import { createWorkerFetch, createWorkerScheduled, installWorkerLogger } from "@uniflowed/server/edge";
|
|
1799
1985
|
|
|
1800
|
-
import { beginRequest, fetch as handle } from ${JSON.stringify(handlerSpecifier)};
|
|
1986
|
+
import { beginRequest, fetch as handle, routing } from ${JSON.stringify(handlerSpecifier)};
|
|
1801
1987
|
|
|
1802
1988
|
// After the imports, so a logger the application installed while it loaded is
|
|
1803
1989
|
// the one that stays; see \`installWorkerLogger\`.
|
|
@@ -1808,7 +1994,7 @@ installWorkerLogger();
|
|
|
1808
1994
|
const routes = ${JSON.stringify(routes, null, 2)};
|
|
1809
1995
|
|
|
1810
1996
|
export default {
|
|
1811
|
-
fetch: createWorkerFetch({ handle, beginRequest }),
|
|
1997
|
+
fetch: createWorkerFetch({ handle, beginRequest, routing }),
|
|
1812
1998
|
scheduled: createWorkerScheduled({ handle, beginRequest, routes }),
|
|
1813
1999
|
};
|
|
1814
2000
|
`;
|
|
@@ -1834,7 +2020,7 @@ import { fileURLToPath } from "node:url";
|
|
|
1834
2020
|
|
|
1835
2021
|
import { createLambdaHandler } from "@uniflowed/server/lambda";
|
|
1836
2022
|
|
|
1837
|
-
import { beginRequest, fetch as handle } from ${JSON.stringify(handlerSpecifier)};
|
|
2023
|
+
import { beginRequest, fetch as handle, routing } from ${JSON.stringify(handlerSpecifier)};
|
|
1838
2024
|
|
|
1839
2025
|
// Resolved from this file and not from the working directory: Lambda sets the
|
|
1840
2026
|
// working directory to the task root today and is under no obligation to keep
|
|
@@ -1842,7 +2028,7 @@ import { beginRequest, fetch as handle } from ${JSON.stringify(handlerSpecifier)
|
|
|
1842
2028
|
// deployment with a trap in it.
|
|
1843
2029
|
const staticDir = path.join(path.dirname(fileURLToPath(import.meta.url)), "static");
|
|
1844
2030
|
|
|
1845
|
-
export const handler = createLambdaHandler({ handle, beginRequest, staticDir });
|
|
2031
|
+
export const handler = createLambdaHandler({ handle, beginRequest, staticDir, routing });
|
|
1846
2032
|
`;
|
|
1847
2033
|
}
|
|
1848
2034
|
|
|
@@ -2019,7 +2205,7 @@ async function buildRscGraph(vite, inline, state, { outDir, conditions }) {
|
|
|
2019
2205
|
* output names its module by path, which only this build's manifest turns
|
|
2020
2206
|
* into a URL.
|
|
2021
2207
|
*/
|
|
2022
|
-
function recordClientChunks(state, manifest, references, rscDir) {
|
|
2208
|
+
function recordClientChunks(state, manifest, references, rscDir, base = "") {
|
|
2023
2209
|
for (const file of references) {
|
|
2024
2210
|
const key = path.relative(root, file).split(path.sep).join("/");
|
|
2025
2211
|
const chunk = manifest[key];
|
|
@@ -2029,7 +2215,7 @@ function recordClientChunks(state, manifest, references, rscDir) {
|
|
|
2029
2215
|
"client component",
|
|
2030
2216
|
);
|
|
2031
2217
|
}
|
|
2032
|
-
state.chunkUrls.set(file,
|
|
2218
|
+
state.chunkUrls.set(file, `${base}/${chunk.file}`);
|
|
2033
2219
|
}
|
|
2034
2220
|
writeFileSync(
|
|
2035
2221
|
path.join(rscDir, FLIGHT_BUILD_FILE),
|
|
@@ -2061,12 +2247,15 @@ function loadFlightBuild(state, rscDir) {
|
|
|
2061
2247
|
* The rsc build's emitted files are copied under `dist/` so those URLs resolve,
|
|
2062
2248
|
* and only its assets: a server bundle's JavaScript is never a deployable file.
|
|
2063
2249
|
*/
|
|
2064
|
-
function flightAssets(manifest, references, rscDir, outDir) {
|
|
2065
|
-
const assets = assetsFromManifest(manifest);
|
|
2250
|
+
function flightAssets(manifest, references, rscDir, outDir, base = "") {
|
|
2251
|
+
const assets = assetsFromManifest(manifest, base);
|
|
2066
2252
|
const styles = new Set();
|
|
2067
2253
|
const rscManifest = path.join(rscDir, ".vite", "manifest.json");
|
|
2068
2254
|
if (existsSync(rscManifest)) {
|
|
2069
|
-
const rscStyles = assetsFromManifest(
|
|
2255
|
+
const rscStyles = assetsFromManifest(
|
|
2256
|
+
JSON.parse(readFileSync(rscManifest, "utf8")),
|
|
2257
|
+
base,
|
|
2258
|
+
).styles;
|
|
2070
2259
|
for (const href of rscStyles) styles.add(href);
|
|
2071
2260
|
}
|
|
2072
2261
|
const rscAssets = path.join(rscDir, "assets");
|
|
@@ -2083,7 +2272,7 @@ function flightAssets(manifest, references, rscDir, outDir) {
|
|
|
2083
2272
|
seen.add(key);
|
|
2084
2273
|
const chunk = manifest[key];
|
|
2085
2274
|
if (chunk == null) return;
|
|
2086
|
-
for (const css of chunk.css ?? []) styles.add(
|
|
2275
|
+
for (const css of chunk.css ?? []) styles.add(`${base}/${css}`);
|
|
2087
2276
|
for (const imported of chunk.imports ?? []) visit(imported);
|
|
2088
2277
|
};
|
|
2089
2278
|
for (const file of references) visit(path.relative(root, file).split(path.sep).join("/"));
|
|
@@ -2225,6 +2414,21 @@ async function renderingPlan(server, prerender) {
|
|
|
2225
2414
|
why: "a middleware guards it, and a middleware runs once per request",
|
|
2226
2415
|
});
|
|
2227
2416
|
}
|
|
2417
|
+
// And `app.router`'s three lists, by the source each rule matches: a file
|
|
2418
|
+
// can be neither a redirect nor a rewrite, and a header a file is served
|
|
2419
|
+
// with is the host's to add rather than the file's.
|
|
2420
|
+
for (const [key, what] of [
|
|
2421
|
+
["redirects", "a redirect"],
|
|
2422
|
+
["rewrites", "a rewrite"],
|
|
2423
|
+
["headers", "a response header"],
|
|
2424
|
+
]) {
|
|
2425
|
+
for (const rule of server.routing?.[key] ?? []) {
|
|
2426
|
+
perRequest.push({
|
|
2427
|
+
path: rule.source,
|
|
2428
|
+
why: `\`app.router.${key}\` names it, and ${what} is answered when a request arrives`,
|
|
2429
|
+
});
|
|
2430
|
+
}
|
|
2431
|
+
}
|
|
2228
2432
|
|
|
2229
2433
|
return { urls, perRequest };
|
|
2230
2434
|
}
|
|
@@ -2246,9 +2450,36 @@ function fillParams(routePath, params) {
|
|
|
2246
2450
|
.join("/");
|
|
2247
2451
|
}
|
|
2248
2452
|
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2453
|
+
/**
|
|
2454
|
+
* The file a prerendered page is written to.
|
|
2455
|
+
*
|
|
2456
|
+
* `guide/index.html`, which every static host serves at `/guide/` and most at
|
|
2457
|
+
* `/guide`, unless `app.router.trailingSlash` is `"never"` — then `guide.html`,
|
|
2458
|
+
* which the same hosts serve at `/guide` without a redirect to the slash.
|
|
2459
|
+
* Next.js's static export makes the same choice from the same setting.
|
|
2460
|
+
*/
|
|
2461
|
+
function htmlPathFor(outDir, url, trailingSlash = "ignore") {
|
|
2462
|
+
const pathname = url.split("?")[0].replace(/^\/+/, "").replace(/\/+$/, "");
|
|
2463
|
+
if (pathname === "") return path.join(outDir, "index.html");
|
|
2464
|
+
return trailingSlash === "never"
|
|
2465
|
+
? path.join(outDir, `${pathname}.html`)
|
|
2253
2466
|
: path.join(outDir, pathname, "index.html");
|
|
2254
2467
|
}
|
|
2468
|
+
|
|
2469
|
+
/**
|
|
2470
|
+
* The URL path at which a static half answers the document `htmlPathFor`
|
|
2471
|
+
* wrote for `url` under `REGENERATED_DIRECTORY`.
|
|
2472
|
+
*
|
|
2473
|
+
* Ending in a slash, so every host answers it with that directory's
|
|
2474
|
+
* `index.html` in the same way: a Node static half tries `index.html` for such
|
|
2475
|
+
* a path, and a Worker's assets binding serves it without the redirect it
|
|
2476
|
+
* answers `…/index.html` with.
|
|
2477
|
+
*/
|
|
2478
|
+
function regeneratedDocumentUrl(url) {
|
|
2479
|
+
const pathname = url.split("?")[0].replace(/^\/+/, "").replace(/\/+$/, "");
|
|
2480
|
+
const encoded = pathname
|
|
2481
|
+
.split("/")
|
|
2482
|
+
.map((segment) => encodeURIComponent(segment))
|
|
2483
|
+
.join("/");
|
|
2484
|
+
return pathname === "" ? `/${REGENERATED_DIRECTORY}/` : `/${REGENERATED_DIRECTORY}/${encoded}/`;
|
|
2485
|
+
}
|