@uniflowed/vite 0.0.0-alpha.45 → 0.0.0-alpha.47
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 +299 -26
- package/index.js +2 -1
- package/internal/assets.js +100 -0
- package/internal/flight.js +16 -3
- package/internal/image-endpoint.js +118 -0
- package/internal/serve.js +126 -5
- package/package.json +5 -5
package/driver.js
CHANGED
|
@@ -49,6 +49,7 @@ import path from "node:path";
|
|
|
49
49
|
import { pathToFileURL } from "node:url";
|
|
50
50
|
|
|
51
51
|
import { COMPILE_ASSETS_ID, compileAssetsPlugin } from "./internal/compile-assets.js";
|
|
52
|
+
import { servesRemoteImages } from "./internal/image-endpoint.js";
|
|
52
53
|
import {
|
|
53
54
|
survivingImports,
|
|
54
55
|
unavailableOnWorkers,
|
|
@@ -65,6 +66,7 @@ import { VIRTUAL, resolveRouteTarget, routingRulesOf, scanRoutes } from "./inter
|
|
|
65
66
|
import {
|
|
66
67
|
BUILD_ID_FILE,
|
|
67
68
|
DOCUMENT_ASSETS_FILE,
|
|
69
|
+
PARTIAL_PRERENDER_FILE,
|
|
68
70
|
REGENERATED_DIRECTORY,
|
|
69
71
|
REGENERATION_FILE,
|
|
70
72
|
answerRouting,
|
|
@@ -73,11 +75,13 @@ import {
|
|
|
73
75
|
buildIdentity,
|
|
74
76
|
createPrerenderGate,
|
|
75
77
|
createServeHandler,
|
|
78
|
+
deploymentIdFor,
|
|
76
79
|
documentAssetsFor,
|
|
77
80
|
forViteBase,
|
|
78
81
|
loadBuild,
|
|
79
82
|
nodeListener,
|
|
80
83
|
providerSpecifier,
|
|
84
|
+
readPartialPrerenders,
|
|
81
85
|
readRegeneration,
|
|
82
86
|
withRequest,
|
|
83
87
|
} from "./internal/serve.js";
|
|
@@ -474,7 +478,13 @@ async function preview() {
|
|
|
474
478
|
// and because it is the same handler the two give the same answer — which is
|
|
475
479
|
// the whole reason `uf preview` exists.
|
|
476
480
|
const handle =
|
|
477
|
-
build == null
|
|
481
|
+
build == null
|
|
482
|
+
? null
|
|
483
|
+
: createServeHandler({
|
|
484
|
+
...build,
|
|
485
|
+
cache: config.app?.rendering?.cache,
|
|
486
|
+
images: config.app?.builtins?.images,
|
|
487
|
+
});
|
|
478
488
|
const answer = (previewServer) => async (request, response, next) => {
|
|
479
489
|
try {
|
|
480
490
|
const asRequest = await toRequest(request, previewServer.config);
|
|
@@ -634,7 +644,11 @@ async function start() {
|
|
|
634
644
|
const port = Number(argument("--port") ?? process.env.PORT ?? 3000);
|
|
635
645
|
const server = createHttpServer(
|
|
636
646
|
nodeListener(
|
|
637
|
-
createServeHandler({
|
|
647
|
+
createServeHandler({
|
|
648
|
+
...build,
|
|
649
|
+
cache: config.app?.rendering?.cache,
|
|
650
|
+
images: config.app?.builtins?.images,
|
|
651
|
+
}),
|
|
638
652
|
build.entry,
|
|
639
653
|
),
|
|
640
654
|
);
|
|
@@ -699,8 +713,14 @@ async function build() {
|
|
|
699
713
|
if (graph != null) {
|
|
700
714
|
inline.plugins = [...(inline.plugins ?? []), graph.plugin];
|
|
701
715
|
}
|
|
716
|
+
// What this build is called, minted before anything is bundled: the rsc
|
|
717
|
+
// graph bakes the public half into every payload it renders, and the
|
|
718
|
+
// documents below carry it in their head. See `deploymentIdFor`.
|
|
719
|
+
const buildId = mintBuildId();
|
|
720
|
+
const deployment = deploymentIdFor(buildId);
|
|
702
721
|
const rscDir = path.join(root, ".uf", "build", "rsc");
|
|
703
722
|
if (flight != null) {
|
|
723
|
+
flight.deployment = deployment;
|
|
704
724
|
emit("phase", { name: "rsc" });
|
|
705
725
|
await buildRscGraph(vite, inline, flight, { outDir: rscDir, conditions: null });
|
|
706
726
|
}
|
|
@@ -761,7 +781,7 @@ async function build() {
|
|
|
761
781
|
// build's URLs with the previous build's documents. See
|
|
762
782
|
// `packages/server/internal/cache-key.js`, which argues the whole of it, and
|
|
763
783
|
// `internal/serve.js`'s `buildIdentity`, which is what reads this.
|
|
764
|
-
writeFileSync(path.join(serverDir, BUILD_ID_FILE), `${
|
|
784
|
+
writeFileSync(path.join(serverDir, BUILD_ID_FILE), `${buildId}\n`);
|
|
765
785
|
|
|
766
786
|
// Every bundle is built by here, and the prerender below builds none.
|
|
767
787
|
graph?.write(path.join(root, ".uf", "build", "meta", MODULE_GRAPH_FILE));
|
|
@@ -774,10 +794,15 @@ async function build() {
|
|
|
774
794
|
// arrives here as one word, and this is where it meets the route table.
|
|
775
795
|
emit("phase", { name: "prerender" });
|
|
776
796
|
const server = await import(pathToFileURL(path.join(serverDir, "server.js")).href);
|
|
777
|
-
const assets =
|
|
778
|
-
flight == null
|
|
797
|
+
const assets = {
|
|
798
|
+
...(flight == null
|
|
779
799
|
? assetsFromManifest(manifest, basePathOf(config))
|
|
780
|
-
: flightAssets(manifest, references, rscDir, outDir, basePathOf(config))
|
|
800
|
+
: flightAssets(manifest, references, rscDir, outDir, basePathOf(config))),
|
|
801
|
+
// Which build these URLs are. Every document names it, a browser sends it
|
|
802
|
+
// back, and a server on another build refuses rather than answering with
|
|
803
|
+
// ids and chunks the page does not have. See `deploymentIdFor`.
|
|
804
|
+
deployment,
|
|
805
|
+
};
|
|
781
806
|
// Recorded beside the server bundle, because whatever serves this build
|
|
782
807
|
// later cannot recompute them from the client manifest alone; see
|
|
783
808
|
// `documentAssetsFor`.
|
|
@@ -849,14 +874,30 @@ async function build() {
|
|
|
849
874
|
// A tag without a lifetime is not enough, for the route cache's own reason:
|
|
850
875
|
// an entry with no end is one another process could serve from its memory
|
|
851
876
|
// for ever after `revalidateTag` took it out of the shared store.
|
|
877
|
+
//
|
|
878
|
+
// A page is written as a static shell when `uf` passed `--partial` — `ppr`
|
|
879
|
+
// allowed in `app.rendering.modes` and a server deployed to fill it — and the
|
|
880
|
+
// page read `cookies()`, `headers()` or `draftMode()` inside a `<Suspense>`
|
|
881
|
+
// boundary. The prerender then answers with a `shell` instead of a document:
|
|
882
|
+
// the markup React could render without a request, and React's record of the
|
|
883
|
+
// holes. Nothing is written under `dist/` for it, neither a document nor a
|
|
884
|
+
// payload, because a shell on its own is a page whose holes never fill and
|
|
885
|
+
// the browser hydrates from the request's payload. The shell goes into
|
|
886
|
+
// `PARTIAL_PRERENDER_FILE` beside the server bundle, and every server that
|
|
887
|
+
// serves this build answers the page from there. A page that read the
|
|
888
|
+
// request outside every boundary still fails, naming what it read.
|
|
852
889
|
const { collectCacheDeclarations } = await import("@uniflowed/server/cache");
|
|
853
890
|
const regenerate = flag("--regenerate");
|
|
891
|
+
const partialPrerender = flag("--partial");
|
|
854
892
|
const regenerated = {};
|
|
893
|
+
const partial = {};
|
|
855
894
|
for (const url of pages) {
|
|
856
895
|
let declared;
|
|
857
896
|
const renderedAt = Date.now();
|
|
858
897
|
try {
|
|
859
|
-
declared = await collectCacheDeclarations(() =>
|
|
898
|
+
declared = await collectCacheDeclarations(() =>
|
|
899
|
+
server.prerender(url, assets, partialPrerender ? { partial: true } : undefined),
|
|
900
|
+
);
|
|
860
901
|
} catch (error) {
|
|
861
902
|
failed(url, error);
|
|
862
903
|
continue;
|
|
@@ -866,6 +907,18 @@ async function build() {
|
|
|
866
907
|
failed(url, result.error);
|
|
867
908
|
continue;
|
|
868
909
|
}
|
|
910
|
+
if (result.shell != null) {
|
|
911
|
+
partial[url] = result.shell;
|
|
912
|
+
emit("page", {
|
|
913
|
+
url,
|
|
914
|
+
file: path.relative(root, path.join(serverDir, PARTIAL_PRERENDER_FILE)),
|
|
915
|
+
status: result.status,
|
|
916
|
+
bytes: Buffer.byteLength(result.shell.html),
|
|
917
|
+
regenerates: false,
|
|
918
|
+
partial: true,
|
|
919
|
+
});
|
|
920
|
+
continue;
|
|
921
|
+
}
|
|
869
922
|
const lifetime = declared.lifetime;
|
|
870
923
|
const regenerates =
|
|
871
924
|
regenerate && result.status === 200 && declared.denied == null && lifetime != null;
|
|
@@ -912,6 +965,12 @@ async function build() {
|
|
|
912
965
|
`${JSON.stringify({ pages: regenerated }, null, 2)}\n`,
|
|
913
966
|
);
|
|
914
967
|
}
|
|
968
|
+
if (Object.keys(partial).length > 0) {
|
|
969
|
+
writeFileSync(
|
|
970
|
+
path.join(serverDir, PARTIAL_PRERENDER_FILE),
|
|
971
|
+
`${JSON.stringify({ pages: partial })}\n`,
|
|
972
|
+
);
|
|
973
|
+
}
|
|
915
974
|
// One `404.html`, from the boundary at the router root: a static host serves
|
|
916
975
|
// a single error document for the whole site, so the nested boundaries a
|
|
917
976
|
// project declares are the server's and the client's to render, not
|
|
@@ -1233,6 +1292,18 @@ async function compile() {
|
|
|
1233
1292
|
}
|
|
1234
1293
|
const assets = path.resolve(root, assetsArgument);
|
|
1235
1294
|
const bundleDir = path.resolve(root, bundleArgument);
|
|
1295
|
+
// The compiled binary answers from `@uniflowed/server/standalone`, which has
|
|
1296
|
+
// no `/__uf/image` — and it would still carry pages whose `Image` writes the
|
|
1297
|
+
// endpoint's URLs. Refused by name rather than compiled into a binary whose
|
|
1298
|
+
// remote images are all 404s. See docs/app/guide/assets.
|
|
1299
|
+
if (servesRemoteImages(config.app?.builtins?.images)) {
|
|
1300
|
+
throw new Error(
|
|
1301
|
+
"uf: app.builtins.images.remotePatterns lists remote hosts, and a `--compile` binary " +
|
|
1302
|
+
"has no /__uf/image to answer them: it carries neither the `uf` encoder nor a place " +
|
|
1303
|
+
"to name one. Serve the build with `uf start` or an `--adapter`, or remove the remote " +
|
|
1304
|
+
"patterns. See docs/app/guide/assets.",
|
|
1305
|
+
);
|
|
1306
|
+
}
|
|
1236
1307
|
|
|
1237
1308
|
emit("phase", { name: "standalone" });
|
|
1238
1309
|
|
|
@@ -1299,11 +1370,24 @@ async function compile() {
|
|
|
1299
1370
|
* invocations and is refused by name instead. A regenerated page kept only in
|
|
1300
1371
|
* memory would go back to the build's copy on every restart, which is a page
|
|
1301
1372
|
* that travels back in time. See [`deploy`].
|
|
1373
|
+
*
|
|
1374
|
+
* `streams: false` marks the one target whose responses are buffered whole.
|
|
1375
|
+
* A page the build prerendered partially is worth nothing there — its shell is
|
|
1376
|
+
* the part that was meant to arrive first — so such a build is refused by name
|
|
1377
|
+
* rather than deployed as a page that waits for its slowest hole.
|
|
1302
1378
|
*/
|
|
1303
1379
|
const ADAPTERS = {
|
|
1304
1380
|
node: {
|
|
1305
|
-
entries: (document, cache, build, schedules, regeneration) => ({
|
|
1306
|
-
handler: handlerEntrySource(
|
|
1381
|
+
entries: (document, cache, build, schedules, regeneration, images, partial) => ({
|
|
1382
|
+
handler: handlerEntrySource(
|
|
1383
|
+
document,
|
|
1384
|
+
cache,
|
|
1385
|
+
NODE_CAPABILITIES,
|
|
1386
|
+
build,
|
|
1387
|
+
regeneration,
|
|
1388
|
+
images,
|
|
1389
|
+
partial,
|
|
1390
|
+
),
|
|
1307
1391
|
server: nodeEntrySource("./handler.js", schedules),
|
|
1308
1392
|
}),
|
|
1309
1393
|
regenerationStore: "filesystem",
|
|
@@ -1323,15 +1407,31 @@ const ADAPTERS = {
|
|
|
1323
1407
|
// nobody here. `edge` pays that price because it must: there is no
|
|
1324
1408
|
// `node:stream` in a Worker.
|
|
1325
1409
|
bun: {
|
|
1326
|
-
entries: (document, cache, build, schedules, regeneration) => ({
|
|
1327
|
-
handler: handlerEntrySource(
|
|
1410
|
+
entries: (document, cache, build, schedules, regeneration, images, partial) => ({
|
|
1411
|
+
handler: handlerEntrySource(
|
|
1412
|
+
document,
|
|
1413
|
+
cache,
|
|
1414
|
+
BUN_CAPABILITIES,
|
|
1415
|
+
build,
|
|
1416
|
+
regeneration,
|
|
1417
|
+
images,
|
|
1418
|
+
partial,
|
|
1419
|
+
),
|
|
1328
1420
|
server: bunEntrySource("./handler.js", schedules),
|
|
1329
1421
|
}),
|
|
1330
1422
|
regenerationStore: "filesystem",
|
|
1331
1423
|
},
|
|
1332
1424
|
deno: {
|
|
1333
|
-
entries: (document, cache, build, schedules, regeneration) => ({
|
|
1334
|
-
handler: handlerEntrySource(
|
|
1425
|
+
entries: (document, cache, build, schedules, regeneration, images, partial) => ({
|
|
1426
|
+
handler: handlerEntrySource(
|
|
1427
|
+
document,
|
|
1428
|
+
cache,
|
|
1429
|
+
DENO_CAPABILITIES,
|
|
1430
|
+
build,
|
|
1431
|
+
regeneration,
|
|
1432
|
+
images,
|
|
1433
|
+
partial,
|
|
1434
|
+
),
|
|
1335
1435
|
server: denoEntrySource("./handler.js", schedules),
|
|
1336
1436
|
}),
|
|
1337
1437
|
regenerationStore: "filesystem",
|
|
@@ -1341,15 +1441,31 @@ const ADAPTERS = {
|
|
|
1341
1441
|
// output rather than anything the bundler produces — see `uf_cli`'s
|
|
1342
1442
|
// `commands::deploy`.
|
|
1343
1443
|
container: {
|
|
1344
|
-
entries: (document, cache, build, schedules, regeneration) => ({
|
|
1345
|
-
handler: handlerEntrySource(
|
|
1444
|
+
entries: (document, cache, build, schedules, regeneration, images, partial) => ({
|
|
1445
|
+
handler: handlerEntrySource(
|
|
1446
|
+
document,
|
|
1447
|
+
cache,
|
|
1448
|
+
NODE_CAPABILITIES,
|
|
1449
|
+
build,
|
|
1450
|
+
regeneration,
|
|
1451
|
+
images,
|
|
1452
|
+
partial,
|
|
1453
|
+
),
|
|
1346
1454
|
server: nodeEntrySource("./handler.js", schedules),
|
|
1347
1455
|
}),
|
|
1348
1456
|
regenerationStore: "filesystem",
|
|
1349
1457
|
},
|
|
1350
1458
|
edge: {
|
|
1351
|
-
entries: (document, cache, build, schedules, regeneration) => ({
|
|
1352
|
-
handler: handlerEntrySource(
|
|
1459
|
+
entries: (document, cache, build, schedules, regeneration, images, partial) => ({
|
|
1460
|
+
handler: handlerEntrySource(
|
|
1461
|
+
document,
|
|
1462
|
+
cache,
|
|
1463
|
+
EDGE_CAPABILITIES,
|
|
1464
|
+
build,
|
|
1465
|
+
regeneration,
|
|
1466
|
+
images,
|
|
1467
|
+
partial,
|
|
1468
|
+
),
|
|
1353
1469
|
worker: workerEntrySource("./handler.js", schedules),
|
|
1354
1470
|
}),
|
|
1355
1471
|
// Workers KV, through the same module seam a project's own provider goes
|
|
@@ -1373,14 +1489,27 @@ const ADAPTERS = {
|
|
|
1373
1489
|
workerBuiltins: true,
|
|
1374
1490
|
},
|
|
1375
1491
|
serverless: {
|
|
1376
|
-
entries: (document, cache, build, _schedules, regeneration) => ({
|
|
1377
|
-
handler: handlerEntrySource(
|
|
1492
|
+
entries: (document, cache, build, _schedules, regeneration, images, partial) => ({
|
|
1493
|
+
handler: handlerEntrySource(
|
|
1494
|
+
document,
|
|
1495
|
+
cache,
|
|
1496
|
+
SERVERLESS_CAPABILITIES,
|
|
1497
|
+
build,
|
|
1498
|
+
regeneration,
|
|
1499
|
+
images,
|
|
1500
|
+
partial,
|
|
1501
|
+
),
|
|
1378
1502
|
lambda: lambdaEntrySource("./handler.js"),
|
|
1379
1503
|
}),
|
|
1380
1504
|
// A Lambda's memory lasts one instance and its disk is that instance's
|
|
1381
1505
|
// `/tmp`, so neither is somewhere a regenerated page survives. A build that
|
|
1382
1506
|
// regenerates pages has to name a provider module; see [`deploy`].
|
|
1383
1507
|
regenerationStore: null,
|
|
1508
|
+
// And `@uniflowed/server/lambda` buffers every response into the one
|
|
1509
|
+
// result an invocation returns, so a page's static shell would reach the
|
|
1510
|
+
// browser with its holes, not before them. A build that prerendered a page
|
|
1511
|
+
// partially is refused by name; see [`deploy`].
|
|
1512
|
+
streams: false,
|
|
1384
1513
|
},
|
|
1385
1514
|
};
|
|
1386
1515
|
|
|
@@ -1541,7 +1670,33 @@ async function deploy() {
|
|
|
1541
1670
|
}
|
|
1542
1671
|
cacheConfig = { ...declaredCache, store: shape.regenerationStore };
|
|
1543
1672
|
}
|
|
1544
|
-
|
|
1673
|
+
// The pages this build prerendered partially. Their static shells are baked
|
|
1674
|
+
// into `handler.js`, and a target that cannot send a shell before its holes
|
|
1675
|
+
// refuses them by name: a partial prerender answered by a buffered response
|
|
1676
|
+
// is a page that waits for its slowest hole, which is exactly the page it
|
|
1677
|
+
// was prerendered not to be.
|
|
1678
|
+
const partial = await readPartialPrerenders(path.join(root, ".uf", "build", "server"));
|
|
1679
|
+
if (partial != null && shape.streams === false) {
|
|
1680
|
+
const pages = Object.keys(partial.pages);
|
|
1681
|
+
throw new Error(
|
|
1682
|
+
`uf: this build prerendered ${pages.length} ${plural(pages.length, "page")} partially ` +
|
|
1683
|
+
`(${pages.join(", ")}), and \`--adapter ${adapter}\` buffers every response, so the ` +
|
|
1684
|
+
"static shell would arrive with its holes rather than before them. Deploy with an " +
|
|
1685
|
+
"adapter that streams — node, bun, deno, container or edge — or export " +
|
|
1686
|
+
'`dynamic = "force-dynamic"` from those pages to render each of them whole per ' +
|
|
1687
|
+
"request. See docs/app/guide/rendering.",
|
|
1688
|
+
);
|
|
1689
|
+
}
|
|
1690
|
+
const images = await imageEndpointFor(adapter, config.app?.builtins?.images);
|
|
1691
|
+
const entries = shape.entries(
|
|
1692
|
+
document,
|
|
1693
|
+
cacheConfig,
|
|
1694
|
+
buildId,
|
|
1695
|
+
schedules,
|
|
1696
|
+
regeneration,
|
|
1697
|
+
images,
|
|
1698
|
+
partial,
|
|
1699
|
+
);
|
|
1545
1700
|
const input = {};
|
|
1546
1701
|
for (const name of Object.keys(entries)) {
|
|
1547
1702
|
writeFileSync(path.join(work, `${name}.js`), entries[name]);
|
|
@@ -1555,6 +1710,9 @@ async function deploy() {
|
|
|
1555
1710
|
const flight = flightStateOf(inline);
|
|
1556
1711
|
if (flight != null) {
|
|
1557
1712
|
loadFlightBuild(flight, path.join(root, ".uf", "build", "rsc"));
|
|
1713
|
+
// The id `build` recorded, and not a new one: a Worker's payloads have to
|
|
1714
|
+
// name the same build its documents do.
|
|
1715
|
+
flight.deployment = document.deployment ?? null;
|
|
1558
1716
|
if (shape.conditions != null) {
|
|
1559
1717
|
await buildRscGraph(vite, inline, flight, {
|
|
1560
1718
|
outDir: path.join(work, "rsc"),
|
|
@@ -1687,7 +1845,7 @@ async function deploy() {
|
|
|
1687
1845
|
* `buildIdentity` is where it came from and
|
|
1688
1846
|
* `packages/server/internal/cache-key.js` is why it exists.
|
|
1689
1847
|
*/
|
|
1690
|
-
function handlerEntrySource(document, cache, capabilities, build, regeneration) {
|
|
1848
|
+
function handlerEntrySource(document, cache, capabilities, build, regeneration, images, partial) {
|
|
1691
1849
|
const route = cache?.route === true;
|
|
1692
1850
|
const fetchCache = cache?.fetch === true;
|
|
1693
1851
|
const dataCache = cache?.data === true;
|
|
@@ -1706,15 +1864,29 @@ function handlerEntrySource(document, cache, capabilities, build, regeneration)
|
|
|
1706
1864
|
// the same reason: the manifest exists on the machine doing the build, and
|
|
1707
1865
|
// the deployed directory has only what this file carries.
|
|
1708
1866
|
...(store && regeneration != null ? [`regeneration: ${JSON.stringify(regeneration)}`] : []),
|
|
1867
|
+
...(images == null ? [] : ["images"]),
|
|
1868
|
+
// The static shells of the pages it prerendered partially, for the same
|
|
1869
|
+
// reason. Absent for a build with none, so its `handler.js` is unchanged.
|
|
1870
|
+
...(partial != null ? [`partial: ${JSON.stringify(partial)}`] : []),
|
|
1709
1871
|
].join(", ");
|
|
1710
|
-
|
|
1711
|
-
|
|
1872
|
+
// The image endpoint keeps its variants over the same durable provider when
|
|
1873
|
+
// there is one, so it needs the store constructor even for a build whose
|
|
1874
|
+
// route cache is off; `imageEndpointSource` argues the rest.
|
|
1875
|
+
const imageDurable =
|
|
1876
|
+
images == null || store ? null : durableStoreSource(root, cache, build, { required: false });
|
|
1877
|
+
const linkedDurable = durable ?? imageDurable;
|
|
1878
|
+
const cacheImport =
|
|
1879
|
+
store || imageDurable != null
|
|
1880
|
+
? 'import { createCacheStore } from "@uniflowed/server/cache";\n'
|
|
1881
|
+
: "";
|
|
1882
|
+
const providerImport = linkedDurable == null ? "" : `${linkedDurable.import}\n`;
|
|
1883
|
+
const imageSource = images == null ? null : imageEndpointSource(images, linkedDurable, build);
|
|
1712
1884
|
const from = JSON.stringify(capabilities.module);
|
|
1713
1885
|
const capabilityImport = `import { ${capabilities.name} } from ${from};`;
|
|
1714
1886
|
return `// Generated by \`uf build --adapter\`. Not checked in, not edited.
|
|
1715
1887
|
import { createFetchHandler } from "@uniflowed/server/fetch";
|
|
1716
1888
|
${cacheImport}${providerImport}${capabilityImport}
|
|
1717
|
-
import * as app from ${JSON.stringify(VIRTUAL.server)};
|
|
1889
|
+
${imageSource == null ? "" : imageSource.imports}import * as app from ${JSON.stringify(VIRTUAL.server)};
|
|
1718
1890
|
|
|
1719
1891
|
${
|
|
1720
1892
|
store
|
|
@@ -1741,7 +1913,7 @@ const cache = { store: createCacheStore(${
|
|
|
1741
1913
|
// passed for the upgrade or the queue — uf defines both and implements
|
|
1742
1914
|
// neither. See \`@uniflowed/server/socket\` and \`@uniflowed/server/queue\`.
|
|
1743
1915
|
const capabilities = ${capabilities.name}();
|
|
1744
|
-
|
|
1916
|
+
${imageSource == null ? "" : `\n${imageSource.declaration}`}
|
|
1745
1917
|
export const fetch = createFetchHandler({ ${options} });
|
|
1746
1918
|
export const beginRequest = app.beginRequest;
|
|
1747
1919
|
// \`app.router\`'s redirects and headers, for the entry beside this file to put
|
|
@@ -1776,10 +1948,14 @@ export default { fetch, beginRequest, routing };
|
|
|
1776
1948
|
* @param {{store?: string, storeDir?: string}} cache
|
|
1777
1949
|
* @param {string | null} build
|
|
1778
1950
|
*/
|
|
1779
|
-
function durableStoreSource(root, cache, build) {
|
|
1951
|
+
function durableStoreSource(root, cache, build, { required = true } = {}) {
|
|
1780
1952
|
const named = cache?.store ?? "memory";
|
|
1781
1953
|
if (named === "memory") return null;
|
|
1782
1954
|
if (build == null) {
|
|
1955
|
+
// Only the image endpoint asks without requiring one: its variants are a
|
|
1956
|
+
// cache of somebody else's images rather than of this build's documents,
|
|
1957
|
+
// and memory is a correct, colder, answer for them.
|
|
1958
|
+
if (!required) return null;
|
|
1783
1959
|
throw new Error(
|
|
1784
1960
|
`uf: rendering.cache.store is ${JSON.stringify(named)}, which keeps entries between ` +
|
|
1785
1961
|
"restarts, and this build has no identity to key them by. Run `uf build` so one is " +
|
|
@@ -1803,6 +1979,103 @@ function durableStoreSource(root, cache, build) {
|
|
|
1803
1979
|
};
|
|
1804
1980
|
}
|
|
1805
1981
|
|
|
1982
|
+
/**
|
|
1983
|
+
* What `--adapter` links for `/__uf/image`, or `null` for a project with no
|
|
1984
|
+
* remote images.
|
|
1985
|
+
*
|
|
1986
|
+
* A deployed directory has no `uf` in it, so the encoder `uf start` uses is
|
|
1987
|
+
* not there to link, and each target gets the one it has:
|
|
1988
|
+
*
|
|
1989
|
+
* * **`edge`**: Cloudflare's Images binding, through
|
|
1990
|
+
* `@uniflowed/server/image/edge` — the platform's own image service, handed
|
|
1991
|
+
* bytes uf fetched and checked rather than a URL it would fetch itself.
|
|
1992
|
+
* `uf` declares the binding in `wrangler.json` when it finds this linked.
|
|
1993
|
+
* * **Every other target**: the module `app.builtins.images.transformer`
|
|
1994
|
+
* names, exporting `createImageTransformer`. `node`, `bun`, `deno`,
|
|
1995
|
+
* `container` and `serverless` have no image service to delegate to, and a
|
|
1996
|
+
* project that deploys to one names the encoder it has — `sharp`, a WASM
|
|
1997
|
+
* codec, a call to a service it runs — behind the same seam `uf` sits behind.
|
|
1998
|
+
*
|
|
1999
|
+
* A target with neither is refused here, by name, rather than linked with an
|
|
2000
|
+
* endpoint that fetches every image and then fails to encode it.
|
|
2001
|
+
*
|
|
2002
|
+
* @param {string} adapter
|
|
2003
|
+
* @param {object | undefined} images `app.builtins.images`
|
|
2004
|
+
*/
|
|
2005
|
+
async function imageEndpointFor(adapter, images) {
|
|
2006
|
+
if (!servesRemoteImages(images)) return null;
|
|
2007
|
+
const defaults = await import("@uniflowed/server/image");
|
|
2008
|
+
const settings = {
|
|
2009
|
+
remotePatterns: images.remotePatterns,
|
|
2010
|
+
widths: images.widths ?? defaults.DEFAULT_WIDTHS,
|
|
2011
|
+
quality: images.quality ?? defaults.DEFAULT_QUALITY,
|
|
2012
|
+
qualities: images.qualities ?? [],
|
|
2013
|
+
};
|
|
2014
|
+
if (adapter === "edge") {
|
|
2015
|
+
return { kind: "edge", settings };
|
|
2016
|
+
}
|
|
2017
|
+
const named = images.transformer;
|
|
2018
|
+
if (typeof named !== "string" || named === "") {
|
|
2019
|
+
throw new Error(
|
|
2020
|
+
`uf: app.builtins.images.remotePatterns lists remote hosts, and \`--adapter ${adapter}\` ` +
|
|
2021
|
+
"has no image encoder to link: uf encodes with the `uf` binary under `uf start` and " +
|
|
2022
|
+
"`uf preview`, and a deployed directory does not carry it. Name a module exporting " +
|
|
2023
|
+
"`createImageTransformer` in app.builtins.images.transformer, deploy with " +
|
|
2024
|
+
"`--adapter edge` to use Cloudflare's image binding, or remove the remote patterns. " +
|
|
2025
|
+
"See docs/app/guide/assets.",
|
|
2026
|
+
);
|
|
2027
|
+
}
|
|
2028
|
+
return {
|
|
2029
|
+
kind: "module",
|
|
2030
|
+
settings,
|
|
2031
|
+
specifier: providerSpecifier(root, named),
|
|
2032
|
+
// Never on a deployment: a switch for a test that serves its own images.
|
|
2033
|
+
allowPrivateAddresses: images.dangerouslyAllowPrivateAddresses === true,
|
|
2034
|
+
};
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
/**
|
|
2038
|
+
* The lines `handler.js` needs for the endpoint [`imageEndpointFor`] chose.
|
|
2039
|
+
*
|
|
2040
|
+
* `durable` is the provider the route cache links, when it links one, so the
|
|
2041
|
+
* variants live where the project already said entries survive a restart; a
|
|
2042
|
+
* Worker with none keeps them in the isolate's memory.
|
|
2043
|
+
*
|
|
2044
|
+
* @param {{kind: "edge" | "module", settings: object, specifier?: string, allowPrivateAddresses?: boolean}} images
|
|
2045
|
+
* @param {{provider: string} | null} durable
|
|
2046
|
+
* @param {string | null} build
|
|
2047
|
+
*/
|
|
2048
|
+
function imageEndpointSource(images, durable, build) {
|
|
2049
|
+
const imports =
|
|
2050
|
+
images.kind === "edge"
|
|
2051
|
+
? 'import { createImageEndpoint, MAX_MEMORY_VARIANTS } from "@uniflowed/server/image";\n' +
|
|
2052
|
+
'import { cloudflareImageTransform, edgeImageFetch } from "@uniflowed/server/image/edge";\n'
|
|
2053
|
+
: 'import { createImageEndpoint, MAX_MEMORY_VARIANTS } from "@uniflowed/server/image";\n' +
|
|
2054
|
+
'import { nodeImageFetch } from "@uniflowed/server/image/node";\n' +
|
|
2055
|
+
`import { createImageTransformer } from ${JSON.stringify(images.specifier)};\n`;
|
|
2056
|
+
const fetch =
|
|
2057
|
+
images.kind === "edge"
|
|
2058
|
+
? "edgeImageFetch()"
|
|
2059
|
+
: `nodeImageFetch(${images.allowPrivateAddresses ? "{ allowPrivateAddresses: true }" : ""})`;
|
|
2060
|
+
const transform =
|
|
2061
|
+
images.kind === "edge" ? "cloudflareImageTransform()" : "createImageTransformer()";
|
|
2062
|
+
const store =
|
|
2063
|
+
durable == null
|
|
2064
|
+
? ""
|
|
2065
|
+
: `\n store: createCacheStore({ provider: ${durable.provider}, build: ${JSON.stringify(
|
|
2066
|
+
build,
|
|
2067
|
+
)}, maxEntries: MAX_MEMORY_VARIANTS }),`;
|
|
2068
|
+
const declaration = `// \`/__uf/image\`, for the remote hosts app.builtins.images.remotePatterns
|
|
2069
|
+
// lists. See \`@uniflowed/server/image\` and ubugeeei-prod/uf#958.
|
|
2070
|
+
const images = createImageEndpoint({
|
|
2071
|
+
...${JSON.stringify(images.settings)},
|
|
2072
|
+
fetch: ${fetch},
|
|
2073
|
+
transform: ${transform},${store}
|
|
2074
|
+
});
|
|
2075
|
+
`;
|
|
2076
|
+
return { imports, declaration };
|
|
2077
|
+
}
|
|
2078
|
+
|
|
1806
2079
|
/**
|
|
1807
2080
|
* The lines a process entry needs to run what the project declared.
|
|
1808
2081
|
*
|
package/index.js
CHANGED
|
@@ -610,7 +610,8 @@ function flowPlugin({
|
|
|
610
610
|
);
|
|
611
611
|
}
|
|
612
612
|
if (flightState != null) {
|
|
613
|
-
if (id === resolved(FLIGHT_VIRTUAL.entry))
|
|
613
|
+
if (id === resolved(FLIGHT_VIRTUAL.entry))
|
|
614
|
+
return rscEntrySource(VIRTUAL.routes, routing, flightState.deployment);
|
|
614
615
|
if (id === resolved(FLIGHT_VIRTUAL.compilerRuntime)) return compilerRuntimeSource();
|
|
615
616
|
if (id === resolved(FLIGHT_VIRTUAL.bridge)) {
|
|
616
617
|
if (server != null) return devBridgeSource();
|
package/internal/assets.js
CHANGED
|
@@ -81,6 +81,63 @@ import {
|
|
|
81
81
|
assetKind,
|
|
82
82
|
} from "@uniflowed/host/assets";
|
|
83
83
|
|
|
84
|
+
import { endpointSettings, localImageEndpoint, servesRemoteImages } from "./image-endpoint.js";
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Whether `id` is `@uniflowed/web`'s `internal/image-endpoint.js`.
|
|
88
|
+
*
|
|
89
|
+
* By path *and* by the package it is in, because the suffix alone is a name
|
|
90
|
+
* any package could have; the manifest two directories up has to say
|
|
91
|
+
* `@uniflowed/web`. The answer is remembered per directory, so the manifest is
|
|
92
|
+
* read once rather than once per module id.
|
|
93
|
+
*/
|
|
94
|
+
const webPackageDirectories = new Map();
|
|
95
|
+
function isWebImageEndpoint(id) {
|
|
96
|
+
const file = id.split("?")[0];
|
|
97
|
+
if (
|
|
98
|
+
!file.endsWith(`${path.sep}internal${path.sep}image-endpoint.js`) &&
|
|
99
|
+
!file.endsWith("/internal/image-endpoint.js")
|
|
100
|
+
) {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
const directory = path.dirname(path.dirname(file));
|
|
104
|
+
if (!webPackageDirectories.has(directory)) {
|
|
105
|
+
let name = null;
|
|
106
|
+
try {
|
|
107
|
+
name = JSON.parse(readFileSync(path.join(directory, "package.json"), "utf8")).name;
|
|
108
|
+
} catch {
|
|
109
|
+
name = null;
|
|
110
|
+
}
|
|
111
|
+
webPackageDirectories.set(directory, name === "@uniflowed/web");
|
|
112
|
+
}
|
|
113
|
+
return webPackageDirectories.get(directory);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* The source `@uniflowed/web/internal/image-endpoint.js` is answered with.
|
|
118
|
+
*
|
|
119
|
+
* The same shape as the file, with the project's values: the endpoint's path
|
|
120
|
+
* under Vite's `base` when `remotePatterns` lists anything, and `null`
|
|
121
|
+
* otherwise, which is the file's own answer — so a project with no remote
|
|
122
|
+
* images renders exactly what it rendered before the endpoint existed. The
|
|
123
|
+
* widths and quality are the ones the endpoint will accept, defaulted the way
|
|
124
|
+
* `@uniflowed/server/image` defaults them, so the `srcset` `Image` writes
|
|
125
|
+
* names no width the endpoint refuses.
|
|
126
|
+
*
|
|
127
|
+
* @param {object} images `app.builtins.images`
|
|
128
|
+
* @param {string} base Vite's resolved `base`
|
|
129
|
+
*/
|
|
130
|
+
export async function imageEndpointSource(images, base) {
|
|
131
|
+
const defaults = await import("@uniflowed/server/image");
|
|
132
|
+
const { widths, quality } = endpointSettings(images, defaults);
|
|
133
|
+
const endpoint = servesRemoteImages(images) ? `${base}__uf/image` : null;
|
|
134
|
+
return (
|
|
135
|
+
"// Generated by uf:asset from app.builtins.images. See @uniflowed/web's\n" +
|
|
136
|
+
"// internal/image-endpoint.js for the file this stands in for.\n" +
|
|
137
|
+
`export const IMAGE_ENDPOINT = ${JSON.stringify({ path: endpoint, widths, quality })};\n`
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
84
141
|
/** Where transformed assets are kept, relative to the project root. */
|
|
85
142
|
export const CACHE_DIR = ".uf/cache/assets";
|
|
86
143
|
|
|
@@ -333,6 +390,11 @@ export function assetPlugin({ images = {}, fonts = {}, icons = {}, og = {}, comm
|
|
|
333
390
|
},
|
|
334
391
|
|
|
335
392
|
async load(id) {
|
|
393
|
+
// `@uniflowed/web`'s idea of the image endpoint, answered with the
|
|
394
|
+
// project's. See `imageEndpointSource`.
|
|
395
|
+
if (isWebImageEndpoint(id)) {
|
|
396
|
+
return imageEndpointSource(imagesOn ? images : { enabled: false }, base);
|
|
397
|
+
}
|
|
336
398
|
if (id.startsWith(OG_PREFIX)) {
|
|
337
399
|
const file = id.slice(OG_PREFIX.length, -OG_SUFFIX.length);
|
|
338
400
|
// Watched by hand, because the module id is no longer the file's path
|
|
@@ -484,6 +546,44 @@ export function assetPlugin({ images = {}, fonts = {}, icons = {}, og = {}, comm
|
|
|
484
546
|
response.setHeader("Cache-Control", "public, max-age=31536000, immutable");
|
|
485
547
|
response.end(readFileSync(target));
|
|
486
548
|
});
|
|
549
|
+
|
|
550
|
+
// `/__uf/image`, so an `Image` with a remote `src` renders under
|
|
551
|
+
// `uf dev` exactly as it will under `uf start`: the same endpoint, the
|
|
552
|
+
// same allow-list, the same encoder. Mounted only for a project that
|
|
553
|
+
// listed remote hosts, and built on the first request for it rather
|
|
554
|
+
// than with the server. See `./image-endpoint.js`.
|
|
555
|
+
if (imagesOn && servesRemoteImages(images)) {
|
|
556
|
+
const endpointPath = `${base}__uf/image`;
|
|
557
|
+
let ready = null;
|
|
558
|
+
devServer.middlewares.use((request, response, next) => {
|
|
559
|
+
const url = request.url ?? "";
|
|
560
|
+
const queryAt = url.indexOf("?");
|
|
561
|
+
if ((queryAt === -1 ? url : url.slice(0, queryAt)) !== endpointPath) return next();
|
|
562
|
+
ready ??= Promise.all([
|
|
563
|
+
localImageEndpoint({ images, root, command }),
|
|
564
|
+
import("@uniflowed/server/node"),
|
|
565
|
+
]);
|
|
566
|
+
ready
|
|
567
|
+
.then(async ([endpoint, { send }]) => {
|
|
568
|
+
// The endpoint reads the query and `Accept` and nothing else, so
|
|
569
|
+
// that is all the request it is handed carries.
|
|
570
|
+
const accept = request.headers.accept;
|
|
571
|
+
const asked = new Request(
|
|
572
|
+
`http://localhost/__uf/image${queryAt === -1 ? "" : url.slice(queryAt)}`,
|
|
573
|
+
{
|
|
574
|
+
method: request.method,
|
|
575
|
+
headers: typeof accept === "string" ? { accept } : {},
|
|
576
|
+
},
|
|
577
|
+
);
|
|
578
|
+
const answered = await endpoint(asked);
|
|
579
|
+
if (answered == null) return next();
|
|
580
|
+
await send(response, answered);
|
|
581
|
+
return undefined;
|
|
582
|
+
})
|
|
583
|
+
.catch(next);
|
|
584
|
+
return undefined;
|
|
585
|
+
});
|
|
586
|
+
}
|
|
487
587
|
},
|
|
488
588
|
|
|
489
589
|
watchChange(id) {
|
package/internal/flight.js
CHANGED
|
@@ -247,6 +247,8 @@ export function createFlightState({ root }) {
|
|
|
247
247
|
clientModules: new Set(),
|
|
248
248
|
chunkUrls: new Map(),
|
|
249
249
|
rscOutput: null,
|
|
250
|
+
// The build's public id, set by the driver before it builds the rsc graph.
|
|
251
|
+
deployment: null,
|
|
250
252
|
};
|
|
251
253
|
}
|
|
252
254
|
|
|
@@ -500,8 +502,14 @@ export function fsFileOf(pathname) {
|
|
|
500
502
|
return /^[A-Za-z]:\//.test(rest) ? rest : `/${rest}`;
|
|
501
503
|
}
|
|
502
504
|
|
|
503
|
-
/**
|
|
504
|
-
|
|
505
|
+
/**
|
|
506
|
+
* `virtual:uf/rsc`: the Flight renderer over the rsc graph's route table.
|
|
507
|
+
*
|
|
508
|
+
* `deployment` is the build's public id, baked in so every payload says which
|
|
509
|
+
* build rendered it — a prerendered payload is a file, and nothing that serves
|
|
510
|
+
* a file can say so for it. `null` under `uf dev`.
|
|
511
|
+
*/
|
|
512
|
+
export function rscEntrySource(routesId, routing = {}, deployment = null) {
|
|
505
513
|
const settings = {
|
|
506
514
|
basePath: routing.basePath ?? "",
|
|
507
515
|
trailingSlash: routing.trailingSlash ?? "ignore",
|
|
@@ -510,7 +518,9 @@ export function rscEntrySource(routesId, routing = {}) {
|
|
|
510
518
|
import { routes, notFound, errors } from ${JSON.stringify(routesId)};
|
|
511
519
|
installRouting(${JSON.stringify(settings)});
|
|
512
520
|
export { routes, notFound, errors };
|
|
513
|
-
export const renderFlight = createFlightRenderer({ routes, notFound, errors
|
|
521
|
+
export const renderFlight = createFlightRenderer({ routes, notFound, errors, deployment: ${JSON.stringify(
|
|
522
|
+
deployment ?? null,
|
|
523
|
+
)} });
|
|
514
524
|
`;
|
|
515
525
|
}
|
|
516
526
|
|
|
@@ -716,6 +726,9 @@ export const render = (url, assets, options = {}) => instrumentRender(
|
|
|
716
726
|
(onError) => renderer.render(url, assets, { ...options, onError }), options.onError,
|
|
717
727
|
);
|
|
718
728
|
export const prerender = renderer.prerender;
|
|
729
|
+
export const resume = (url, assets, shell, options = {}) => instrumentRender(
|
|
730
|
+
(onError) => renderer.resume(url, assets, shell, { ...options, onError }), options.onError,
|
|
731
|
+
);
|
|
719
732
|
export const flight = (url, options = {}) => instrumentRender(
|
|
720
733
|
(onError) => renderer.flight(url, { ...options, onError }), options.onError,
|
|
721
734
|
);
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// @noflow
|
|
2
|
+
//
|
|
3
|
+
// Plain JavaScript: executed by the host that serves a build, and by `uf dev`.
|
|
4
|
+
//
|
|
5
|
+
// `/__uf/image` on the three doors `uf` itself runs — `uf dev`, `uf preview`
|
|
6
|
+
// and `uf start` — with `uf` as the encoder.
|
|
7
|
+
//
|
|
8
|
+
// The endpoint is `@uniflowed/server/image`'s, the fetch is
|
|
9
|
+
// `@uniflowed/server/image/node`'s, and this is the one piece those two
|
|
10
|
+
// cannot have: a transformer that reaches the `uf` binary. It writes the
|
|
11
|
+
// bounded source to a private temporary directory, asks `uf assets` for one
|
|
12
|
+
// variant of it (`crates/uf_cli/src/commands/assets.rs`'s `variant` request,
|
|
13
|
+
// which is `uf_assets::variant`), and reads the one file that comes back.
|
|
14
|
+
// Both files are removed before the answer is returned: the cache is the
|
|
15
|
+
// endpoint's `CacheStore`, not this directory.
|
|
16
|
+
//
|
|
17
|
+
// A deployed directory has no `uf` in it, which is why this is not in
|
|
18
|
+
// `@uniflowed/server`. What `--adapter` links instead is argued in
|
|
19
|
+
// `driver.js`'s `imageEndpointSource`.
|
|
20
|
+
|
|
21
|
+
import { randomUUID } from "node:crypto";
|
|
22
|
+
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
23
|
+
import os from "node:os";
|
|
24
|
+
import path from "node:path";
|
|
25
|
+
|
|
26
|
+
import { AssetService } from "@uniflowed/host/assets";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Whether the project asked for an endpoint at all.
|
|
30
|
+
*
|
|
31
|
+
* `app.builtins.images.remotePatterns` with something in it, and images not
|
|
32
|
+
* turned off. Nothing else constructs one: a project that listed no host has
|
|
33
|
+
* no `/__uf/image`, and the path is an ordinary 404.
|
|
34
|
+
*
|
|
35
|
+
* @param {{enabled?: boolean, remotePatterns?: unknown[]} | undefined} images
|
|
36
|
+
*/
|
|
37
|
+
export function servesRemoteImages(images) {
|
|
38
|
+
return (
|
|
39
|
+
images?.enabled !== false &&
|
|
40
|
+
Array.isArray(images?.remotePatterns) &&
|
|
41
|
+
images.remotePatterns.length > 0
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The endpoint's options from `app.builtins.images`, with uf's defaults where
|
|
47
|
+
* the project said nothing — the same defaults `uf_assets::ImagesConfig`
|
|
48
|
+
* applies, from `@uniflowed/server/image` where both sides can read them.
|
|
49
|
+
*
|
|
50
|
+
* @param {object} images `app.builtins.images`
|
|
51
|
+
* @param {{DEFAULT_WIDTHS: number[], DEFAULT_QUALITY: number}} defaults
|
|
52
|
+
*/
|
|
53
|
+
export function endpointSettings(images, defaults) {
|
|
54
|
+
return {
|
|
55
|
+
remotePatterns: images.remotePatterns,
|
|
56
|
+
widths: images.widths ?? defaults.DEFAULT_WIDTHS,
|
|
57
|
+
quality: images.quality ?? defaults.DEFAULT_QUALITY,
|
|
58
|
+
qualities: images.qualities ?? [],
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* An encoder that is `uf` itself.
|
|
64
|
+
*
|
|
65
|
+
* One `uf assets` process for the life of the server, started on the first
|
|
66
|
+
* miss and not before, and one temporary directory beside it.
|
|
67
|
+
*
|
|
68
|
+
* @param {{root: string, command?: string}} options
|
|
69
|
+
*/
|
|
70
|
+
export function ufImageTransform({ root, command }) {
|
|
71
|
+
let service = null;
|
|
72
|
+
let directory = null;
|
|
73
|
+
return async function transform({ bytes, width, quality, avif }) {
|
|
74
|
+
service ??= new AssetService({ command, root });
|
|
75
|
+
directory ??= mkdtemp(path.join(os.tmpdir(), "uf-image-"));
|
|
76
|
+
const base = path.join(await directory, randomUUID());
|
|
77
|
+
const source = `${base}.source`;
|
|
78
|
+
const out = `${base}.out`;
|
|
79
|
+
await writeFile(source, bytes);
|
|
80
|
+
try {
|
|
81
|
+
const variant = await service.variant(source, { outDir: out, width, quality, avif });
|
|
82
|
+
const encoded = await readFile(path.join(out, variant.file));
|
|
83
|
+
return {
|
|
84
|
+
bytes: new Uint8Array(encoded.buffer, encoded.byteOffset, encoded.byteLength),
|
|
85
|
+
type: variant.mime,
|
|
86
|
+
};
|
|
87
|
+
} finally {
|
|
88
|
+
await rm(source, { force: true });
|
|
89
|
+
await rm(out, { recursive: true, force: true });
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* `/__uf/image` for a door `uf` runs, or `undefined` for a project with no
|
|
96
|
+
* remote images.
|
|
97
|
+
*
|
|
98
|
+
* `store` is the `CacheStore` a durable `rendering.cache.store` gave the route
|
|
99
|
+
* cache's provider to, when there is one; without it the endpoint keeps its
|
|
100
|
+
* variants in memory.
|
|
101
|
+
*
|
|
102
|
+
* @param {{images?: object, root: string, command?: string, store?: object}} options
|
|
103
|
+
*/
|
|
104
|
+
export async function localImageEndpoint({ images, root, command, store }) {
|
|
105
|
+
if (!servesRemoteImages(images)) return undefined;
|
|
106
|
+
const [endpoint, node] = await Promise.all([
|
|
107
|
+
import("@uniflowed/server/image"),
|
|
108
|
+
import("@uniflowed/server/image/node"),
|
|
109
|
+
]);
|
|
110
|
+
return endpoint.createImageEndpoint({
|
|
111
|
+
...endpointSettings(images, endpoint),
|
|
112
|
+
fetch: node.nodeImageFetch({
|
|
113
|
+
allowPrivateAddresses: images.dangerouslyAllowPrivateAddresses === true,
|
|
114
|
+
}),
|
|
115
|
+
transform: ufImageTransform({ root, command }),
|
|
116
|
+
...(store == null ? {} : { store }),
|
|
117
|
+
});
|
|
118
|
+
}
|
package/internal/serve.js
CHANGED
|
@@ -71,10 +71,13 @@
|
|
|
71
71
|
// guard would run, the page would render, and every `cookies()` in it would
|
|
72
72
|
// throw as though no host had run at all. See ubugeeei-prod/uf#389.
|
|
73
73
|
|
|
74
|
+
import { createHash } from "node:crypto";
|
|
74
75
|
import { readFile, stat } from "node:fs/promises";
|
|
75
76
|
import path from "node:path";
|
|
76
77
|
import { pathToFileURL } from "node:url";
|
|
77
78
|
|
|
79
|
+
import { localImageEndpoint, servesRemoteImages } from "./image-endpoint.js";
|
|
80
|
+
|
|
78
81
|
/**
|
|
79
82
|
* `@uniflowed/server`'s two halves, loaded once.
|
|
80
83
|
*
|
|
@@ -127,6 +130,29 @@ async function cacheFor(declared, createCacheStore, where) {
|
|
|
127
130
|
return { store, route, fetch: fetchCache, data };
|
|
128
131
|
}
|
|
129
132
|
|
|
133
|
+
/**
|
|
134
|
+
* Where `/__uf/image` keeps its variants: over the provider
|
|
135
|
+
* `rendering.cache.store` names, or `undefined` for the endpoint's own memory.
|
|
136
|
+
*
|
|
137
|
+
* The same provider as the route cache, because it is the one a project
|
|
138
|
+
* already said survives a restart and is shared between processes — a second
|
|
139
|
+
* setting for "where do image variants go" would be a second answer to a
|
|
140
|
+
* question the project answered once. Asked only when a store is named, so a
|
|
141
|
+
* project that names none needs no build identity for its images.
|
|
142
|
+
*
|
|
143
|
+
* @param {{store?: string, storeDir?: string} | undefined} declared
|
|
144
|
+
* @param {(options?: object) => object} createCacheStore
|
|
145
|
+
* @param {{root: string, build: string | null}} where
|
|
146
|
+
*/
|
|
147
|
+
async function imageStoreFor(declared, createCacheStore, where) {
|
|
148
|
+
const named = declared?.store ?? "memory";
|
|
149
|
+
if (named === "memory") return undefined;
|
|
150
|
+
const provider = await providerFor(declared, where);
|
|
151
|
+
if (provider == null) return undefined;
|
|
152
|
+
const { MAX_MEMORY_VARIANTS } = await import("@uniflowed/server/image");
|
|
153
|
+
return createCacheStore({ provider, build: where.build, maxEntries: MAX_MEMORY_VARIANTS });
|
|
154
|
+
}
|
|
155
|
+
|
|
130
156
|
/**
|
|
131
157
|
* The durable provider `rendering.cache.store` names, or `null` for memory.
|
|
132
158
|
*
|
|
@@ -220,6 +246,7 @@ export async function loadBuild({ root, outDir, serverDir }) {
|
|
|
220
246
|
await deployment();
|
|
221
247
|
const build = await buildIdentity(root, serverDir);
|
|
222
248
|
const regeneration = await readRegeneration(path.resolve(root, serverDir));
|
|
249
|
+
const partial = await readPartialPrerenders(path.resolve(root, serverDir));
|
|
223
250
|
return {
|
|
224
251
|
entry,
|
|
225
252
|
assets: await documentAssetsFor(path.resolve(root, serverDir), manifest),
|
|
@@ -227,6 +254,7 @@ export async function loadBuild({ root, outDir, serverDir }) {
|
|
|
227
254
|
root,
|
|
228
255
|
build,
|
|
229
256
|
regeneration,
|
|
257
|
+
partial,
|
|
230
258
|
};
|
|
231
259
|
}
|
|
232
260
|
|
|
@@ -296,6 +324,33 @@ export async function readRegeneration(serverDir) {
|
|
|
296
324
|
return JSON.parse(text);
|
|
297
325
|
}
|
|
298
326
|
|
|
327
|
+
/**
|
|
328
|
+
* The file beside the server bundle that holds the static shells of the pages a
|
|
329
|
+
* build prerendered partially.
|
|
330
|
+
*
|
|
331
|
+
* Beside the server bundle rather than under the output directory, because a
|
|
332
|
+
* shell is not a document anybody should be sent on its own: its holes are
|
|
333
|
+
* filled by the server that reads this file, and a file server would send it
|
|
334
|
+
* with every hole showing its fallback forever.
|
|
335
|
+
*/
|
|
336
|
+
export const PARTIAL_PRERENDER_FILE = "partial-prerender.json";
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* The pages this build prerendered partially, or `undefined` for a build with
|
|
340
|
+
* none — which then serves exactly as every build did before partial
|
|
341
|
+
* prerendering existed.
|
|
342
|
+
*/
|
|
343
|
+
export async function readPartialPrerenders(serverDir) {
|
|
344
|
+
let text;
|
|
345
|
+
try {
|
|
346
|
+
text = await readFile(path.join(serverDir, PARTIAL_PRERENDER_FILE), "utf8");
|
|
347
|
+
} catch (error) {
|
|
348
|
+
if (error?.code === "ENOENT") return undefined;
|
|
349
|
+
throw error;
|
|
350
|
+
}
|
|
351
|
+
return JSON.parse(text);
|
|
352
|
+
}
|
|
353
|
+
|
|
299
354
|
/**
|
|
300
355
|
* What `import()` should be given for a provider a project named.
|
|
301
356
|
*
|
|
@@ -351,6 +406,24 @@ export async function buildIdentity(root, serverDir) {
|
|
|
351
406
|
}
|
|
352
407
|
}
|
|
353
408
|
|
|
409
|
+
/**
|
|
410
|
+
* The id a build's documents publish, derived from the build id.
|
|
411
|
+
*
|
|
412
|
+
* A browser names it on every action call and payload request, and a front
|
|
413
|
+
* door on another build refuses rather than answering; see
|
|
414
|
+
* `@uniflowed/server`'s `internal/deployment.js`. It has to be public — it is
|
|
415
|
+
* in every document — and the build id must not be: it is the key the action
|
|
416
|
+
* ids are an HMAC under whenever `UF_BUILD_ID` names both. So this is a
|
|
417
|
+
* digest of it, under a label of its own, and sixteen hex characters of that:
|
|
418
|
+
* the same for two artefacts that are one build, different for any two that
|
|
419
|
+
* are not, and no help to anybody guessing the key.
|
|
420
|
+
*
|
|
421
|
+
* @param {string} buildId
|
|
422
|
+
*/
|
|
423
|
+
export function deploymentIdFor(buildId) {
|
|
424
|
+
return createHash("sha256").update("uf:deployment\0").update(buildId).digest("hex").slice(0, 16);
|
|
425
|
+
}
|
|
426
|
+
|
|
354
427
|
async function readable(file, message) {
|
|
355
428
|
try {
|
|
356
429
|
await stat(file);
|
|
@@ -524,9 +597,21 @@ export async function beginRequest(entry, request) {
|
|
|
524
597
|
* keyed by. Both are `undefined` for a caller that constructs a handler by
|
|
525
598
|
* hand, which is the memory-only store and needs neither.
|
|
526
599
|
*
|
|
527
|
-
*
|
|
600
|
+
* `images` is `app.builtins.images`, and it adds `/__uf/image` to what the
|
|
601
|
+
* handler answers when it lists remote hosts; see `./image-endpoint.js`.
|
|
602
|
+
*
|
|
603
|
+
* @param {{entry: object, assets: object, cache?: object, root?: string, build?: string | null, regeneration?: object, images?: object, partial?: object}} build
|
|
528
604
|
*/
|
|
529
|
-
export function createApplicationHandler({
|
|
605
|
+
export function createApplicationHandler({
|
|
606
|
+
entry,
|
|
607
|
+
assets,
|
|
608
|
+
cache,
|
|
609
|
+
root,
|
|
610
|
+
build,
|
|
611
|
+
regeneration,
|
|
612
|
+
images,
|
|
613
|
+
partial,
|
|
614
|
+
}) {
|
|
530
615
|
const ready = deployment().then(
|
|
531
616
|
async ({ createFetchHandler, createCacheStore, nodeCapabilities }) =>
|
|
532
617
|
createFetchHandler({
|
|
@@ -537,10 +622,27 @@ export function createApplicationHandler({ entry, assets, cache, root, build, re
|
|
|
537
622
|
build: build ?? null,
|
|
538
623
|
regenerates: regeneration != null,
|
|
539
624
|
}),
|
|
625
|
+
// `/__uf/image`, for a project that listed remote hosts. See
|
|
626
|
+
// `./image-endpoint.js`.
|
|
627
|
+
...(servesRemoteImages(images)
|
|
628
|
+
? {
|
|
629
|
+
images: await localImageEndpoint({
|
|
630
|
+
images,
|
|
631
|
+
root: root ?? process.cwd(),
|
|
632
|
+
store: await imageStoreFor(cache, createCacheStore, {
|
|
633
|
+
root: root ?? process.cwd(),
|
|
634
|
+
build: build ?? null,
|
|
635
|
+
}),
|
|
636
|
+
}),
|
|
637
|
+
}
|
|
638
|
+
: {}),
|
|
540
639
|
// The pages this build regenerates, from the manifest beside the server
|
|
541
640
|
// bundle. Absent for a build with none, which then serves exactly as it
|
|
542
641
|
// did before regeneration existed.
|
|
543
642
|
...(regeneration == null ? {} : { regeneration }),
|
|
643
|
+
// And the static shells of the pages it prerendered partially, from the
|
|
644
|
+
// file beside the server bundle, for the same reason.
|
|
645
|
+
...(partial == null ? {} : { partial }),
|
|
544
646
|
// `uf preview` and `uf start` are a Node process with a socket, which is
|
|
545
647
|
// what a deployed `--adapter node` build is too — so a route handler
|
|
546
648
|
// that streams events answers the same way in the preview it is checked
|
|
@@ -589,10 +691,29 @@ export function createStaticHandler({ root }) {
|
|
|
589
691
|
* a deployment. `uf preview` puts the same two in front of Vite's file
|
|
590
692
|
* middleware as well; see [`answerRouting`].
|
|
591
693
|
*
|
|
592
|
-
* @param {{entry: object, assets: object, distDir: string, cache?: object, root?: string, build?: string | null, regeneration?: object}} build
|
|
694
|
+
* @param {{entry: object, assets: object, distDir: string, cache?: object, root?: string, build?: string | null, regeneration?: object, partial?: object}} build
|
|
593
695
|
*/
|
|
594
|
-
export function createServeHandler({
|
|
595
|
-
|
|
696
|
+
export function createServeHandler({
|
|
697
|
+
entry,
|
|
698
|
+
assets,
|
|
699
|
+
distDir,
|
|
700
|
+
cache,
|
|
701
|
+
root,
|
|
702
|
+
build,
|
|
703
|
+
regeneration,
|
|
704
|
+
images,
|
|
705
|
+
partial,
|
|
706
|
+
}) {
|
|
707
|
+
const application = createApplicationHandler({
|
|
708
|
+
entry,
|
|
709
|
+
assets,
|
|
710
|
+
cache,
|
|
711
|
+
root,
|
|
712
|
+
build,
|
|
713
|
+
regeneration,
|
|
714
|
+
images,
|
|
715
|
+
partial,
|
|
716
|
+
});
|
|
596
717
|
const ready = deployment().then(({ createServeHandler: create }) =>
|
|
597
718
|
create({ staticDir: distDir, handle: application, routing: entry.routing }),
|
|
598
719
|
);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniflowed/vite",
|
|
3
|
-
"version": "0.0.0-alpha.
|
|
3
|
+
"version": "0.0.0-alpha.47",
|
|
4
4
|
"description": "Vite, driven by uf.config.js: every Flow module through `uf transform`, MDX, the file-system router and static rendering as Vite plugins.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -35,10 +35,10 @@
|
|
|
35
35
|
"@babel/core": "^7.29.7",
|
|
36
36
|
"@mdx-js/rollup": "^3.1.1",
|
|
37
37
|
"@shikijs/rehype": "^3.23.0",
|
|
38
|
-
"@uniflowed/host": "0.0.0-alpha.
|
|
39
|
-
"@uniflowed/router": "0.0.0-alpha.
|
|
40
|
-
"@uniflowed/server": "0.0.0-alpha.
|
|
41
|
-
"@uniflowed/validator": "0.0.0-alpha.
|
|
38
|
+
"@uniflowed/host": "0.0.0-alpha.47",
|
|
39
|
+
"@uniflowed/router": "0.0.0-alpha.47",
|
|
40
|
+
"@uniflowed/server": "0.0.0-alpha.47",
|
|
41
|
+
"@uniflowed/validator": "0.0.0-alpha.47",
|
|
42
42
|
"babel-plugin-relay": "^21.0.1",
|
|
43
43
|
"estree-util-value-to-estree": "^3.5.0",
|
|
44
44
|
"rehype-slug": "^6.0.0",
|