@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 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 ? null : createServeHandler({ ...build, cache: config.app?.rendering?.cache });
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({ ...build, cache: config.app?.rendering?.cache }),
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), `${mintBuildId()}\n`);
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(() => server.prerender(url, assets));
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(document, cache, NODE_CAPABILITIES, build, regeneration),
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(document, cache, BUN_CAPABILITIES, build, regeneration),
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(document, cache, DENO_CAPABILITIES, build, regeneration),
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(document, cache, NODE_CAPABILITIES, build, regeneration),
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(document, cache, EDGE_CAPABILITIES, build, regeneration),
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(document, cache, SERVERLESS_CAPABILITIES, build, regeneration),
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
- const entries = shape.entries(document, cacheConfig, buildId, schedules, regeneration);
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
- const cacheImport = store ? 'import { createCacheStore } from "@uniflowed/server/cache";\n' : "";
1711
- const providerImport = durable == null ? "" : `${durable.import}\n`;
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)) return rscEntrySource(VIRTUAL.routes, routing);
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();
@@ -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) {
@@ -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
- /** `virtual:uf/rsc`: the Flight renderer over the rsc graph's route table. */
504
- export function rscEntrySource(routesId, routing = {}) {
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
- * @param {{entry: object, assets: object, cache?: object, root?: string, build?: string | null}} build
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({ entry, assets, cache, root, build, regeneration }) {
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({ entry, assets, distDir, cache, root, build, regeneration }) {
595
- const application = createApplicationHandler({ entry, assets, cache, root, build, regeneration });
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.45",
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.45",
39
- "@uniflowed/router": "0.0.0-alpha.45",
40
- "@uniflowed/server": "0.0.0-alpha.45",
41
- "@uniflowed/validator": "0.0.0-alpha.45",
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",