@uniflowed/vite 0.0.0-alpha.35 → 0.0.0-alpha.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,134 @@
1
+ // @noflow
2
+ //
3
+ // Plain JavaScript: the driver imports it, and the driver runs before any Flow
4
+ // loader exists.
5
+ //
6
+ // The module graph `uf build --analyze` reads: every module each bundle was
7
+ // built from, what it imports, and which chunk its rendered code went into.
8
+ //
9
+ // What the graph means — which route a module belongs to, the chain of imports
10
+ // behind it, what it weighs — is uf's, in `crates/uf_bundle/src/analysis.rs`.
11
+ // This file writes down only what the bundler knows and uf cannot, and that
12
+ // split is the reason the graph is a file at all: a builder other than this
13
+ // one that writes the same file gets the same analysis.
14
+ //
15
+ // {
16
+ // "version": 1,
17
+ // "builds": [{
18
+ // "environment": "client",
19
+ // "entries": ["virtual:uf/client"],
20
+ // "modules": [{ "id": "app/$page.js", "imports": ["app/Counter.js"], "dynamicImports": [] }],
21
+ // "chunks": [{
22
+ // "file": "assets/index-3f2a.js",
23
+ // "facade": "virtual:uf/client",
24
+ // "modules": [{ "id": "virtual:uf/client", "code": "…" }]
25
+ // }]
26
+ // }]
27
+ // }
28
+
29
+ import { mkdirSync, writeFileSync } from "node:fs";
30
+ import path from "node:path";
31
+
32
+ /** Where the driver writes the graph, in the build's metadata directory. */
33
+ export const MODULE_GRAPH_FILE = "uf-module-graph.json";
34
+
35
+ const VERSION = 1;
36
+
37
+ /**
38
+ * A plugin that records the graph of every bundle it takes part in, and the
39
+ * means to write what it recorded.
40
+ *
41
+ * `isReference(file)` says whether an absolute file is a client reference: an
42
+ * entry the browser loads because a server component named it, rather than on
43
+ * every page. Its chunk still carries it as the facade, and it is left out of
44
+ * `entries`, which is the list of what every page of a bundle loads.
45
+ */
46
+ export function createModuleGraphCollector(root, { isReference = () => false } = {}) {
47
+ const builds = [];
48
+ const graph = () => ({ version: VERSION, builds });
49
+ return {
50
+ plugin: {
51
+ name: "uf:module-graph",
52
+ // Last, so the bundle it reads is one every other plugin has finished.
53
+ enforce: "post",
54
+ generateBundle(_options, bundle) {
55
+ builds.push(describeBundle(this, root, bundle, isReference));
56
+ },
57
+ },
58
+ graph,
59
+ write(file) {
60
+ mkdirSync(path.dirname(file), { recursive: true });
61
+ writeFileSync(file, `${JSON.stringify(graph())}\n`);
62
+ },
63
+ };
64
+ }
65
+
66
+ /** One bundle, as `generateBundle` sees it. */
67
+ function describeBundle(context, root, bundle, isReference) {
68
+ const ids = typeof context.getModuleIds === "function" ? [...context.getModuleIds()] : [];
69
+ const modules = ids
70
+ .map((id) => {
71
+ const info = context.getModuleInfo(id);
72
+ return {
73
+ id: moduleId(root, id),
74
+ imports: (info?.importedIds ?? []).map((imported) => moduleId(root, imported)),
75
+ dynamicImports: (info?.dynamicallyImportedIds ?? []).map((imported) =>
76
+ moduleId(root, imported),
77
+ ),
78
+ };
79
+ })
80
+ .sort(byKey("id"));
81
+ const entries = new Set();
82
+ const chunks = [];
83
+ for (const output of Object.values(bundle)) {
84
+ if (output.type !== "chunk") {
85
+ continue;
86
+ }
87
+ const facade = output.isEntry ? (output.facadeModuleId ?? null) : null;
88
+ if (facade != null && !isReference(facade.split("?")[0])) {
89
+ entries.add(moduleId(root, facade));
90
+ }
91
+ chunks.push({
92
+ file: output.fileName,
93
+ facade: facade == null ? null : moduleId(root, facade),
94
+ // A module the bundler left no code for was still walked through to
95
+ // reach what it imports, so it stays in `modules` above; it has nothing
96
+ // to weigh, so it is not in the chunk.
97
+ modules: Object.entries(output.modules ?? {})
98
+ .map(([id, rendered]) => ({ id: moduleId(root, id), code: rendered?.code ?? "" }))
99
+ .filter((module) => module.code !== ""),
100
+ });
101
+ }
102
+ return {
103
+ environment: context.environment?.name ?? "client",
104
+ entries: [...entries].sort(),
105
+ modules,
106
+ chunks: chunks.sort(byKey("file")),
107
+ };
108
+ }
109
+
110
+ /**
111
+ * A module id as the analysis spells it: relative to the project root with
112
+ * `/`, without the `\0` a resolved virtual module carries, and with its query.
113
+ *
114
+ * Relative even when it climbs out of the root, as a workspace package does,
115
+ * because an absolute path would make two machines' reports of the same build
116
+ * disagree.
117
+ */
118
+ export function moduleId(root, id) {
119
+ const bare = id.startsWith("\0") ? id.slice(1) : id;
120
+ const at = bare.indexOf("?");
121
+ const file = at === -1 ? bare : bare.slice(0, at);
122
+ if (!path.isAbsolute(file)) {
123
+ return bare;
124
+ }
125
+ const relative = path.relative(root, file);
126
+ if (path.isAbsolute(relative)) {
127
+ return bare;
128
+ }
129
+ return `${relative.split(path.sep).join("/")}${at === -1 ? "" : bare.slice(at)}`;
130
+ }
131
+
132
+ function byKey(key) {
133
+ return (a, b) => (a[key] < b[key] ? -1 : a[key] > b[key] ? 1 : 0);
134
+ }
@@ -1653,10 +1653,11 @@ export function clientModuleSource(appEntry, options = {}) {
1653
1653
  const strictMode = options.strictMode === true ? ", strictMode: true" : "";
1654
1654
  const navigation = options.navigation === "document" ? ', navigation: "document"' : "";
1655
1655
  const mount = options.mount === "render" ? "render" : "hydrate";
1656
+ const routing = routingArgumentSource(options.routing);
1656
1657
  return `import { ${mount} } from "@uniflowed/router/client";
1657
1658
  import { routes, notFound, errors } from ${JSON.stringify(VIRTUAL.routes)};
1658
1659
  import App from ${JSON.stringify(appEntry)};
1659
- ${mount}({ App, routes, notFound, errors${strictMode}${navigation} });
1660
+ ${mount}({ App, routes, notFound, errors${strictMode}${navigation}${routing} });
1660
1661
  `;
1661
1662
  }
