@uniflowed/vite 0.0.0-alpha.17 → 0.0.0-alpha.20
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 +356 -27
- package/index.js +102 -16
- package/internal/a11y.js +6 -4
- package/internal/devtools.js +2 -2
- package/internal/diagnostics.js +2 -2
- package/internal/flow-keywords.js +1 -1
- package/internal/routes.js +457 -54
- package/internal/rsc.js +38 -4
- package/internal/serve.js +158 -28
- package/package.json +5 -4
package/driver.js
CHANGED
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
// Rust side reads a config that may hold functions and plugin instances: the
|
|
40
40
|
// one host that can evaluate the file evaluates it.
|
|
41
41
|
|
|
42
|
+
import { randomUUID } from "node:crypto";
|
|
42
43
|
import { createServer as createHttpServer } from "node:http";
|
|
43
44
|
import { builtinModules, register } from "node:module";
|
|
44
45
|
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
@@ -52,11 +53,14 @@ import { send, toRequest } from "./internal/http.js";
|
|
|
52
53
|
import { withProjectConfig } from "./merge.js";
|
|
53
54
|
import { VIRTUAL, scanRoutes } from "./internal/routes.js";
|
|
54
55
|
import {
|
|
56
|
+
BUILD_ID_FILE,
|
|
55
57
|
assetsFromManifest,
|
|
58
|
+
buildIdentity,
|
|
56
59
|
createPrerenderGate,
|
|
57
60
|
createServeHandler,
|
|
58
61
|
loadBuild,
|
|
59
62
|
nodeListener,
|
|
63
|
+
providerSpecifier,
|
|
60
64
|
withRequest,
|
|
61
65
|
} from "./internal/serve.js";
|
|
62
66
|
|
|
@@ -113,6 +117,33 @@ run().catch((error) => {
|
|
|
113
117
|
process.exit(1);
|
|
114
118
|
});
|
|
115
119
|
|
|
120
|
+
/**
|
|
121
|
+
* What this build is called, for anything that outlives it.
|
|
122
|
+
*
|
|
123
|
+
* `UF_BUILD_ID` when it is set, and a fresh random name otherwise — which is
|
|
124
|
+
* `crates/uf_rsc`'s `BuildId::from_env_or_generate` exactly, reading the same
|
|
125
|
+
* variable, because it is the same question asked by a different half of the
|
|
126
|
+
* toolchain. Two artefacts that have to *be* one build say so with the
|
|
127
|
+
* variable; everything else gets a name no other build has.
|
|
128
|
+
*
|
|
129
|
+
* Minted once per build and written into the build, never per process. Four
|
|
130
|
+
* servers started from one artefact are one build and must share one cache;
|
|
131
|
+
* generating this where the server starts would give them four, which is worse
|
|
132
|
+
* than none at all — four copies of everything written into one directory and
|
|
133
|
+
* none of them read.
|
|
134
|
+
*
|
|
135
|
+
* Not a hash of the output. A content hash would be reproducible, which is
|
|
136
|
+
* appealing and wrong here: two builds with identical client bundles can have
|
|
137
|
+
* different server behaviour — a loader's body moves and no asset hash does —
|
|
138
|
+
* and a cache keyed by one would serve the old loader's documents. A name that
|
|
139
|
+
* changes whenever the build ran is the conservative direction to be wrong in.
|
|
140
|
+
*/
|
|
141
|
+
function mintBuildId() {
|
|
142
|
+
const named = process.env.UF_BUILD_ID;
|
|
143
|
+
if (typeof named === "string" && named.trim() !== "") return named.trim();
|
|
144
|
+
return randomUUID().replaceAll("-", "");
|
|
145
|
+
}
|
|
146
|
+
|
|
116
147
|
/** Load `uf.config.js`, reporting where it was found. */
|
|
117
148
|
async function loadConfig() {
|
|
118
149
|
const { config, file } = await loadUfConfig(root);
|
|
@@ -423,6 +454,30 @@ async function preview() {
|
|
|
423
454
|
if (handle != null) {
|
|
424
455
|
server.middlewares.use(answer(server));
|
|
425
456
|
}
|
|
457
|
+
// The rewrite rule a single-page deployment needs, in the one place uf can
|
|
458
|
+
// apply one. A `["csr"]` build writes `index.html` and nothing else that is a
|
|
459
|
+
// page, so a file server answers `/` and 404s every other URL — and this
|
|
460
|
+
// preview exists to be believed about the deployment. A host serving that
|
|
461
|
+
// build has to send unmatched paths to the shell, so a preview that did not
|
|
462
|
+
// would be right about a deployment nobody is doing.
|
|
463
|
+
//
|
|
464
|
+
// Behind the static middleware, which is what makes it a *fallback*: a real
|
|
465
|
+
// file still wins, so `/assets/client.js` is still the chunk and not the
|
|
466
|
+
// shell. And `Accept: text/html` only, so a `fetch` for a missing JSON file
|
|
467
|
+
// gets a 404 rather than a document — the failure mode of a fallback that
|
|
468
|
+
// answers everything is a parse error two layers away from the missing file.
|
|
469
|
+
if (flag("--spa-fallback")) {
|
|
470
|
+
const shell = path.resolve(root, inline.build.outDir, "index.html");
|
|
471
|
+
server.middlewares.use((request, response, next) => {
|
|
472
|
+
if (request.method !== "GET" && request.method !== "HEAD") return next();
|
|
473
|
+
if (!(request.headers.accept ?? "").includes("text/html")) return next();
|
|
474
|
+
if (!existsSync(shell)) return next();
|
|
475
|
+
response.statusCode = 200;
|
|
476
|
+
response.setHeader("content-type", "text/html; charset=utf-8");
|
|
477
|
+
response.end(request.method === "HEAD" ? undefined : readFileSync(shell));
|
|
478
|
+
return undefined;
|
|
479
|
+
});
|
|
480
|
+
}
|
|
426
481
|
|
|
427
482
|
const urls = server.resolvedUrls ?? { local: [], network: [] };
|
|
428
483
|
emit("listening", {
|
|
@@ -560,6 +615,13 @@ async function build() {
|
|
|
560
615
|
},
|
|
561
616
|
},
|
|
562
617
|
});
|
|
618
|
+
// What this build is, beside the bundle it is about. One line, written by
|
|
619
|
+
// every build and read by nothing unless a project turned a durable cache on
|
|
620
|
+
// — at which point it is the thing that stops a deploy answering the new
|
|
621
|
+
// build's URLs with the previous build's documents. See
|
|
622
|
+
// `packages/server/internal/cache-key.js`, which argues the whole of it, and
|
|
623
|
+
// `internal/serve.js`'s `buildIdentity`, which is what reads this.
|
|
624
|
+
writeFileSync(path.join(serverDir, BUILD_ID_FILE), `${mintBuildId()}\n`);
|
|
563
625
|
|
|
564
626
|
// 3. Which routes this build renders when, and every route it renders now.
|
|
565
627
|
//
|
|
@@ -645,7 +707,7 @@ async function build() {
|
|
|
645
707
|
//
|
|
646
708
|
// The condition is "there is a root boundary", not "there is any boundary",
|
|
647
709
|
// because `/__uf_not_found__` is a path at the root: a project whose only
|
|
648
|
-
//
|
|
710
|
+
// `$not-found.js` is in `app/guide/` would otherwise get a `404.html`
|
|
649
711
|
// rendered from the framework's bare default, which is worse than the file
|
|
650
712
|
// it used to write, which was none.
|
|
651
713
|
//
|
|
@@ -657,11 +719,50 @@ async function build() {
|
|
|
657
719
|
// host would then serve uf's error page to every visitor who mistyped a URL,
|
|
658
720
|
// and nothing between the throw and the deploy would have mentioned it.
|
|
659
721
|
let attempted = pages.length;
|
|
722
|
+
// The single-page build's whole output, written here because it is the one
|
|
723
|
+
// document this build has and the loop above had no route to write it for.
|
|
724
|
+
//
|
|
725
|
+
// Twice, to two names, and the second is the load-bearing one. `index.html`
|
|
726
|
+
// is what a host serves for `/`; `404.html` is what a static host serves for
|
|
727
|
+
// every path it has no file for, which under this plan is *every other URL
|
|
728
|
+
// in the application*. Without it a deployment of `dist/` answers `/` and
|
|
729
|
+
// 404s `/orders` — a build with a hole in it, found from a 404, which is the
|
|
730
|
+
// failure ubugeeei-prod/uf#336 is about wearing a different hat.
|
|
731
|
+
//
|
|
732
|
+
// It is still the host's rewrite rule that makes this correct, and the two
|
|
733
|
+
// files are what uf can do without one: a host with a proper SPA fallback
|
|
734
|
+
// serves `index.html` and never looks at `404.html`, and a host with only an
|
|
735
|
+
// error document (Pages, Netlify, an S3 bucket) serves `404.html` and gets
|
|
736
|
+
// the same bytes with a 404 status — which is the right status for a URL
|
|
737
|
+
// this application does not have, and the not-found boundary is what the
|
|
738
|
+
// browser then renders into it.
|
|
739
|
+
if (prerender === "shell") {
|
|
740
|
+
const html = server.shellDocument(assets);
|
|
741
|
+
for (const [file, url, status] of [
|
|
742
|
+
["index.html", "/", 200],
|
|
743
|
+
["404.html", "/404", 404],
|
|
744
|
+
]) {
|
|
745
|
+
const target = path.join(outDir, file);
|
|
746
|
+
writeFileSync(target, html);
|
|
747
|
+
attempted += 1;
|
|
748
|
+
emit("page", {
|
|
749
|
+
url,
|
|
750
|
+
file: path.relative(root, target),
|
|
751
|
+
status,
|
|
752
|
+
bytes: Buffer.byteLength(html),
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
}
|
|
660
756
|
// Not for a build that prerenders nothing. `404.html` is a file a static
|
|
661
757
|
// host serves for every path it has no file for, and a project whose
|
|
662
758
|
// `rendering.modes` allows only `ssr` has no such host: its not-found
|
|
663
759
|
// boundary is rendered per request, by the server, with the right status.
|
|
664
|
-
|
|
760
|
+
//
|
|
761
|
+
// Nor for a shell build, which has just written its own: the boundary this
|
|
762
|
+
// would render is one the *browser* renders in that plan, and a document
|
|
763
|
+
// holding the framework's 404 markup would be served in place of the shell
|
|
764
|
+
// for every URL the host could not match.
|
|
765
|
+
else if (prerender !== "nothing" && server.notFound.some((boundary) => boundary.path === "/")) {
|
|
665
766
|
attempted += 1;
|
|
666
767
|
// `/404` rather than `/__uf_not_found__`: the internal path is how the
|
|
667
768
|
// router is asked, and the file the reader is looking for is `404.html`.
|
|
@@ -971,13 +1072,33 @@ async function compile() {
|
|
|
971
1072
|
*
|
|
972
1073
|
* `bun`, `deno` and `static` are deliberately absent; `uf_config`'s
|
|
973
1074
|
* `DeployAdapter::is_implemented` is the other half of that fact and
|
|
974
|
-
* `docs/app/reference/cli
|
|
1075
|
+
* `docs/app/reference/cli/$page.mdx` says why for each of them.
|
|
975
1076
|
*/
|
|
976
1077
|
const ADAPTERS = {
|
|
977
1078
|
node: {
|
|
978
|
-
entries: (document, cache) => ({
|
|
979
|
-
handler: handlerEntrySource(document, cache, NODE_CAPABILITIES),
|
|
980
|
-
server: nodeEntrySource("./handler.js"),
|
|
1079
|
+
entries: (document, cache, build, schedules) => ({
|
|
1080
|
+
handler: handlerEntrySource(document, cache, NODE_CAPABILITIES, build),
|
|
1081
|
+
server: nodeEntrySource("./handler.js", schedules),
|
|
1082
|
+
}),
|
|
1083
|
+
},
|
|
1084
|
+
// The same two files as `node`, with `@uniflowed/server/bun` in place of
|
|
1085
|
+
// `@uniflowed/server/node`. That module is `./internal/static.js` for every
|
|
1086
|
+
// decision and `Bun.file` for the bytes, which is where the measured win is
|
|
1087
|
+
// — see ubugeeei-prod/uf#391 and the header of `packages/server/bun.js`.
|
|
1088
|
+
//
|
|
1089
|
+
// No `conditions` of its own, unlike `edge`. Bun honours a `bun` export
|
|
1090
|
+
// condition and this deliberately does not ask for it: the measured win is
|
|
1091
|
+
// `Bun.serve` and `Bun.file` and not resolution, so asking would trade a
|
|
1092
|
+
// `handler.js` that is byte-for-byte `node`'s — which is what
|
|
1093
|
+
// `every_adapter_answers_exactly_what_the_node_adapter_answers` checks, and
|
|
1094
|
+
// what makes an adapter one file rather than one application — for whichever
|
|
1095
|
+
// build a dependency happens to ship behind that condition, tested by
|
|
1096
|
+
// nobody here. `edge` pays that price because it must: there is no
|
|
1097
|
+
// `node:stream` in a Worker.
|
|
1098
|
+
bun: {
|
|
1099
|
+
entries: (document, cache, build, schedules) => ({
|
|
1100
|
+
handler: handlerEntrySource(document, cache, BUN_CAPABILITIES, build),
|
|
1101
|
+
server: bunEntrySource("./handler.js", schedules),
|
|
981
1102
|
}),
|
|
982
1103
|
},
|
|
983
1104
|
// The same two files. What `--adapter container` adds is a `Dockerfile` and
|
|
@@ -985,25 +1106,31 @@ const ADAPTERS = {
|
|
|
985
1106
|
// output rather than anything the bundler produces — see `uf_cli`'s
|
|
986
1107
|
// `commands::deploy`.
|
|
987
1108
|
container: {
|
|
988
|
-
entries: (document, cache) => ({
|
|
989
|
-
handler: handlerEntrySource(document, cache, NODE_CAPABILITIES),
|
|
990
|
-
server: nodeEntrySource("./handler.js"),
|
|
1109
|
+
entries: (document, cache, build, schedules) => ({
|
|
1110
|
+
handler: handlerEntrySource(document, cache, NODE_CAPABILITIES, build),
|
|
1111
|
+
server: nodeEntrySource("./handler.js", schedules),
|
|
991
1112
|
}),
|
|
992
1113
|
},
|
|
993
1114
|
edge: {
|
|
994
|
-
entries: (document, cache) => ({
|
|
995
|
-
handler: handlerEntrySource(document, cache, EDGE_CAPABILITIES),
|
|
996
|
-
worker: workerEntrySource("./handler.js"),
|
|
1115
|
+
entries: (document, cache, build, schedules) => ({
|
|
1116
|
+
handler: handlerEntrySource(document, cache, EDGE_CAPABILITIES, build),
|
|
1117
|
+
worker: workerEntrySource("./handler.js", schedules),
|
|
997
1118
|
}),
|
|
998
1119
|
// `workerd` first, so React resolves to the build that has
|
|
999
1120
|
// `renderToReadableStream` and no `node:stream`. `browser` and `module`
|
|
1000
1121
|
// after it are Vite's own SSR defaults, kept so a dependency with no
|
|
1001
1122
|
// worker condition still resolves the way it does for every other target.
|
|
1002
1123
|
conditions: ["workerd", "worker", "edge-light", "browser", "module", "import", "default"],
|
|
1124
|
+
// A Worker has no filesystem, so uf's built-in durable provider cannot run
|
|
1125
|
+
// here. Refused by name at the build rather than linked into a bundle that
|
|
1126
|
+
// fails on its first `node:fs` import — the deployment rule is that a
|
|
1127
|
+
// target which cannot provide a durable store says so, and this is the one
|
|
1128
|
+
// target that cannot.
|
|
1129
|
+
filesystem: false,
|
|
1003
1130
|
},
|
|
1004
1131
|
serverless: {
|
|
1005
|
-
entries: (document, cache) => ({
|
|
1006
|
-
handler: handlerEntrySource(document, cache, SERVERLESS_CAPABILITIES),
|
|
1132
|
+
entries: (document, cache, build) => ({
|
|
1133
|
+
handler: handlerEntrySource(document, cache, SERVERLESS_CAPABILITIES, build),
|
|
1007
1134
|
lambda: lambdaEntrySource("./handler.js"),
|
|
1008
1135
|
}),
|
|
1009
1136
|
},
|
|
@@ -1027,6 +1154,7 @@ const ADAPTERS = {
|
|
|
1027
1154
|
* dropped.
|
|
1028
1155
|
*/
|
|
1029
1156
|
const NODE_CAPABILITIES = { module: "@uniflowed/server/node", name: "nodeCapabilities" };
|
|
1157
|
+
const BUN_CAPABILITIES = { module: "@uniflowed/server/bun", name: "bunCapabilities" };
|
|
1030
1158
|
const EDGE_CAPABILITIES = { module: "@uniflowed/server/edge", name: "edgeCapabilities" };
|
|
1031
1159
|
const SERVERLESS_CAPABILITIES = { module: "@uniflowed/server/lambda", name: "lambdaCapabilities" };
|
|
1032
1160
|
|
|
@@ -1094,6 +1222,12 @@ async function deploy() {
|
|
|
1094
1222
|
if (adapter == null || workArgument == null || outputArgument == null) {
|
|
1095
1223
|
throw new Error("uf: `driver.js deploy` needs --adapter, --work and --output");
|
|
1096
1224
|
}
|
|
1225
|
+
// What the project declared, read by `uf`'s own walk of the route handlers
|
|
1226
|
+
// and handed over rather than found again here — one reading of a module,
|
|
1227
|
+
// and the same list `wrangler.json`'s `triggers.crons` is written from.
|
|
1228
|
+
// Absent on an older `uf` spawning a newer driver, which is a build with no
|
|
1229
|
+
// schedules rather than an error. See ubugeeei-prod/uf#531.
|
|
1230
|
+
const schedules = JSON.parse(argument("--schedules") ?? "[]");
|
|
1097
1231
|
// The Rust side has already refused every adapter it has no implementation
|
|
1098
1232
|
// for, by name and with the issue that tracks it. This is the second half of
|
|
1099
1233
|
// that fact rather than a duplicate of it: the driver may be spawned by a
|
|
@@ -1119,7 +1253,22 @@ async function deploy() {
|
|
|
1119
1253
|
// misbehaves.
|
|
1120
1254
|
mkdirSync(work, { recursive: true });
|
|
1121
1255
|
const document = assetsFromManifest(readManifest(outDir));
|
|
1122
|
-
|
|
1256
|
+
// Whatever `build` above minted, so a durable cache in the deployed artefact
|
|
1257
|
+
// is keyed by the build that produced it and not by the moment it was
|
|
1258
|
+
// packaged. Read rather than minted again for exactly that reason: a second
|
|
1259
|
+
// `randomUUID()` here would key the adapter's copy differently from the one
|
|
1260
|
+
// `uf start` serves out of `.uf/build/`, which is two caches for one build.
|
|
1261
|
+
const buildId = await buildIdentity(root, path.join(".uf", "build", "server"));
|
|
1262
|
+
const cacheConfig = config.app?.rendering?.cache;
|
|
1263
|
+
if (shape.filesystem === false && cacheConfig?.store === "filesystem") {
|
|
1264
|
+
throw new Error(
|
|
1265
|
+
`uf: rendering.cache.store is "filesystem" and \`--adapter ${adapter}\` has no ` +
|
|
1266
|
+
"filesystem. Name a module exporting `createCacheProvider` instead — a KV " +
|
|
1267
|
+
"namespace or a Redis behind the same seam — or leave the store in memory. See " +
|
|
1268
|
+
"docs/app/guide/cache.",
|
|
1269
|
+
);
|
|
1270
|
+
}
|
|
1271
|
+
const entries = shape.entries(document, cacheConfig, buildId, schedules);
|
|
1123
1272
|
const input = {};
|
|
1124
1273
|
for (const name of Object.keys(entries)) {
|
|
1125
1274
|
writeFileSync(path.join(work, `${name}.js`), entries[name]);
|
|
@@ -1209,8 +1358,22 @@ async function deploy() {
|
|
|
1209
1358
|
* re-exported above — a module-level singleton belongs to whichever copy of the
|
|
1210
1359
|
* package a bundler happened to give it, and the copy that matters is the one
|
|
1211
1360
|
* the application resolved. See ubugeeei-prod/uf#277 and #389.
|
|
1361
|
+
*
|
|
1362
|
+
* # And where a durable store is named
|
|
1363
|
+
*
|
|
1364
|
+
* `rendering.cache.store` is the fifth key, and it is the one that turns the
|
|
1365
|
+
* store into a shared one: `"filesystem"` links uf's built-in provider, and
|
|
1366
|
+
* anything else is a module specifier the project wrote, imported here by name
|
|
1367
|
+
* so the bundler links it like any other dependency of the application. Neither
|
|
1368
|
+
* appears at all when the key is absent, which is what a default project keeps.
|
|
1369
|
+
*
|
|
1370
|
+
* `build` is baked in beside it, and it is the reason this can be an `import`
|
|
1371
|
+
* at all rather than something read at boot: the build id is a fact about the
|
|
1372
|
+
* artefact being written, known here and nowhere later. `internal/serve.js`'s
|
|
1373
|
+
* `buildIdentity` is where it came from and
|
|
1374
|
+
* `packages/server/internal/cache-key.js` is why it exists.
|
|
1212
1375
|
*/
|
|
1213
|
-
function handlerEntrySource(document, cache, capabilities) {
|
|
1376
|
+
function handlerEntrySource(document, cache, capabilities, build) {
|
|
1214
1377
|
const route = cache?.route === true;
|
|
1215
1378
|
const fetchCache = cache?.fetch === true;
|
|
1216
1379
|
// Nothing at all when both switches are off, so a default project's
|
|
@@ -1218,6 +1381,7 @@ function handlerEntrySource(document, cache, capabilities) {
|
|
|
1218
1381
|
// generated output nobody asked for is the second half of the complaint
|
|
1219
1382
|
// #277 makes about the first half.
|
|
1220
1383
|
const store = route || fetchCache;
|
|
1384
|
+
const durable = store ? durableStoreSource(root, cache, build) : null;
|
|
1221
1385
|
const options = [
|
|
1222
1386
|
"app",
|
|
1223
1387
|
`document: ${JSON.stringify(document)}`,
|
|
@@ -1225,19 +1389,29 @@ function handlerEntrySource(document, cache, capabilities) {
|
|
|
1225
1389
|
"capabilities",
|
|
1226
1390
|
].join(", ");
|
|
1227
1391
|
const cacheImport = store ? 'import { createCacheStore } from "@uniflowed/server/cache";\n' : "";
|
|
1392
|
+
const providerImport = durable == null ? "" : `${durable.import}\n`;
|
|
1228
1393
|
const from = JSON.stringify(capabilities.module);
|
|
1229
1394
|
const capabilityImport = `import { ${capabilities.name} } from ${from};`;
|
|
1230
1395
|
return `// Generated by \`uf build --adapter\`. Not checked in, not edited.
|
|
1231
1396
|
import { createFetchHandler } from "@uniflowed/server/fetch";
|
|
1232
|
-
${cacheImport}${capabilityImport}
|
|
1397
|
+
${cacheImport}${providerImport}${capabilityImport}
|
|
1233
1398
|
import * as app from ${JSON.stringify(VIRTUAL.server)};
|
|
1234
1399
|
|
|
1235
1400
|
${
|
|
1236
1401
|
store
|
|
1237
|
-
? `// \`rendering.cache\` from uf.config.js.
|
|
1402
|
+
? `// \`rendering.cache\` from uf.config.js. ${
|
|
1403
|
+
durable == null
|
|
1404
|
+
? `One store per process: it is
|
|
1238
1405
|
// emptied by a restart and is not shared with any other instance of this
|
|
1239
|
-
// application
|
|
1240
|
-
|
|
1406
|
+
// application.`
|
|
1407
|
+
: `Entries are kept by ${durable.what},
|
|
1408
|
+
// under this build's identity, so a restart finds them where it left them and
|
|
1409
|
+
// every process of this deployment reads one store — and \`revalidateTag\` in
|
|
1410
|
+
// any of them takes an entry out of the store all of them fill from.`
|
|
1411
|
+
} See ubugeeei-prod/uf#277.
|
|
1412
|
+
const cache = { store: createCacheStore(${
|
|
1413
|
+
durable == null ? "" : `{ provider: ${durable.provider}, build: ${JSON.stringify(build)} }`
|
|
1414
|
+
}), route: ${String(route)}, fetch: ${String(fetchCache)} };
|
|
1241
1415
|
|
|
1242
1416
|
`
|
|
1243
1417
|
: ""
|
|
@@ -1256,6 +1430,89 @@ export default { fetch, beginRequest };
|
|
|
1256
1430
|
`;
|
|
1257
1431
|
}
|
|
1258
1432
|
|
|
1433
|
+
/**
|
|
1434
|
+
* The import and the expression that give a generated handler a durable store.
|
|
1435
|
+
*
|
|
1436
|
+
* `null` for `"memory"` and for a project that said nothing, which is every
|
|
1437
|
+
* project until one asks: persistence is a second opt-in on top of `route` and
|
|
1438
|
+
* `fetch`, not something a build decides on a project's behalf.
|
|
1439
|
+
*
|
|
1440
|
+
* The directory is baked in as written rather than resolved here, and that is
|
|
1441
|
+
* deliberate. This function runs on the machine doing the build; the path has
|
|
1442
|
+
* to mean something on the machine doing the *serving*, which may be a
|
|
1443
|
+
* container with one writable mount or a Lambda with only `/tmp`. A relative
|
|
1444
|
+
* one is resolved against the working directory at boot, by the provider, where
|
|
1445
|
+
* the answer is a fact rather than a guess.
|
|
1446
|
+
*
|
|
1447
|
+
* The specifier is resolved against the project for the same reason
|
|
1448
|
+
* `internal/serve.js`'s `providerSpecifier` does it: `"./cache/redis.js"` in
|
|
1449
|
+
* `uf.config.js` is relative to the project, and this file is written into
|
|
1450
|
+
* `.uf/deploy/work/`, where that path means nothing. Absolute is safe here
|
|
1451
|
+
* because the bundler inlines the module rather than emitting the specifier.
|
|
1452
|
+
*
|
|
1453
|
+
* @param {string} root
|
|
1454
|
+
* @param {{store?: string, storeDir?: string}} cache
|
|
1455
|
+
* @param {string | null} build
|
|
1456
|
+
*/
|
|
1457
|
+
function durableStoreSource(root, cache, build) {
|
|
1458
|
+
const named = cache?.store ?? "memory";
|
|
1459
|
+
if (named === "memory") return null;
|
|
1460
|
+
if (build == null) {
|
|
1461
|
+
throw new Error(
|
|
1462
|
+
`uf: rendering.cache.store is ${JSON.stringify(named)}, which keeps entries between ` +
|
|
1463
|
+
"restarts, and this build has no identity to key them by. Run `uf build` so one is " +
|
|
1464
|
+
"written, or set UF_BUILD_ID. Without one the deployment would answer this build's " +
|
|
1465
|
+
"URLs with the previous build's documents.",
|
|
1466
|
+
);
|
|
1467
|
+
}
|
|
1468
|
+
const directory = JSON.stringify(cache?.storeDir ?? path.join(".uf", "cache", "route"));
|
|
1469
|
+
if (named === "filesystem") {
|
|
1470
|
+
return {
|
|
1471
|
+
import: 'import { createFilesystemCache } from "@uniflowed/server/cache/filesystem";',
|
|
1472
|
+
provider: `createFilesystemCache({ directory: ${directory} })`,
|
|
1473
|
+
what: "uf's filesystem provider",
|
|
1474
|
+
};
|
|
1475
|
+
}
|
|
1476
|
+
const from = JSON.stringify(providerSpecifier(root, named));
|
|
1477
|
+
return {
|
|
1478
|
+
import: `import { createCacheProvider } from ${from};`,
|
|
1479
|
+
provider: `createCacheProvider({ build: ${JSON.stringify(build)}, directory: ${directory} })`,
|
|
1480
|
+
what: `${named}'s provider`,
|
|
1481
|
+
};
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
/**
|
|
1485
|
+
* The lines a process entry needs to run what the project declared.
|
|
1486
|
+
*
|
|
1487
|
+
* Shared by `nodeEntrySource` and `bunEntrySource` because the two differ in
|
|
1488
|
+
* which module they take `serve` from and in nothing else — and a schedule
|
|
1489
|
+
* that behaved differently between them would be the drift the whole seam
|
|
1490
|
+
* exists to prevent. Empty strings when a project declared none, so the entry
|
|
1491
|
+
* a default project gets is the file it has always been.
|
|
1492
|
+
*/
|
|
1493
|
+
function scheduleLines(module, schedules) {
|
|
1494
|
+
const declared = schedules ?? [];
|
|
1495
|
+
if (declared.length === 0) {
|
|
1496
|
+
return { imports: "", declarations: "", option: "" };
|
|
1497
|
+
}
|
|
1498
|
+
const built = declared
|
|
1499
|
+
.map(
|
|
1500
|
+
(schedule) =>
|
|
1501
|
+
` routeSchedule({ handle: fetch, beginRequest, path: ${JSON.stringify(
|
|
1502
|
+
schedule.path,
|
|
1503
|
+
)}, cron: ${JSON.stringify(schedule.cron)} }),`,
|
|
1504
|
+
)
|
|
1505
|
+
.join("\n");
|
|
1506
|
+
return {
|
|
1507
|
+
imports: `import { routeSchedule } from ${JSON.stringify(module)};\n`,
|
|
1508
|
+
// A schedule runs the route by asking the application for it, so a
|
|
1509
|
+
// scheduled run and a request for the same path are one code path. See
|
|
1510
|
+
// ubugeeei-prod/uf#531.
|
|
1511
|
+
declarations: `\nconst schedules = [\n${built}\n];\n`,
|
|
1512
|
+
option: ", schedules",
|
|
1513
|
+
};
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1259
1516
|
/**
|
|
1260
1517
|
* The source of `server.js`: the Node socket around that handler.
|
|
1261
1518
|
*
|
|
@@ -1265,13 +1522,14 @@ export default { fetch, beginRequest };
|
|
|
1265
1522
|
* request answered by `uf start` go through one implementation, not two that
|
|
1266
1523
|
* agree today.
|
|
1267
1524
|
*/
|
|
1268
|
-
function nodeEntrySource(handlerSpecifier) {
|
|
1525
|
+
function nodeEntrySource(handlerSpecifier, schedules) {
|
|
1526
|
+
const cron = scheduleLines("@uniflowed/server/schedule", schedules);
|
|
1269
1527
|
return `// Generated by \`uf build --adapter node\`. Not checked in, not edited.
|
|
1270
1528
|
import path from "node:path";
|
|
1271
1529
|
import { fileURLToPath } from "node:url";
|
|
1272
1530
|
|
|
1273
1531
|
import { serve } from "@uniflowed/server/node";
|
|
1274
|
-
|
|
1532
|
+
${cron.imports}
|
|
1275
1533
|
// \`beginRequest\` comes from the handler beside this file rather than from
|
|
1276
1534
|
// \`@uniflowed/server/node\` above, because the request has to be established in
|
|
1277
1535
|
// the storage the *application* reads, which is the copy bundled into
|
|
@@ -1284,12 +1542,48 @@ import { beginRequest, fetch } from ${JSON.stringify(handlerSpecifier)};
|
|
|
1284
1542
|
// assets when it was started from inside itself would be a deployment with a
|
|
1285
1543
|
// trap in it.
|
|
1286
1544
|
const staticDir = path.join(path.dirname(fileURLToPath(import.meta.url)), "static");
|
|
1287
|
-
|
|
1545
|
+
${cron.declarations}
|
|
1288
1546
|
// Not \`await serve(...)\` at the top level. uf parses that now
|
|
1289
1547
|
// (ubugeeei-prod/uf#204) and this entry is a module, so it would work; \`.catch\`
|
|
1290
1548
|
// is the better spelling regardless — a server that cannot take its port should
|
|
1291
1549
|
// say so and exit non-zero, rather than die as an unhandled rejection.
|
|
1292
|
-
serve({ handle: fetch, staticDir, beginRequest }).catch((error) => {
|
|
1550
|
+
serve({ handle: fetch, staticDir, beginRequest${cron.option} }).catch((error) => {
|
|
1551
|
+
process.stderr.write(\`uf: \${error?.message ?? String(error)}\\n\`);
|
|
1552
|
+
process.exit(1);
|
|
1553
|
+
});
|
|
1554
|
+
`;
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
/**
|
|
1558
|
+
* The source of `server.js`: the Bun socket around that handler.
|
|
1559
|
+
*
|
|
1560
|
+
* `nodeEntrySource`'s twin, and identical but for the module it imports
|
|
1561
|
+
* `serve` from — `@uniflowed/server/bun` exports the same signature on
|
|
1562
|
+
* purpose, so that the two adapters are one contract and a project moving
|
|
1563
|
+
* between them changes a flag and nothing else.
|
|
1564
|
+
*/
|
|
1565
|
+
function bunEntrySource(handlerSpecifier, schedules) {
|
|
1566
|
+
const cron = scheduleLines("@uniflowed/server/schedule", schedules);
|
|
1567
|
+
return `// Generated by \`uf build --adapter bun\`. Not checked in, not edited.
|
|
1568
|
+
import path from "node:path";
|
|
1569
|
+
import { fileURLToPath } from "node:url";
|
|
1570
|
+
|
|
1571
|
+
import { serve } from "@uniflowed/server/bun";
|
|
1572
|
+
${cron.imports}
|
|
1573
|
+
// \`beginRequest\` comes from the handler beside this file rather than from
|
|
1574
|
+
// \`@uniflowed/server/bun\` above, because the request has to be established in
|
|
1575
|
+
// the storage the *application* reads, which is the copy bundled into
|
|
1576
|
+
// \`handler.js\`. See ubugeeei-prod/uf#389.
|
|
1577
|
+
import { beginRequest, fetch } from ${JSON.stringify(handlerSpecifier)};
|
|
1578
|
+
|
|
1579
|
+
// Resolved from this file and not from the working directory: a process
|
|
1580
|
+
// manager, a container entrypoint and a person in a shell each start a server
|
|
1581
|
+
// from wherever they happen to be, and a directory that only served its own
|
|
1582
|
+
// assets when it was started from inside itself would be a deployment with a
|
|
1583
|
+
// trap in it.
|
|
1584
|
+
const staticDir = path.join(path.dirname(fileURLToPath(import.meta.url)), "static");
|
|
1585
|
+
${cron.declarations}
|
|
1586
|
+
serve({ handle: fetch, staticDir, beginRequest${cron.option} }).catch((error) => {
|
|
1293
1587
|
process.stderr.write(\`uf: \${error?.message ?? String(error)}\\n\`);
|
|
1294
1588
|
process.exit(1);
|
|
1295
1589
|
});
|
|
@@ -1308,13 +1602,39 @@ serve({ handle: fetch, staticDir, beginRequest }).catch((error) => {
|
|
|
1308
1602
|
* `nodeEntrySource` gives: the request has to be established in the storage the
|
|
1309
1603
|
* *application* reads. See ubugeeei-prod/uf#389.
|
|
1310
1604
|
*/
|
|
1311
|
-
function workerEntrySource(handlerSpecifier) {
|
|
1312
|
-
|
|
1605
|
+
function workerEntrySource(handlerSpecifier, schedules) {
|
|
1606
|
+
const declared = schedules ?? [];
|
|
1607
|
+
// Nothing at all when the project declared none, so a `worker.js` without
|
|
1608
|
+
// schedules is the file it has always been — and `wrangler.json` carries no
|
|
1609
|
+
// `triggers` for it either, so there is nothing to call the export that
|
|
1610
|
+
// would not be there.
|
|
1611
|
+
if (declared.length === 0) {
|
|
1612
|
+
return `// Generated by \`uf build --adapter edge\`. Not checked in, not edited.
|
|
1313
1613
|
import { createWorkerFetch } from "@uniflowed/server/edge";
|
|
1314
1614
|
|
|
1315
1615
|
import { beginRequest, fetch as handle } from ${JSON.stringify(handlerSpecifier)};
|
|
1316
1616
|
|
|
1317
1617
|
export default { fetch: createWorkerFetch({ handle, beginRequest }) };
|
|
1618
|
+
`;
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
// The expression Cloudflare fires, mapped to the route that answers it.
|
|
1622
|
+
// `event.cron` arrives spelled exactly as `wrangler.json` spells it, and
|
|
1623
|
+
// `uf` writes both from one list, so the two cannot disagree.
|
|
1624
|
+
const routes = Object.fromEntries(declared.map((schedule) => [schedule.cron, schedule.path]));
|
|
1625
|
+
return `// Generated by \`uf build --adapter edge\`. Not checked in, not edited.
|
|
1626
|
+
import { createWorkerFetch, createWorkerScheduled } from "@uniflowed/server/edge";
|
|
1627
|
+
|
|
1628
|
+
import { beginRequest, fetch as handle } from ${JSON.stringify(handlerSpecifier)};
|
|
1629
|
+
|
|
1630
|
+
// \`triggers.crons\` in the wrangler.json beside this file names these same
|
|
1631
|
+
// expressions. See ubugeeei-prod/uf#531.
|
|
1632
|
+
const routes = ${JSON.stringify(routes, null, 2)};
|
|
1633
|
+
|
|
1634
|
+
export default {
|
|
1635
|
+
fetch: createWorkerFetch({ handle, beginRequest }),
|
|
1636
|
+
scheduled: createWorkerScheduled({ handle, beginRequest, routes }),
|
|
1637
|
+
};
|
|
1318
1638
|
`;
|
|
1319
1639
|
}
|
|
1320
1640
|
|
|
@@ -1469,7 +1789,7 @@ function readManifest(outDir) {
|
|
|
1469
1789
|
* this function are about.
|
|
1470
1790
|
*
|
|
1471
1791
|
* Handlers and middleware are in the same list, and they belong there: this is
|
|
1472
|
-
* the list of things that need a process, and a
|
|
1792
|
+
* the list of things that need a process, and a `$route.js` needs one more
|
|
1473
1793
|
* obviously than any page does. They carry no per-route render — the build has
|
|
1474
1794
|
* never written a file for either — so they appear only when the answer might
|
|
1475
1795
|
* be a refusal.
|
|
@@ -1481,6 +1801,15 @@ async function renderingPlan(server, prerender) {
|
|
|
1481
1801
|
const urls = [];
|
|
1482
1802
|
const perRequest = [];
|
|
1483
1803
|
|
|
1804
|
+
// One document, and it is no route's, so there is no route to ask anything
|
|
1805
|
+
// about. `perRequest` is empty rather than "every route": nothing here is
|
|
1806
|
+
// left for a server — the browser answers all of it — and listing routes
|
|
1807
|
+
// under a heading that means "these need a process" would be a build
|
|
1808
|
+
// describing itself wrongly to `uf`, which prints that list.
|
|
1809
|
+
if (prerender === "shell") {
|
|
1810
|
+
return { urls: [], perRequest: [] };
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1484
1813
|
// Nothing is prerendered and nothing is refused, so no page module is
|
|
1485
1814
|
// loaded: a project that renders everything per request should not pay for
|
|
1486
1815
|
// a `generateStaticParams` this build will not call.
|