@uniflowed/vite 0.0.0-alpha.35 → 0.0.0-alpha.37
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/driver.js +263 -50
- package/index.js +132 -15
- package/internal/barrel-imports.js +459 -0
- package/internal/flight.js +125 -11
- package/internal/http.js +21 -2
- package/internal/module-graph.js +134 -0
- package/internal/routes.js +55 -2
- package/internal/serve.js +158 -12
- package/internal/server-components.js +109 -0
- package/package.json +5 -5
|
@@ -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
|
+
}
|
package/internal/routes.js
CHANGED
|
@@ -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
|
-
|
|
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: [
|
|
383
|
-
styles: [...styles].map((file) =>
|
|
384
|
-
preloads: [...preloads].map((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
|
-
*
|
|
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
|
|
535
|
-
const
|
|
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
|
|
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.
|
|
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.
|
|
38
|
-
"@uniflowed/router": "0.0.0-alpha.
|
|
39
|
-
"@uniflowed/server": "0.0.0-alpha.
|
|
40
|
-
"@uniflowed/validator": "0.0.0-alpha.
|
|
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",
|