@uniflowed/vite 0.0.0-alpha.34 → 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 +537 -48
- package/index.js +327 -24
- package/internal/barrel-imports.js +459 -0
- package/internal/flight.js +736 -0
- package/internal/frontmatter.js +33 -0
- package/internal/http.js +21 -2
- package/internal/module-graph.js +134 -0
- package/internal/routes.js +55 -2
- package/internal/serve.js +206 -14
- package/internal/server-components.js +109 -0
- package/internal/worker-builtins.js +170 -0
- package/package.json +9 -7
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// @noflow
|
|
2
|
+
//
|
|
3
|
+
// A document's YAML front matter, as `export const frontmatter`.
|
|
4
|
+
//
|
|
5
|
+
// Plain JavaScript, for the reason `index.js` gives: Vite imports this before
|
|
6
|
+
// any transform runs.
|
|
7
|
+
//
|
|
8
|
+
// This is `remark-mdx-frontmatter` for the one format uf reads, and it replaces
|
|
9
|
+
// that package for one reason. The package imported `toml` 3.0.0 at the top of
|
|
10
|
+
// its module whether or not a document held any TOML, so every project uf
|
|
11
|
+
// scaffolded installed a parser with two high advisories and failed its first
|
|
12
|
+
// `uf audit` (#1009). The TOML half was never reachable from here either:
|
|
13
|
+
// `remark-frontmatter` is given its default, which recognises YAML and nothing
|
|
14
|
+
// else, so a `+++` block was never a front-matter node to parse.
|
|
15
|
+
//
|
|
16
|
+
// For YAML the output is the package's, through the same two helpers it used:
|
|
17
|
+
// the first `yaml` node is parsed and defined as `frontmatter`, and a document
|
|
18
|
+
// with none exports `undefined`.
|
|
19
|
+
|
|
20
|
+
import { valueToEstree } from "estree-util-value-to-estree";
|
|
21
|
+
import { define } from "unist-util-mdx-define";
|
|
22
|
+
import { parse } from "yaml";
|
|
23
|
+
|
|
24
|
+
/** The remark plugin: `export const frontmatter` from a document's YAML. */
|
|
25
|
+
export default function remarkFrontmatterExport() {
|
|
26
|
+
return (tree, file) => {
|
|
27
|
+
const node = tree.children.find((child) => child.type === "yaml");
|
|
28
|
+
const data = node == null ? undefined : parse(node.value);
|
|
29
|
+
define(tree, file, {
|
|
30
|
+
frontmatter: valueToEstree(data, { preserveReferences: true }),
|
|
31
|
+
});
|
|
32
|
+
};
|
|
33
|
+
}
|
package/internal/http.js
CHANGED
|
@@ -25,13 +25,18 @@
|
|
|
25
25
|
* that accepts an upload should not need the whole thing buffered before it
|
|
26
26
|
* starts.
|
|
27
27
|
*
|
|
28
|
+
* `path` names the path and query to build it at, when that is not the one the
|
|
29
|
+
* request line carried — `uf dev` passes the one Vite's base middleware has
|
|
30
|
+
* already taken `app.router.basePath` off.
|
|
31
|
+
*
|
|
28
32
|
* @param {import("node:http").IncomingMessage} incoming
|
|
29
33
|
* @param {{server?: {https?: unknown}} | undefined} config the resolved Vite config
|
|
34
|
+
* @param {string} [path]
|
|
30
35
|
*/
|
|
31
|
-
export async function toRequest(incoming, config) {
|
|
36
|
+
export async function toRequest(incoming, config, path) {
|
|
32
37
|
const host = incoming.headers.host ?? "localhost";
|
|
33
38
|
const protocol = config?.server?.https == null ? "http" : "https";
|
|
34
|
-
const url = new URL(incoming.originalUrl ?? incoming.url ?? "/", `${protocol}://${host}`);
|
|
39
|
+
const url = new URL(path ?? incoming.originalUrl ?? incoming.url ?? "/", `${protocol}://${host}`);
|
|
35
40
|
|
|
36
41
|
const headers = new Headers();
|
|
37
42
|
for (const [name, value] of Object.entries(incoming.headers)) {
|
|
@@ -52,6 +57,20 @@ export async function toRequest(incoming, config) {
|
|
|
52
57
|
return new Request(url, init);
|
|
53
58
|
}
|
|
54
59
|
|
|
60
|
+
/**
|
|
61
|
+
* A Node request as a `Request` that carries its address and nothing else.
|
|
62
|
+
*
|
|
63
|
+
* For the questions asked about a URL alone — `app.router`'s redirects and
|
|
64
|
+
* headers, in front of Vite's own middleware — which must not wrap the body a
|
|
65
|
+
* later middleware turns into the `Request` the application reads.
|
|
66
|
+
*
|
|
67
|
+
* @param {import("node:http").IncomingMessage} incoming
|
|
68
|
+
*/
|
|
69
|
+
export function toAddressRequest(incoming) {
|
|
70
|
+
const host = incoming.headers.host ?? "localhost";
|
|
71
|
+
return new Request(new URL(incoming.originalUrl ?? incoming.url ?? "/", `http://${host}`));
|
|
72
|
+
}
|
|
73
|
+
|
|
55
74
|
/**
|
|
56
75
|
* Write a `Response` to a Node response.
|
|
57
76
|
*
|
|
@@ -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,7 +218,81 @@ 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);
|
|
216
|
-
|
|
221
|
+
const regeneration = await readRegeneration(path.resolve(root, serverDir));
|
|
222
|
+
return {
|
|
223
|
+
entry,
|
|
224
|
+
assets: await documentAssetsFor(path.resolve(root, serverDir), manifest),
|
|
225
|
+
distDir,
|
|
226
|
+
root,
|
|
227
|
+
build,
|
|
228
|
+
regeneration,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** What `uf build` records a document's tags in, beside the server bundle. */
|
|
233
|
+
export const DOCUMENT_ASSETS_FILE = "uf-document-assets.json";
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* The tags a served document needs: the ones `uf build` recorded, or — for a
|
|
237
|
+
* build from before it recorded them — the client manifest's.
|
|
238
|
+
*
|
|
239
|
+
* Recorded rather than recomputed, because the client manifest is no longer the
|
|
240
|
+
* whole answer. An application React Server Components render links the
|
|
241
|
+
* stylesheets its rsc graph emitted and the ones each client module's chunk
|
|
242
|
+
* carries, and neither is reachable from the client entry the manifest is walked
|
|
243
|
+
* from — so a server that recomputed the tags rendered every page without the
|
|
244
|
+
* stylesheets its prerendered pages had. `uf start`, `uf preview`, `--adapter`
|
|
245
|
+
* and `--compile` all read them from here.
|
|
246
|
+
*
|
|
247
|
+
* @param {string} serverDir absolute path of the server bundle's directory
|
|
248
|
+
* @param {object} manifest the client build's Vite manifest
|
|
249
|
+
*/
|
|
250
|
+
export async function documentAssetsFor(serverDir, manifest) {
|
|
251
|
+
const file = path.join(serverDir, DOCUMENT_ASSETS_FILE);
|
|
252
|
+
let recorded;
|
|
253
|
+
try {
|
|
254
|
+
recorded = await readFile(file, "utf8");
|
|
255
|
+
} catch {
|
|
256
|
+
return assetsFromManifest(manifest);
|
|
257
|
+
}
|
|
258
|
+
try {
|
|
259
|
+
return JSON.parse(recorded);
|
|
260
|
+
} catch {
|
|
261
|
+
throw new Error(`uf: ${file} is not the JSON \`uf build\` writes; run \`uf build\` again`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
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);
|
|
217
296
|
}
|
|
218
297
|
|
|
219
298
|
/**
|
|
@@ -303,8 +382,15 @@ async function readable(file, message) {
|
|
|
303
382
|
* the rest moved to `@uniflowed/server`. `uf build --adapter` calls it too,
|
|
304
383
|
* at build time, and bakes the answer into what it emits.
|
|
305
384
|
*/
|
|
306
|
-
export function assetsFromManifest(manifest) {
|
|
307
|
-
|
|
385
|
+
export function assetsFromManifest(manifest, base = "") {
|
|
386
|
+
// `client` by name first. An application React Server Components render
|
|
387
|
+
// gives the client build one entry per client module as well, and the
|
|
388
|
+
// document's script is the application's entry, not whichever of those the
|
|
389
|
+
// manifest happens to list first.
|
|
390
|
+
const chunks = Object.values(manifest);
|
|
391
|
+
const entry =
|
|
392
|
+
chunks.find((chunk) => chunk.isEntry && chunk.name === "client") ??
|
|
393
|
+
chunks.find((chunk) => chunk.isEntry);
|
|
308
394
|
if (entry == null) throw new Error("uf: the client manifest has no entry chunk");
|
|
309
395
|
|
|
310
396
|
const styles = new Set(entry.css ?? []);
|
|
@@ -332,10 +418,12 @@ export function assetsFromManifest(manifest) {
|
|
|
332
418
|
};
|
|
333
419
|
collectPreloads(entry);
|
|
334
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.
|
|
335
423
|
return {
|
|
336
|
-
scripts: [
|
|
337
|
-
styles: [...styles].map((file) =>
|
|
338
|
-
preloads: [...preloads].map((file) =>
|
|
424
|
+
scripts: [`${base}/${entry.file}`],
|
|
425
|
+
styles: [...styles].map((file) => `${base}/${file}`),
|
|
426
|
+
preloads: [...preloads].map((file) => `${base}/${file}`),
|
|
339
427
|
};
|
|
340
428
|
}
|
|
341
429
|
|
|
@@ -437,7 +525,7 @@ export async function beginRequest(entry, request) {
|
|
|
437
525
|
*
|
|
438
526
|
* @param {{entry: object, assets: object, cache?: object, root?: string, build?: string | null}} build
|
|
439
527
|
*/
|
|
440
|
-
export function createApplicationHandler({ entry, assets, cache, root, build }) {
|
|
528
|
+
export function createApplicationHandler({ entry, assets, cache, root, build, regeneration }) {
|
|
441
529
|
const ready = deployment().then(
|
|
442
530
|
async ({ createFetchHandler, createCacheStore, nodeCapabilities }) =>
|
|
443
531
|
createFetchHandler({
|
|
@@ -446,7 +534,12 @@ export function createApplicationHandler({ entry, assets, cache, root, build })
|
|
|
446
534
|
cache: await cacheFor(cache, createCacheStore, {
|
|
447
535
|
root: root ?? process.cwd(),
|
|
448
536
|
build: build ?? null,
|
|
537
|
+
regenerates: regeneration != null,
|
|
449
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 }),
|
|
450
543
|
// `uf preview` and `uf start` are a Node process with a socket, which is
|
|
451
544
|
// what a deployed `--adapter node` build is too — so a route handler
|
|
452
545
|
// that streams events answers the same way in the preview it is checked
|
|
@@ -482,16 +575,115 @@ export function createStaticHandler({ root }) {
|
|
|
482
575
|
* project whose handler path collides with a file in `public/` behaves one way
|
|
483
576
|
* when it is checked and the other way when it is deployed.
|
|
484
577
|
*
|
|
485
|
-
*
|
|
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
|
|
486
592
|
*/
|
|
487
|
-
export function createServeHandler({ entry, assets, distDir, cache, root, build }) {
|
|
488
|
-
const
|
|
489
|
-
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
|
+
);
|
|
490
598
|
return async function handle(request) {
|
|
491
|
-
return (await
|
|
599
|
+
return (await ready)(request);
|
|
492
600
|
};
|
|
493
601
|
}
|
|
494
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
|
+
|
|
495
687
|
/**
|
|
496
688
|
* Whether a file server may answer this request, or uf has to go first.
|
|
497
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
|
+
}
|