@uniflowed/vite 0.0.0-alpha.35 → 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 +263 -50
- package/index.js +132 -15
- package/internal/barrel-imports.js +459 -0
- package/internal/flight.js +125 -11
- package/internal/http.js +21 -2
- package/internal/module-graph.js +134 -0
- package/internal/routes.js +55 -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,9 @@ async function viteConfig(config, mode) {
|
|
|
199
228
|
configFile: false,
|
|
200
229
|
envDir: false,
|
|
201
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)}/`,
|
|
202
234
|
clearScreen: false,
|
|
203
235
|
customLogger: eventLogger(argument("--log-level") ?? "info"),
|
|
204
236
|
plugins: [uniflowed({ root, config, target: routeTarget })],
|
|
@@ -445,6 +477,26 @@ async function preview() {
|
|
|
445
477
|
const draftFirst = {
|
|
446
478
|
name: "uf:draft-before-files",
|
|
447
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
|
+
}
|
|
448
500
|
// A prerendered payload is a file whose extension Vite's static middleware
|
|
449
501
|
// knows no type for, and the router hands bytes to React only when they
|
|
450
502
|
// are answered as a payload — so a navigation on a preview would silently
|
|
@@ -613,6 +665,18 @@ async function build() {
|
|
|
613
665
|
// First, because it is what finds the client modules the next pass has
|
|
614
666
|
// to build; `./internal/flight.js` has the order and the reason for it.
|
|
615
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
|
+
}
|
|
616
680
|
const rscDir = path.join(root, ".uf", "build", "rsc");
|
|
617
681
|
if (flight != null) {
|
|
618
682
|
emit("phase", { name: "rsc" });
|
|
@@ -640,7 +704,7 @@ async function build() {
|
|
|
640
704
|
});
|
|
641
705
|
const manifest = readManifest(outDir);
|
|
642
706
|
if (flight != null) {
|
|
643
|
-
recordClientChunks(flight, manifest, references, rscDir);
|
|
707
|
+
recordClientChunks(flight, manifest, references, rscDir, basePathOf(config));
|
|
644
708
|
// What the summary's "pages in the client bundle" reads. None: a browser
|
|
645
709
|
// that hydrates a payload imports no page, whichever route it is on.
|
|
646
710
|
emit("rsc-split", {
|
|
@@ -677,6 +741,9 @@ async function build() {
|
|
|
677
741
|
// `internal/serve.js`'s `buildIdentity`, which is what reads this.
|
|
678
742
|
writeFileSync(path.join(serverDir, BUILD_ID_FILE), `${mintBuildId()}\n`);
|
|
679
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
|
+
|
|
680
747
|
// 3. Which routes this build renders when, and every route it renders now.
|
|
681
748
|
//
|
|
682
749
|
// The decision comes from `uf.config.js` and is made in Rust — see
|
|
@@ -687,8 +754,8 @@ async function build() {
|
|
|
687
754
|
const server = await import(pathToFileURL(path.join(serverDir, "server.js")).href);
|
|
688
755
|
const assets =
|
|
689
756
|
flight == null
|
|
690
|
-
? assetsFromManifest(manifest)
|
|
691
|
-
: flightAssets(manifest, references, rscDir, outDir);
|
|
757
|
+
? assetsFromManifest(manifest, basePathOf(config))
|
|
758
|
+
: flightAssets(manifest, references, rscDir, outDir, basePathOf(config));
|
|
692
759
|
// Recorded beside the server bundle, because whatever serves this build
|
|
693
760
|
// later cannot recompute them from the client manifest alone; see
|
|
694
761
|
// `documentAssetsFor`.
|
|
@@ -743,33 +810,86 @@ async function build() {
|
|
|
743
810
|
failures.push(url);
|
|
744
811
|
emit("page-failed", { url, ...errorEvent(error) });
|
|
745
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 = {};
|
|
746
833
|
for (const url of pages) {
|
|
747
|
-
let
|
|
834
|
+
let declared;
|
|
835
|
+
const renderedAt = Date.now();
|
|
748
836
|
try {
|
|
749
|
-
|
|
837
|
+
declared = await collectCacheDeclarations(() => server.prerender(url, assets));
|
|
750
838
|
} catch (error) {
|
|
751
839
|
failed(url, error);
|
|
752
840
|
continue;
|
|
753
841
|
}
|
|
842
|
+
const result = declared.value;
|
|
754
843
|
if (result.error != null) {
|
|
755
844
|
failed(url, result.error);
|
|
756
845
|
continue;
|
|
757
846
|
}
|
|
758
|
-
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);
|
|
759
855
|
mkdirSync(path.dirname(file), { recursive: true });
|
|
760
856
|
writeFileSync(file, result.html);
|
|
761
857
|
// The payload the document was rendered from, beside it: what a browser
|
|
762
|
-
// navigating to this route fetches, from whatever serves the files.
|
|
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.
|
|
763
862
|
if (result.payload != null) {
|
|
764
|
-
|
|
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
|
+
};
|
|
765
878
|
}
|
|
766
879
|
emit("page", {
|
|
767
880
|
url,
|
|
768
881
|
file: path.relative(root, file),
|
|
769
882
|
status: result.status,
|
|
770
883
|
bytes: Buffer.byteLength(result.html),
|
|
884
|
+
regenerates,
|
|
771
885
|
});
|
|
772
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
|
+
}
|
|
773
893
|
// One `404.html`, from the boundary at the router root: a static host serves
|
|
774
894
|
// a single error document for the whole site, so the nested boundaries a
|
|
775
895
|
// project declares are the server's and the client's to render, not
|
|
@@ -1150,13 +1270,21 @@ async function compile() {
|
|
|
1150
1270
|
* `static` is deliberately absent; `uf_config`'s
|
|
1151
1271
|
* `DeployAdapter::is_implemented` is the other half of that fact and
|
|
1152
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`].
|
|
1153
1280
|
*/
|
|
1154
1281
|
const ADAPTERS = {
|
|
1155
1282
|
node: {
|
|
1156
|
-
entries: (document, cache, build, schedules) => ({
|
|
1157
|
-
handler: handlerEntrySource(document, cache, NODE_CAPABILITIES, build),
|
|
1283
|
+
entries: (document, cache, build, schedules, regeneration) => ({
|
|
1284
|
+
handler: handlerEntrySource(document, cache, NODE_CAPABILITIES, build, regeneration),
|
|
1158
1285
|
server: nodeEntrySource("./handler.js", schedules),
|
|
1159
1286
|
}),
|
|
1287
|
+
regenerationStore: "filesystem",
|
|
1160
1288
|
},
|
|
1161
1289
|
// The same two files as `node`, with `@uniflowed/server/bun` in place of
|
|
1162
1290
|
// `@uniflowed/server/node`. That module is `./internal/static.js` for every
|
|
@@ -1173,32 +1301,40 @@ const ADAPTERS = {
|
|
|
1173
1301
|
// nobody here. `edge` pays that price because it must: there is no
|
|
1174
1302
|
// `node:stream` in a Worker.
|
|
1175
1303
|
bun: {
|
|
1176
|
-
entries: (document, cache, build, schedules) => ({
|
|
1177
|
-
handler: handlerEntrySource(document, cache, BUN_CAPABILITIES, build),
|
|
1304
|
+
entries: (document, cache, build, schedules, regeneration) => ({
|
|
1305
|
+
handler: handlerEntrySource(document, cache, BUN_CAPABILITIES, build, regeneration),
|
|
1178
1306
|
server: bunEntrySource("./handler.js", schedules),
|
|
1179
1307
|
}),
|
|
1308
|
+
regenerationStore: "filesystem",
|
|
1180
1309
|
},
|
|
1181
1310
|
deno: {
|
|
1182
|
-
entries: (document, cache, build, schedules) => ({
|
|
1183
|
-
handler: handlerEntrySource(document, cache, DENO_CAPABILITIES, build),
|
|
1311
|
+
entries: (document, cache, build, schedules, regeneration) => ({
|
|
1312
|
+
handler: handlerEntrySource(document, cache, DENO_CAPABILITIES, build, regeneration),
|
|
1184
1313
|
server: denoEntrySource("./handler.js", schedules),
|
|
1185
1314
|
}),
|
|
1315
|
+
regenerationStore: "filesystem",
|
|
1186
1316
|
},
|
|
1187
1317
|
// The same two files. What `--adapter container` adds is a `Dockerfile` and
|
|
1188
1318
|
// a `.dockerignore`, and both are plain text that `uf` writes beside this
|
|
1189
1319
|
// output rather than anything the bundler produces — see `uf_cli`'s
|
|
1190
1320
|
// `commands::deploy`.
|
|
1191
1321
|
container: {
|
|
1192
|
-
entries: (document, cache, build, schedules) => ({
|
|
1193
|
-
handler: handlerEntrySource(document, cache, NODE_CAPABILITIES, build),
|
|
1322
|
+
entries: (document, cache, build, schedules, regeneration) => ({
|
|
1323
|
+
handler: handlerEntrySource(document, cache, NODE_CAPABILITIES, build, regeneration),
|
|
1194
1324
|
server: nodeEntrySource("./handler.js", schedules),
|
|
1195
1325
|
}),
|
|
1326
|
+
regenerationStore: "filesystem",
|
|
1196
1327
|
},
|
|
1197
1328
|
edge: {
|
|
1198
|
-
entries: (document, cache, build, schedules) => ({
|
|
1199
|
-
handler: handlerEntrySource(document, cache, EDGE_CAPABILITIES, build),
|
|
1329
|
+
entries: (document, cache, build, schedules, regeneration) => ({
|
|
1330
|
+
handler: handlerEntrySource(document, cache, EDGE_CAPABILITIES, build, regeneration),
|
|
1200
1331
|
worker: workerEntrySource("./handler.js", schedules),
|
|
1201
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",
|
|
1202
1338
|
// `workerd` first, so React resolves to the build that has
|
|
1203
1339
|
// `renderToReadableStream` and no `node:stream`. `browser` and `module`
|
|
1204
1340
|
// after it are Vite's own SSR defaults, kept so a dependency with no
|
|
@@ -1215,10 +1351,14 @@ const ADAPTERS = {
|
|
|
1215
1351
|
workerBuiltins: true,
|
|
1216
1352
|
},
|
|
1217
1353
|
serverless: {
|
|
1218
|
-
entries: (document, cache, build) => ({
|
|
1219
|
-
handler: handlerEntrySource(document, cache, SERVERLESS_CAPABILITIES, build),
|
|
1354
|
+
entries: (document, cache, build, _schedules, regeneration) => ({
|
|
1355
|
+
handler: handlerEntrySource(document, cache, SERVERLESS_CAPABILITIES, build, regeneration),
|
|
1220
1356
|
lambda: lambdaEntrySource("./handler.js"),
|
|
1221
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,
|
|
1222
1362
|
},
|
|
1223
1363
|
};
|
|
1224
1364
|
|
|
@@ -1349,8 +1489,8 @@ async function deploy() {
|
|
|
1349
1489
|
// `randomUUID()` here would key the adapter's copy differently from the one
|
|
1350
1490
|
// `uf start` serves out of `.uf/build/`, which is two caches for one build.
|
|
1351
1491
|
const buildId = await buildIdentity(root, path.join(".uf", "build", "server"));
|
|
1352
|
-
const
|
|
1353
|
-
if (shape.filesystem === false &&
|
|
1492
|
+
const declaredCache = config.app?.rendering?.cache;
|
|
1493
|
+
if (shape.filesystem === false && declaredCache?.store === "filesystem") {
|
|
1354
1494
|
throw new Error(
|
|
1355
1495
|
`uf: rendering.cache.store is "filesystem" and \`--adapter ${adapter}\` has no ` +
|
|
1356
1496
|
"filesystem. Name a module exporting `createCacheProvider` instead — a KV " +
|
|
@@ -1358,7 +1498,28 @@ async function deploy() {
|
|
|
1358
1498
|
"docs/app/guide/cache.",
|
|
1359
1499
|
);
|
|
1360
1500
|
}
|
|
1361
|
-
|
|
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);
|
|
1362
1523
|
const input = {};
|
|
1363
1524
|
for (const name of Object.keys(entries)) {
|
|
1364
1525
|
writeFileSync(path.join(work, `${name}.js`), entries[name]);
|
|
@@ -1495,7 +1656,7 @@ async function deploy() {
|
|
|
1495
1656
|
* `buildIdentity` is where it came from and
|
|
1496
1657
|
* `packages/server/internal/cache-key.js` is why it exists.
|
|
1497
1658
|
*/
|
|
1498
|
-
function handlerEntrySource(document, cache, capabilities, build) {
|
|
1659
|
+
function handlerEntrySource(document, cache, capabilities, build, regeneration) {
|
|
1499
1660
|
const route = cache?.route === true;
|
|
1500
1661
|
const fetchCache = cache?.fetch === true;
|
|
1501
1662
|
// Nothing at all when both switches are off, so a default project's
|
|
@@ -1509,6 +1670,10 @@ function handlerEntrySource(document, cache, capabilities, build) {
|
|
|
1509
1670
|
`document: ${JSON.stringify(document)}`,
|
|
1510
1671
|
...(store ? ["cache"] : []),
|
|
1511
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)}`] : []),
|
|
1512
1677
|
].join(", ");
|
|
1513
1678
|
const cacheImport = store ? 'import { createCacheStore } from "@uniflowed/server/cache";\n' : "";
|
|
1514
1679
|
const providerImport = durable == null ? "" : `${durable.import}\n`;
|
|
@@ -1547,8 +1712,11 @@ const capabilities = ${capabilities.name}();
|
|
|
1547
1712
|
|
|
1548
1713
|
export const fetch = createFetchHandler({ ${options} });
|
|
1549
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;
|
|
1550
1718
|
|
|
1551
|
-
export default { fetch, beginRequest };
|
|
1719
|
+
export default { fetch, beginRequest, routing };
|
|
1552
1720
|
`;
|
|
1553
1721
|
}
|
|
1554
1722
|
|
|
@@ -1656,7 +1824,7 @@ ${cron.imports}
|
|
|
1656
1824
|
// \`@uniflowed/server/node\` above, because the request has to be established in
|
|
1657
1825
|
// the storage the *application* reads, which is the copy bundled into
|
|
1658
1826
|
// \`handler.js\`. See ubugeeei-prod/uf#389.
|
|
1659
|
-
import { beginRequest, fetch } from ${JSON.stringify(handlerSpecifier)};
|
|
1827
|
+
import { beginRequest, fetch, routing } from ${JSON.stringify(handlerSpecifier)};
|
|
1660
1828
|
|
|
1661
1829
|
// Resolved from this file and not from the working directory: a process
|
|
1662
1830
|
// manager, a container entrypoint and a person in a shell each start a server
|
|
@@ -1669,7 +1837,7 @@ ${cron.declarations}
|
|
|
1669
1837
|
// (ubugeeei-prod/uf#204) and this entry is a module, so it would work; \`.catch\`
|
|
1670
1838
|
// is the better spelling regardless — a server that cannot take its port should
|
|
1671
1839
|
// say so and exit non-zero, rather than die as an unhandled rejection.
|
|
1672
|
-
serve({ handle: fetch, staticDir, beginRequest${cron.option} }).catch((error) => {
|
|
1840
|
+
serve({ handle: fetch, staticDir, beginRequest, routing${cron.option} }).catch((error) => {
|
|
1673
1841
|
process.stderr.write(\`uf: \${error?.message ?? String(error)}\\n\`);
|
|
1674
1842
|
process.exit(1);
|
|
1675
1843
|
});
|
|
@@ -1696,7 +1864,7 @@ ${cron.imports}
|
|
|
1696
1864
|
// \`@uniflowed/server/bun\` above, because the request has to be established in
|
|
1697
1865
|
// the storage the *application* reads, which is the copy bundled into
|
|
1698
1866
|
// \`handler.js\`. See ubugeeei-prod/uf#389.
|
|
1699
|
-
import { beginRequest, fetch } from ${JSON.stringify(handlerSpecifier)};
|
|
1867
|
+
import { beginRequest, fetch, routing } from ${JSON.stringify(handlerSpecifier)};
|
|
1700
1868
|
|
|
1701
1869
|
// Resolved from this file and not from the working directory: a process
|
|
1702
1870
|
// manager, a container entrypoint and a person in a shell each start a server
|
|
@@ -1705,7 +1873,7 @@ import { beginRequest, fetch } from ${JSON.stringify(handlerSpecifier)};
|
|
|
1705
1873
|
// trap in it.
|
|
1706
1874
|
const staticDir = path.join(path.dirname(fileURLToPath(import.meta.url)), "static");
|
|
1707
1875
|
${cron.declarations}
|
|
1708
|
-
serve({ handle: fetch, staticDir, beginRequest${cron.option} }).catch((error) => {
|
|
1876
|
+
serve({ handle: fetch, staticDir, beginRequest, routing${cron.option} }).catch((error) => {
|
|
1709
1877
|
process.stderr.write(\`uf: \${error?.message ?? String(error)}\\n\`);
|
|
1710
1878
|
process.exit(1);
|
|
1711
1879
|
});
|
|
@@ -1743,7 +1911,7 @@ globalThis.Buffer ??= {
|
|
|
1743
1911
|
};
|
|
1744
1912
|
globalThis.setImmediate ??= (callback, ...args) => setTimeout(callback, 0, ...args);
|
|
1745
1913
|
globalThis.clearImmediate ??= (handle) => clearTimeout(handle);
|
|
1746
|
-
const { beginRequest, fetch } = await import(${JSON.stringify(handlerSpecifier)});
|
|
1914
|
+
const { beginRequest, fetch, routing } = await import(${JSON.stringify(handlerSpecifier)});
|
|
1747
1915
|
|
|
1748
1916
|
// Resolved from this file and not from the working directory: a process
|
|
1749
1917
|
// manager and a person in a shell each start a server from wherever they
|
|
@@ -1751,7 +1919,7 @@ const { beginRequest, fetch } = await import(${JSON.stringify(handlerSpecifier)}
|
|
|
1751
1919
|
// started from inside itself would be a deployment with a trap in it.
|
|
1752
1920
|
const staticDir = decodeURIComponent(new URL("./static", import.meta.url).pathname);
|
|
1753
1921
|
${cron.declarations}
|
|
1754
|
-
serve({ handle: fetch, staticDir, beginRequest${cron.option} }).catch((error) => {
|
|
1922
|
+
serve({ handle: fetch, staticDir, beginRequest, routing${cron.option} }).catch((error) => {
|
|
1755
1923
|
console.error(\`uf: \${error?.message ?? String(error)}\`);
|
|
1756
1924
|
Deno.exit(1);
|
|
1757
1925
|
});
|
|
@@ -1780,13 +1948,13 @@ function workerEntrySource(handlerSpecifier, schedules) {
|
|
|
1780
1948
|
return `// Generated by \`uf build --adapter edge\`. Not checked in, not edited.
|
|
1781
1949
|
import { createWorkerFetch, installWorkerLogger } from "@uniflowed/server/edge";
|
|
1782
1950
|
|
|
1783
|
-
import { beginRequest, fetch as handle } from ${JSON.stringify(handlerSpecifier)};
|
|
1951
|
+
import { beginRequest, fetch as handle, routing } from ${JSON.stringify(handlerSpecifier)};
|
|
1784
1952
|
|
|
1785
1953
|
// After the imports, so a logger the application installed while it loaded is
|
|
1786
1954
|
// the one that stays; see \`installWorkerLogger\`.
|
|
1787
1955
|
installWorkerLogger();
|
|
1788
1956
|
|
|
1789
|
-
export default { fetch: createWorkerFetch({ handle, beginRequest }) };
|
|
1957
|
+
export default { fetch: createWorkerFetch({ handle, beginRequest, routing }) };
|
|
1790
1958
|
`;
|
|
1791
1959
|
}
|
|
1792
1960
|
|
|
@@ -1797,7 +1965,7 @@ export default { fetch: createWorkerFetch({ handle, beginRequest }) };
|
|
|
1797
1965
|
return `// Generated by \`uf build --adapter edge\`. Not checked in, not edited.
|
|
1798
1966
|
import { createWorkerFetch, createWorkerScheduled, installWorkerLogger } from "@uniflowed/server/edge";
|
|
1799
1967
|
|
|
1800
|
-
import { beginRequest, fetch as handle } from ${JSON.stringify(handlerSpecifier)};
|
|
1968
|
+
import { beginRequest, fetch as handle, routing } from ${JSON.stringify(handlerSpecifier)};
|
|
1801
1969
|
|
|
1802
1970
|
// After the imports, so a logger the application installed while it loaded is
|
|
1803
1971
|
// the one that stays; see \`installWorkerLogger\`.
|
|
@@ -1808,7 +1976,7 @@ installWorkerLogger();
|
|
|
1808
1976
|
const routes = ${JSON.stringify(routes, null, 2)};
|
|
1809
1977
|
|
|
1810
1978
|
export default {
|
|
1811
|
-
fetch: createWorkerFetch({ handle, beginRequest }),
|
|
1979
|
+
fetch: createWorkerFetch({ handle, beginRequest, routing }),
|
|
1812
1980
|
scheduled: createWorkerScheduled({ handle, beginRequest, routes }),
|
|
1813
1981
|
};
|
|
1814
1982
|
`;
|
|
@@ -1834,7 +2002,7 @@ import { fileURLToPath } from "node:url";
|
|
|
1834
2002
|
|
|
1835
2003
|
import { createLambdaHandler } from "@uniflowed/server/lambda";
|
|
1836
2004
|
|
|
1837
|
-
import { beginRequest, fetch as handle } from ${JSON.stringify(handlerSpecifier)};
|
|
2005
|
+
import { beginRequest, fetch as handle, routing } from ${JSON.stringify(handlerSpecifier)};
|
|
1838
2006
|
|
|
1839
2007
|
// Resolved from this file and not from the working directory: Lambda sets the
|
|
1840
2008
|
// working directory to the task root today and is under no obligation to keep
|
|
@@ -1842,7 +2010,7 @@ import { beginRequest, fetch as handle } from ${JSON.stringify(handlerSpecifier)
|
|
|
1842
2010
|
// deployment with a trap in it.
|
|
1843
2011
|
const staticDir = path.join(path.dirname(fileURLToPath(import.meta.url)), "static");
|
|
1844
2012
|
|
|
1845
|
-
export const handler = createLambdaHandler({ handle, beginRequest, staticDir });
|
|
2013
|
+
export const handler = createLambdaHandler({ handle, beginRequest, staticDir, routing });
|
|
1846
2014
|
`;
|
|
1847
2015
|
}
|
|
1848
2016
|
|
|
@@ -2019,7 +2187,7 @@ async function buildRscGraph(vite, inline, state, { outDir, conditions }) {
|
|
|
2019
2187
|
* output names its module by path, which only this build's manifest turns
|
|
2020
2188
|
* into a URL.
|
|
2021
2189
|
*/
|
|
2022
|
-
function recordClientChunks(state, manifest, references, rscDir) {
|
|
2190
|
+
function recordClientChunks(state, manifest, references, rscDir, base = "") {
|
|
2023
2191
|
for (const file of references) {
|
|
2024
2192
|
const key = path.relative(root, file).split(path.sep).join("/");
|
|
2025
2193
|
const chunk = manifest[key];
|
|
@@ -2029,7 +2197,7 @@ function recordClientChunks(state, manifest, references, rscDir) {
|
|
|
2029
2197
|
"client component",
|
|
2030
2198
|
);
|
|
2031
2199
|
}
|
|
2032
|
-
state.chunkUrls.set(file,
|
|
2200
|
+
state.chunkUrls.set(file, `${base}/${chunk.file}`);
|
|
2033
2201
|
}
|
|
2034
2202
|
writeFileSync(
|
|
2035
2203
|
path.join(rscDir, FLIGHT_BUILD_FILE),
|
|
@@ -2061,12 +2229,15 @@ function loadFlightBuild(state, rscDir) {
|
|
|
2061
2229
|
* The rsc build's emitted files are copied under `dist/` so those URLs resolve,
|
|
2062
2230
|
* and only its assets: a server bundle's JavaScript is never a deployable file.
|
|
2063
2231
|
*/
|
|
2064
|
-
function flightAssets(manifest, references, rscDir, outDir) {
|
|
2065
|
-
const assets = assetsFromManifest(manifest);
|
|
2232
|
+
function flightAssets(manifest, references, rscDir, outDir, base = "") {
|
|
2233
|
+
const assets = assetsFromManifest(manifest, base);
|
|
2066
2234
|
const styles = new Set();
|
|
2067
2235
|
const rscManifest = path.join(rscDir, ".vite", "manifest.json");
|
|
2068
2236
|
if (existsSync(rscManifest)) {
|
|
2069
|
-
const rscStyles = assetsFromManifest(
|
|
2237
|
+
const rscStyles = assetsFromManifest(
|
|
2238
|
+
JSON.parse(readFileSync(rscManifest, "utf8")),
|
|
2239
|
+
base,
|
|
2240
|
+
).styles;
|
|
2070
2241
|
for (const href of rscStyles) styles.add(href);
|
|
2071
2242
|
}
|
|
2072
2243
|
const rscAssets = path.join(rscDir, "assets");
|
|
@@ -2083,7 +2254,7 @@ function flightAssets(manifest, references, rscDir, outDir) {
|
|
|
2083
2254
|
seen.add(key);
|
|
2084
2255
|
const chunk = manifest[key];
|
|
2085
2256
|
if (chunk == null) return;
|
|
2086
|
-
for (const css of chunk.css ?? []) styles.add(
|
|
2257
|
+
for (const css of chunk.css ?? []) styles.add(`${base}/${css}`);
|
|
2087
2258
|
for (const imported of chunk.imports ?? []) visit(imported);
|
|
2088
2259
|
};
|
|
2089
2260
|
for (const file of references) visit(path.relative(root, file).split(path.sep).join("/"));
|
|
@@ -2225,6 +2396,21 @@ async function renderingPlan(server, prerender) {
|
|
|
2225
2396
|
why: "a middleware guards it, and a middleware runs once per request",
|
|
2226
2397
|
});
|
|
2227
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
|
+
}
|
|
2228
2414
|
|
|
2229
2415
|
return { urls, perRequest };
|
|
2230
2416
|
}
|
|
@@ -2246,9 +2432,36 @@ function fillParams(routePath, params) {
|
|
|
2246
2432
|
.join("/");
|
|
2247
2433
|
}
|
|
2248
2434
|
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
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`)
|
|
2253
2448
|
: path.join(outDir, pathname, "index.html");
|
|
2254
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
|
+
}
|