1662
1663
 
@@ -1714,16 +1715,18 @@ ${mount}({ App, routes, notFound, errors${strictMode}${navigation} });
1714
1715
  * shorter proof of the paragraph above: the copy the router dispatches and
1715
1716
  * renders with is by construction the copy the host is handed.
1716
1717
  */
1717
- export function serverModuleSource(appEntry) {
1718
+ export function serverModuleSource(appEntry, routing = routingRulesOf({})) {
1718
1719
  return `import {
1719
1720
  createActionDispatcher,
1720
1721
  createDispatcher,
1721
1722
  createMiddlewareRunner,
1722
1723
  createRenderer,
1724
+ installRouting,
1723
1725
  } from "@uniflowed/router/server";
1724
1726
  import { routes, handlers, middleware, notFound, errors } from ${JSON.stringify(VIRTUAL.routes)};
1725
1727
  import { actions } from ${JSON.stringify(VIRTUAL.actions)};
1726
1728
  import App from ${JSON.stringify(appEntry)};
1729
+ ${routingExportSource(routing)}installRouting(routing);
1727
1730
  export { routes, handlers, middleware, notFound, errors };
1728
1731
  export { beginRequest } from "@uniflowed/router/server";
1729
1732
  const renderer = createRenderer({ App, routes, notFound, errors });
@@ -1735,3 +1738,53 @@ export const callAction = createActionDispatcher({ actions });
1735
1738
  export const runMiddleware = createMiddlewareRunner({ middleware });
1736
1739
  `;
1737
1740
  }
1741
+
1742
+ /**
1743
+ * `app.router.redirects`, `rewrites` and `headers`, as the bundle carries them.
1744
+ *
1745
+ * Three lists and nothing else, each present, so a host reads `routing` the one
1746
+ * way whatever the project wrote. Validation is not here: `uf_config` refuses a
1747
+ * rule it cannot read when the file is loaded, with a sentence per spelling,
1748
+ * and `@uniflowed/server`'s `internal/routing.js` is what interprets one.
1749
+ *
1750
+ * @param {{redirects?: unknown[], rewrites?: unknown[], headers?: unknown[]} | undefined} router
1751
+ */
1752
+ export function routingRulesOf(router) {
1753
+ const policy = router?.trailingSlash;
1754
+ return {
1755
+ redirects: Array.isArray(router?.redirects) ? router.redirects : [],
1756
+ rewrites: Array.isArray(router?.rewrites) ? router.rewrites : [],
1757
+ headers: Array.isArray(router?.headers) ? router.headers : [],
1758
+ basePath: typeof router?.basePath === "string" ? router.basePath.replace(/\/+$/, "") : "",
1759
+ trailingSlash: policy === "never" || policy === "always" ? policy : "ignore",
1760
+ };
1761
+ }
1762
+
1763
+ /**
1764
+ * `basePath` and `trailingSlash` as arguments to a client entry's call, or
1765
+ * nothing for a project at the root with the default policy — so a default
1766
+ * project's entry is the module it has always been.
1767
+ *
1768
+ * @param {{basePath?: string, trailingSlash?: string} | undefined} routing
1769
+ */
1770
+ export function routingArgumentSource(routing) {
1771
+ const basePath = routing?.basePath ?? "";
1772
+ const trailingSlash = routing?.trailingSlash ?? "ignore";
1773
+ let source = "";
1774
+ if (basePath !== "") source += `, basePath: ${JSON.stringify(basePath)}`;
1775
+ if (trailingSlash !== "ignore") source += `, trailingSlash: ${JSON.stringify(trailingSlash)}`;
1776
+ return source;
1777
+ }
1778
+
1779
+ /**
1780
+ * The `routing` export of `virtual:uf/server`.
1781
+ *
1782
+ * On the bundle rather than read from `uf.config.js` where a host starts, so a
1783
+ * served build answers with the rules it was built with — `uf start` of last
1784
+ * week's build, and every `--adapter` artefact, carry their own.
1785
+ *
1786
+ * @param {ReturnType<typeof routingRulesOf>} routing
1787
+ */
1788
+ export function routingExportSource(routing) {
1789
+ return `export const routing = ${JSON.stringify(routing)};\n`;
1790
+ }
package/internal/serve.js CHANGED
@@ -156,8 +156,13 @@ async function cacheFor(declared, createCacheStore, where) {
156
156
  * @param {{store?: string, storeDir?: string} | undefined} declared
157
157
  * @param {{root: string, build: string | null}} where
158
158
  */
159
- async function providerFor(declared, { root, build }) {
160
- const named = declared?.store ?? "memory";
159
+ async function providerFor(declared, { root, build, regenerates }) {
160
+ // A build that regenerates pages keeps what it regenerated on disk unless the
161
+ // project named a store. A restart that took every regenerated page back to
162
+ // the build's copy would be a server whose pages went back in time, which is
163
+ // the one thing regeneration is for not doing. `"memory"`, said out loud, is
164
+ // still memory.
165
+ const named = declared?.store ?? (regenerates === true ? "filesystem" : "memory");
161
166
  if (named === "memory") return null;
162
167
  if (build == null) {
163
168
  throw new Error(
@@ -213,12 +218,14 @@ export async function loadBuild({ root, outDir, serverDir }) {
213
218
  const entry = await import(pathToFileURL(entryFile).href);
214
219
  await deployment();
215
220
  const build = await buildIdentity(root, serverDir);
221
+ const regeneration = await readRegeneration(path.resolve(root, serverDir));
216
222
  return {
217
223
  entry,
218
224
  assets: await documentAssetsFor(path.resolve(root, serverDir), manifest),
219
225
  distDir,
220
226
  root,
221
227
  build,
228
+ regeneration,
222
229
  };
223
230
  }
224
231
 
@@ -255,6 +262,39 @@ export async function documentAssetsFor(serverDir, manifest) {
255
262
  }
256
263
  }
257
264
 
265
+ /**
266
+ * The file beside the server bundle that names the pages a build regenerates.
267
+ *
268
+ * Written by `driver.js`'s build, and only for a build that has such a page.
269
+ */
270
+ export const REGENERATION_FILE = "regenerate.json";
271
+
272
+ /**
273
+ * Where a regenerated page's document goes, under the build's output directory.
274
+ *
275
+ * Somewhere no static half answers the page's own URL, which is the point: a
276
+ * file at `dist/posts/a/index.html` would be served by every host before the
277
+ * server saw the request, forever, whatever the page's lifetime said.
278
+ */
279
+ export const REGENERATED_DIRECTORY = "__uf/regenerate";
280
+
281
+ /**
282
+ * The pages this build regenerates, or `undefined` for a build that has none.
283
+ *
284
+ * `undefined` rather than an empty manifest, so a build with nothing to
285
+ * regenerate serves exactly as every build did before regeneration existed.
286
+ */
287
+ export async function readRegeneration(serverDir) {
288
+ let text;
289
+ try {
290
+ text = await readFile(path.join(serverDir, REGENERATION_FILE), "utf8");
291
+ } catch (error) {
292
+ if (error?.code === "ENOENT") return undefined;
293
+ throw error;
294
+ }
295
+ return JSON.parse(text);
296
+ }
297
+
258
298
  /**
259
299
  * What `import()` should be given for a provider a project named.
260
300
  *
@@ -342,7 +382,7 @@ async function readable(file, message) {
342
382
  * the rest moved to `@uniflowed/server`. `uf build --adapter` calls it too,
343
383
  * at build time, and bakes the answer into what it emits.
344
384
  */
345
- export function assetsFromManifest(manifest) {
385
+ export function assetsFromManifest(manifest, base = "") {
346
386
  // `client` by name first. An application React Server Components render
347
387
  // gives the client build one entry per client module as well, and the
348
388
  // document's script is the application's entry, not whichever of those the
@@ -378,10 +418,12 @@ export function assetsFromManifest(manifest) {
378
418
  };
379
419
  collectPreloads(entry);
380
420
 
421
+ // Under `app.router.basePath` when there is one: a prerendered document is
422
+ // written outside Vite's HTML transform, so nothing else would put it there.
381
423
  return {
382
- scripts: [`/${entry.file}`],
383
- styles: [...styles].map((file) => `/${file}`),
384
- preloads: [...preloads].map((file) => `/${file}`),
424
+ scripts: [`${base}/${entry.file}`],
425
+ styles: [...styles].map((file) => `${base}/${file}`),
426
+ preloads: [...preloads].map((file) => `${base}/${file}`),
385
427
  };
386
428
  }
387
429
 
@@ -483,7 +525,7 @@ export async function beginRequest(entry, request) {
483
525
  *
484
526
  * @param {{entry: object, assets: object, cache?: object, root?: string, build?: string | null}} build
485
527
  */
486
- export function createApplicationHandler({ entry, assets, cache, root, build }) {
528
+ export function createApplicationHandler({ entry, assets, cache, root, build, regeneration }) {
487
529
  const ready = deployment().then(
488
530
  async ({ createFetchHandler, createCacheStore, nodeCapabilities }) =>
489
531
  createFetchHandler({
@@ -492,7 +534,12 @@ export function createApplicationHandler({ entry, assets, cache, root, build })
492
534
  cache: await cacheFor(cache, createCacheStore, {
493
535
  root: root ?? process.cwd(),
494
536
  build: build ?? null,
537
+ regenerates: regeneration != null,
495
538
  }),
539
+ // The pages this build regenerates, from the manifest beside the server
540
+ // bundle. Absent for a build with none, which then serves exactly as it
541
+ // did before regeneration existed.
542
+ ...(regeneration == null ? {} : { regeneration }),
496
543
  // `uf preview` and `uf start` are a Node process with a socket, which is
497
544
  // what a deployed `--adapter node` build is too — so a route handler
498
545
  // that streams events answers the same way in the preview it is checked
@@ -528,16 +575,115 @@ export function createStaticHandler({ root }) {
528
575
  * project whose handler path collides with a file in `public/` behaves one way
529
576
  * when it is checked and the other way when it is deployed.
530
577
  *
531
- * @param {{entry: object, assets: object, distDir: string, cache?: object, root?: string, build?: string | null}} build
578
+ * It is `@uniflowed/server/node`'s own `createServeHandler`, the one a deployed
579
+ * `server.js` runs, rather than the two halves composed a second time here.
580
+ * That one also hands the application the build's files when the static half
581
+ * has nothing, which is how a regenerated page starts from the document the
582
+ * build wrote; a composition of its own here would be a `uf start` whose
583
+ * regenerated pages rendered on their first request while a deployment's did
584
+ * not.
585
+ *
586
+ * It is handed the bundle's `routing` too, so `app.router`'s redirects answer
587
+ * before the files and its headers go on whatever answers, here exactly as in
588
+ * a deployment. `uf preview` puts the same two in front of Vite's file
589
+ * middleware as well; see [`answerRouting`].
590
+ *
591
+ * @param {{entry: object, assets: object, distDir: string, cache?: object, root?: string, build?: string | null, regeneration?: object}} build
532
592
  */
533
- export function createServeHandler({ entry, assets, distDir, cache, root, build }) {
534
- const serveStatic = createStaticHandler({ root: distDir });
535
- const application = createApplicationHandler({ entry, assets, cache, root, build });
593
+ export function createServeHandler({ entry, assets, distDir, cache, root, build, regeneration }) {
594
+ const application = createApplicationHandler({ entry, assets, cache, root, build, regeneration });
595
+ const ready = deployment().then(({ createServeHandler: create }) =>
596
+ create({ staticDir: distDir, handle: application, routing: entry.routing }),
597
+ );
536
598
  return async function handle(request) {
537
- return (await serveStatic(request)) ?? (await application(request));
599
+ return (await ready)(request);
538
600
  };
539
601
  }
540
602
 
603
+ /**
604
+ * Whether `routing` has anything to say in front of a file server.
605
+ *
606
+ * Redirects and headers are the two that do; a rewrite is the application's.
607
+ * Asked before a middleware is mounted at all, so a project with no rules pays
608
+ * nothing per request under `uf dev` or `uf preview`.
609
+ *
610
+ * @param {{redirects?: unknown[], headers?: unknown[]} | undefined} routing
611
+ */
612
+ export function answersInFrontOfFiles(routing) {
613
+ return (
614
+ (routing?.redirects?.length ?? 0) > 0 ||
615
+ (routing?.headers?.length ?? 0) > 0 ||
616
+ (routing?.basePath ?? "") !== "" ||
617
+ (routing?.trailingSlash ?? "ignore") !== "ignore"
618
+ );
619
+ }
620
+
621
+ /**
622
+ * `app.router`'s headers and redirects, for a door whose files Vite serves.
623
+ *
624
+ * `uf dev` and `uf preview` mount this in front of Vite's own middleware:
625
+ * the headers are pinned on the Node response, so they survive the
626
+ * `writeHead` Vite's file server writes its own with, and a redirect is
627
+ * answered before any file is looked for. `true` when it answered.
628
+ *
629
+ * @param {object} routing the bundle's `routing`
630
+ * @param {Request} request the address alone; see `toAddressRequest`
631
+ * @param {import("node:http").ServerResponse} response
632
+ */
633
+ export async function answerRouting(routing, request, response) {
634
+ const { admit, headersFor, pinHeaders, send: write } = await deployment();
635
+ pinHeaders(response, headersFor(routing, request));
636
+ // A request outside the base path, the other spelling of a path, or a
637
+ // redirect rule: answered here, before Vite's own base middleware would
638
+ // answer the first in its words rather than uf's.
639
+ const admitted = admit(routing, request);
640
+ if (admitted.kind !== "answer") return false;
641
+ await write(response, admitted.response);
642
+ return true;
643
+ }
644
+
645
+ /**
646
+ * A request [`answerRouting`] let through, spelled so Vite's own middleware
647
+ * recognises it.
648
+ *
649
+ * Vite serves under its `base` with the trailing slash, `/docs/`, and its base
650
+ * middleware answers every other path with a 404 of its own, the bare `/docs`
651
+ * included. But `/docs` is the application's root under `app.router.basePath`,
652
+ * and the only spelling of it unless the trailing-slash policy is `"always"`,
653
+ * which has already answered `/docs` with a `308` by the time this is asked.
654
+ * So a request for exactly the base goes on to Vite as `/docs/`, which Vite
655
+ * takes the base off and hands on as the root. The application is handed the
656
+ * root either way.
657
+ *
658
+ * @param {{basePath?: string} | undefined} routing the bundle's `routing`
659
+ * @param {import("node:http").IncomingMessage} request
660
+ */
661
+ export function forViteBase(routing, request) {
662
+ const base = routing?.basePath ?? "";
663
+ const url = request.url ?? "/";
664
+ if (base === "") return;
665
+ const queryAt = url.indexOf("?");
666
+ const pathname = queryAt === -1 ? url : url.slice(0, queryAt);
667
+ if (pathname === base) {
668
+ request.url = `${base}/${queryAt === -1 ? "" : url.slice(queryAt)}`;
669
+ }
670
+ }
671
+
672
+ /**
673
+ * `app.router.rewrites` for this request, for `uf dev`: the rewritten request,
674
+ * or `null`.
675
+ *
676
+ * `@uniflowed/server`'s `rewriteFor`, which `createFetchHandler` asks for every
677
+ * other front door at the same point — after the files, before the guard.
678
+ *
679
+ * @param {object | undefined} routing the bundle's `routing`
680
+ * @param {Request} request
681
+ */
682
+ export async function rewriteRouting(routing, request) {
683
+ const { rewriteFor } = await deployment();
684
+ return rewriteFor(routing, request);
685
+ }
686
+
541
687
  /**
542
688
  * Whether a file server may answer this request, or uf has to go first.
543
689
  *
@@ -0,0 +1,109 @@
1
+ // @noflow
2
+ //
3
+ // Plain JavaScript: executed by the host that runs Vite, before any transform.
4
+ //
5
+ // What a project needs before its routes can render as React Server Components.
6
+ //
7
+ // `@uniflowed/router` installs beside React 19.2.3, the React that Expo SDK 57
8
+ // and React Native 0.87 ship, and lists `react-server-dom-parcel` as an optional
9
+ // peer (ubugeeei-prod/uf#992). An application rendered from its modules
10
+ // (`app.rsc: false`) needs nothing more. One whose routes render as Server
11
+ // Components, which is the default, needs that package and the React it was
12
+ // released with. Without the package a build fails on an unresolved import
13
+ // somewhere inside the router, and on too old a React it fails later, inside a
14
+ // render, in terms of React's internals. So `uf:flow` asks here once, while Vite
15
+ // reads its configuration, and stops with one sentence that says what the
16
+ // project has and what to change.
17
+ //
18
+ // The router's own Server Components entries refuse the same React at run time
19
+ // (`packages/router/internal/react-version.js`). This is the earlier half of the
20
+ // same rule, for a project that builds with uf.
21
+
22
+ import fs from "node:fs";
23
+ import { createRequire } from "node:module";
24
+ import path from "node:path";
25
+ import { fileURLToPath } from "node:url";
26
+
27
+ /** The oldest React, and `react-server-dom-parcel`, that Server Components render on. */
28
+ export const SERVER_COMPONENTS_REACT = "19.3.0";
29
+
30
+ /**
31
+ * What stops the project at `root` from rendering React Server Components, as a
32
+ * sentence, or `null` when nothing does.
33
+ *
34
+ * Both packages are resolved the way the router's own imports resolve them: from
35
+ * the `@uniflowed/router` the project resolves, or from the one this package
36
+ * depends on when the project names none. A prerelease tag is ignored, so a 19.3
37
+ * canary counts as 19.3.
38
+ *
39
+ * @param {string} root
40
+ * @returns {string | null}
41
+ */
42
+ export function serverComponentsProblem(root) {
43
+ const router =
44
+ resolveFrom(path.join(root, "package.json"), "@uniflowed/router/package.json") ??
45
+ resolveFrom(fileURLToPath(import.meta.url), "@uniflowed/router/package.json") ??
46
+ path.join(root, "package.json");
47
+ const react = versionOf(resolveFrom(router, "react/package.json"));
48
+ const flight = versionOf(resolveFrom(router, "react-server-dom-parcel/package.json"));
49
+
50
+ const found = [];
51
+ if (react == null) {
52
+ found.push("no react");
53
+ } else if (!isRecentEnough(react)) {
54
+ found.push(`React ${react}`);
55
+ }
56
+ if (flight == null) {
57
+ found.push("no react-server-dom-parcel");
58
+ } else if (!isRecentEnough(flight)) {
59
+ found.push(`react-server-dom-parcel ${flight}`);
60
+ }
61
+ if (found.length === 0) {
62
+ return null;
63
+ }
64
+ return (
65
+ "uf: routes render as React Server Components unless `app.rsc` is false, and that needs " +
66
+ `react-server-dom-parcel and React ${SERVER_COMPONENTS_REACT} or newer. This project has ` +
67
+ `${found.join(" and ")}. Install react, react-dom and react-server-dom-parcel at ^19.3.0, ` +
68
+ "or set `app.rsc: false` in uf.config.js to render routes from their modules, which the " +
69
+ "router supports from React 19.2.3."
70
+ );
71
+ }
72
+
73
+ /** Where `specifier` resolves from the file at `from`, or `null`. */
74
+ function resolveFrom(from, specifier) {
75
+ try {
76
+ return createRequire(from).resolve(specifier);
77
+ } catch {
78
+ return null;
79
+ }
80
+ }
81
+
82
+ /** The `version` in the manifest at `file`, or `null`. */
83
+ function versionOf(file) {
84
+ if (file == null) {
85
+ return null;
86
+ }
87
+ try {
88
+ const { version } = JSON.parse(fs.readFileSync(file, "utf8"));
89
+ return typeof version === "string" ? version : null;
90
+ } catch {
91
+ return null;
92
+ }
93
+ }
94
+
95
+ /** Whether `version` is at least `SERVER_COMPONENTS_REACT`, ignoring a prerelease tag. */
96
+ function isRecentEnough(version) {
97
+ const found = /^(\d+)\.(\d+)\.(\d+)/.exec(version);
98
+ if (found == null) {
99
+ return false;
100
+ }
101
+ const needed = SERVER_COMPONENTS_REACT.split(".").map(Number);
102
+ for (let index = 0; index < 3; index += 1) {
103
+ const part = Number(found[index + 1]);
104
+ if (part !== needed[index]) {
105
+ return part > needed[index];
106
+ }
107
+ }
108
+ return true;
109
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniflowed/vite",
3
- "version": "0.0.0-alpha.35",
3
+ "version": "0.0.0-alpha.37",
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",
@@ -34,10 +34,10 @@
34
34
  "dependencies": {
35
35
  "@mdx-js/rollup": "^3.1.1",
36
36
  "@shikijs/rehype": "^3.23.0",
37
- "@uniflowed/host": "0.0.0-alpha.35",
38
- "@uniflowed/router": "0.0.0-alpha.35",
39
- "@uniflowed/server": "0.0.0-alpha.35",
40
- "@uniflowed/validator": "0.0.0-alpha.35",
37
+ "@uniflowed/host": "0.0.0-alpha.37",
38
+ "@uniflowed/router": "0.0.0-alpha.37",
39
+ "@uniflowed/server": "0.0.0-alpha.37",
40
+ "@uniflowed/validator": "0.0.0-alpha.37",
41
41
  "estree-util-value-to-estree": "^3.5.0",
42
42
  "rehype-slug": "^6.0.0",
43
43
  "remark-frontmatter": "^5.0.0",