@uniflowed/vite 0.0.0-alpha.1 → 0.0.0-alpha.11
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 +932 -93
- package/index.js +384 -26
- package/internal/assets.js +396 -0
- package/internal/config.js +22 -7
- package/internal/events.js +22 -0
- package/internal/flow-grammar-shim.js +241 -0
- package/internal/flow-keywords.js +505 -0
- package/internal/highlight.js +181 -0
- package/internal/http.js +79 -0
- package/internal/refresh-runtime.js +221 -246
- package/internal/refresh.js +2 -0
- package/internal/routes.js +391 -34
- package/internal/rsc.js +151 -0
- package/internal/serve.js +345 -0
- package/merge.js +87 -0
- package/package.json +10 -10
- package/bun-preload.js +0 -22
- package/internal/node-hooks.js +0 -111
- package/register.js +0 -11
- package/transform.js +0 -187
package/internal/rsc.js
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// @noflow
|
|
2
|
+
//
|
|
3
|
+
// Plain JavaScript: executed by the host that runs Vite, before any transform.
|
|
4
|
+
//
|
|
5
|
+
// The server/client split, as the bundler applies it.
|
|
6
|
+
//
|
|
7
|
+
// `crates/uf_rsc` decides which modules a `"use client"` boundary is reachable
|
|
8
|
+
// from. That answer used to reach nothing: `virtual:uf/routes` emitted
|
|
9
|
+
// `page: () => import(<file>)` for every route, `virtual:uf/client` imported
|
|
10
|
+
// that table, and so every page in the application was a chunk of the *client*
|
|
11
|
+
// bundle whether or not a browser had anything to do with it.
|
|
12
|
+
//
|
|
13
|
+
// This module is the first thing that reads the answer. What is done with it
|
|
14
|
+
// is in `routesModuleSource`, including the one thing a dropped route keeps.
|
|
15
|
+
//
|
|
16
|
+
// # Why the unit is a route and not a module
|
|
17
|
+
//
|
|
18
|
+
// Dropping a single Server Component from the client bundle is what Next.js
|
|
19
|
+
// does, and it works there because the browser is handed a Flight payload
|
|
20
|
+
// describing the tree the server rendered. uf has no such payload yet:
|
|
21
|
+
// `packages/router/client.js` hydrates by re-rendering the matched tree from
|
|
22
|
+
// the same modules the server rendered it from, so a module missing from the
|
|
23
|
+
// client bundle is a module React cannot hydrate. What *can* be dropped is a
|
|
24
|
+
// route the browser never renders at all — one where no client boundary is
|
|
25
|
+
// reachable from the page, its layouts, its loading fallbacks or the
|
|
26
|
+
// boundaries that cover it. Nothing under it is ever re-rendered in the
|
|
27
|
+
// browser, so nothing under it has to be shipped. See ubugeeei-prod/uf#350.
|
|
28
|
+
//
|
|
29
|
+
// # Why "unknown" means "ship it"
|
|
30
|
+
//
|
|
31
|
+
// The analysis scans `.js`. A page written as `.mdx`, a `.jsx` module, a file
|
|
32
|
+
// past the scanner's size limit: none of them is in the manifest, and the
|
|
33
|
+
// honest reading of a module the analysis never saw is that it might reach a
|
|
34
|
+
// boundary. Every unknown answers `true`, so the split can only ever remove a
|
|
35
|
+
// route uf has positively decided needs no browser — and a manifest that is
|
|
36
|
+
// missing, unreadable, or written by an older uf removes nothing at all.
|
|
37
|
+
|
|
38
|
+
import { readFileSync } from "node:fs";
|
|
39
|
+
import path from "node:path";
|
|
40
|
+
|
|
41
|
+
/** Environment variable naming the manifest, set by `uf build` and `uf dev`. */
|
|
42
|
+
export const RSC_MANIFEST_ENV = "UF_RSC_MANIFEST";
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The manifest schema this understands.
|
|
46
|
+
*
|
|
47
|
+
* Version 1 published the client boundaries and nothing that said which
|
|
48
|
+
* modules sat *above* one, so it cannot answer the question this module asks.
|
|
49
|
+
* An older manifest is therefore refused rather than read optimistically: a
|
|
50
|
+
* missing `proximity` would read as `undefined`, compare unequal to
|
|
51
|
+
* `"reaches-boundary"`, and quietly drop every route from the client bundle.
|
|
52
|
+
*/
|
|
53
|
+
const SUPPORTED_VERSION = 2;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Read the RSC manifest, or `null` when there is nothing usable to read.
|
|
57
|
+
*
|
|
58
|
+
* Never throws. The split is an optimisation over a build that is already
|
|
59
|
+
* correct without it, so no failure here may be a failure of the build.
|
|
60
|
+
*
|
|
61
|
+
* @param {string | undefined} file absolute path, from the environment
|
|
62
|
+
*/
|
|
63
|
+
export function readRscManifest(file) {
|
|
64
|
+
if (file == null || file === "") return null;
|
|
65
|
+
let parsed;
|
|
66
|
+
try {
|
|
67
|
+
parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
68
|
+
} catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
if (parsed == null || typeof parsed !== "object") return null;
|
|
72
|
+
if (parsed.version !== SUPPORTED_VERSION || !Array.isArray(parsed.modules)) return null;
|
|
73
|
+
return parsed;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Which modules the browser has to be able to evaluate, keyed by project path.
|
|
78
|
+
*
|
|
79
|
+
* A `"use client"` module is a client bundle root by definition, and
|
|
80
|
+
* `proximity` never says so about it — it is the far side of the boundary
|
|
81
|
+
* rather than a module above one — so both halves are asked. This mirrors
|
|
82
|
+
* `RscModule::requires_client_bundle` in `crates/uf_rsc/src/graph.rs`.
|
|
83
|
+
*/
|
|
84
|
+
function clientModules(manifest) {
|
|
85
|
+
const modules = new Map();
|
|
86
|
+
for (const module of manifest.modules) {
|
|
87
|
+
if (module == null || typeof module.path !== "string") continue;
|
|
88
|
+
modules.set(
|
|
89
|
+
module.path,
|
|
90
|
+
module.environment === "client" || module.proximity === "reaches-boundary",
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
return modules;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Whether `boundary`'s route path covers `route`'s.
|
|
98
|
+
*
|
|
99
|
+
* Deliberately "covers" and not "is nearest to". `packages/router` picks the
|
|
100
|
+
* nearest not-found and error boundary above a path at render time; asking the
|
|
101
|
+
* same question here would be a second implementation of that rule, and the
|
|
102
|
+
* two would disagree the first time either moved. Every boundary that could
|
|
103
|
+
* apply is counted instead, which can only decide that more routes need the
|
|
104
|
+
* browser than strictly do.
|
|
105
|
+
*/
|
|
106
|
+
function covers(boundaryPath, routePath) {
|
|
107
|
+
return (
|
|
108
|
+
boundaryPath === "/" || routePath === boundaryPath || routePath.startsWith(`${boundaryPath}/`)
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Build the predicate `routesModuleSource` asks about each route.
|
|
114
|
+
*
|
|
115
|
+
* Returns `(route) => boolean`: true when the route's page module belongs in
|
|
116
|
+
* the client bundle. With no manifest every route answers true, which is the
|
|
117
|
+
* whole table and exactly what the build emitted before this existed.
|
|
118
|
+
*
|
|
119
|
+
* @param {object | null} manifest from {@link readRscManifest}
|
|
120
|
+
* @param {string} root absolute project root
|
|
121
|
+
* @param {{notFound?: Array<object>, errors?: Array<object>}} [boundaries]
|
|
122
|
+
* the scanned table, so a boundary that needs the browser keeps the routes
|
|
123
|
+
* it covers in the client bundle
|
|
124
|
+
*/
|
|
125
|
+
export function clientRouteFilter(manifest, root, boundaries = {}) {
|
|
126
|
+
if (manifest == null) return () => true;
|
|
127
|
+
const modules = clientModules(manifest);
|
|
128
|
+
|
|
129
|
+
const needed = (file) => {
|
|
130
|
+
if (typeof file !== "string") return true;
|
|
131
|
+
const relative = path.relative(root, file).split(path.sep).join("/");
|
|
132
|
+
const answer = modules.get(relative);
|
|
133
|
+
return answer === undefined ? true : answer;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const notFound = boundaries.notFound ?? [];
|
|
137
|
+
const errors = boundaries.errors ?? [];
|
|
138
|
+
|
|
139
|
+
return (route) => {
|
|
140
|
+
if (needed(route.page)) return true;
|
|
141
|
+
if (route.layouts.some(needed)) return true;
|
|
142
|
+
if ((route.loading ?? []).some((entry) => needed(entry.module))) return true;
|
|
143
|
+
for (const boundary of notFound) {
|
|
144
|
+
if (covers(boundary.path, route.path) && needed(boundary.page)) return true;
|
|
145
|
+
}
|
|
146
|
+
for (const boundary of errors) {
|
|
147
|
+
if (covers(boundary.path, route.path) && needed(boundary.module)) return true;
|
|
148
|
+
}
|
|
149
|
+
return false;
|
|
150
|
+
};
|
|
151
|
+
}
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
// @noflow
|
|
2
|
+
//
|
|
3
|
+
// Plain JavaScript: executed by the host that serves a build.
|
|
4
|
+
//
|
|
5
|
+
// Serving what `uf build` wrote — one request handler, behind two front doors.
|
|
6
|
+
//
|
|
7
|
+
// `uf build` writes three things: a client bundle and prerendered HTML in
|
|
8
|
+
// `dist/`, and a server bundle in `.uf/build/server/server.js` that exports
|
|
9
|
+
// `render`, `dispatch`, `runMiddleware`, `routes`, `middleware`, `notFound`
|
|
10
|
+
// and `errors`. This module finds them, reads the client manifest, and hands
|
|
11
|
+
// both to the handler `uf preview` and `uf start` mount.
|
|
12
|
+
//
|
|
13
|
+
// `uf preview` and `uf start` are the two front doors, and they share
|
|
14
|
+
// everything below on purpose. A preview whose answers differ from the
|
|
15
|
+
// production server's is worse than no preview, because it is checked and
|
|
16
|
+
// believed. The difference between the two commands is which socket the
|
|
17
|
+
// handler is bolted to, not what it decides:
|
|
18
|
+
//
|
|
19
|
+
// preview — Vite's own preview server, with this handler behind its static
|
|
20
|
+
// middleware, so `vite.preview.proxy`, `vite.preview.https`,
|
|
21
|
+
// `headers` and `cors` are in effect and what is being checked is
|
|
22
|
+
// the build *as Vite serves it*.
|
|
23
|
+
// start — `node:http`, with no bundler in the process, because a host
|
|
24
|
+
// running a production build should not need Vite installed to
|
|
25
|
+
// answer a request.
|
|
26
|
+
//
|
|
27
|
+
// # Where the answering actually happens
|
|
28
|
+
//
|
|
29
|
+
// Not here, any more. Every decision about *what* a request is answered with
|
|
30
|
+
// lives in `@uniflowed/server` — `@uniflowed/server/fetch` for the application
|
|
31
|
+
// half and `@uniflowed/server/node` for the files and the socket — and this
|
|
32
|
+
// module is the part that is genuinely Vite's: finding the build on disk and
|
|
33
|
+
// reading the manifest a Vite build wrote.
|
|
34
|
+
//
|
|
35
|
+
// It moved because of `uf build --adapter`. `tests/library/serve.test.js` said
|
|
36
|
+
// what was wrong with the old arrangement while it was still the only one:
|
|
37
|
+
// "`internal/serve.js` is the seam a deploy adapter will need, and naming it
|
|
38
|
+
// in `exports` before one exists would be promising an interface nothing has
|
|
39
|
+
// used yet." An adapter exists now, and it may not import this package —
|
|
40
|
+
// `@uniflowed/vite` is the bundler, and the whole claim of deployable output
|
|
41
|
+
// is that the host needs neither the bundler nor the toolchain. So the seam is
|
|
42
|
+
// a package export of `@uniflowed/server`, and `uf preview`, `uf start` and
|
|
43
|
+
// every adapter now answer out of one implementation instead of copies that
|
|
44
|
+
// agree until they do not.
|
|
45
|
+
//
|
|
46
|
+
// # Why those imports are dynamic
|
|
47
|
+
//
|
|
48
|
+
// `@uniflowed/server` is Flow, and `driver.js` registers the loader hooks that
|
|
49
|
+
// make Flow importable *in its body* — after every static import in this graph
|
|
50
|
+
// has already been evaluated. So they are reached the same way the server
|
|
51
|
+
// bundle is: with `await import`, from [`loadBuild`], which is the point at
|
|
52
|
+
// which this process stops being plain JavaScript and starts being the
|
|
53
|
+
// project's.
|
|
54
|
+
//
|
|
55
|
+
// # Who owns the request
|
|
56
|
+
//
|
|
57
|
+
// The host does, and none of the three handlers below: each of them has a
|
|
58
|
+
// `Response` in hand rather than a response on the wire, and what `after()`
|
|
59
|
+
// promises is the wire. [`withRequest`] is the shape for a caller that writes
|
|
60
|
+
// into a Node response itself — `uf dev` and `uf preview` — and
|
|
61
|
+
// `@uniflowed/server/node`'s `nodeListener` does the same thing for `uf start`
|
|
62
|
+
// and for the `server.js` an adapter writes. Each of them begins the request
|
|
63
|
+
// with `entry.beginRequest`, runs the whole of answering it inside `run`, and
|
|
64
|
+
// settles it on the line after the last byte.
|
|
65
|
+
//
|
|
66
|
+
// It has to be the *entry's* `beginRequest` rather than one imported here: the
|
|
67
|
+
// request lives in an `AsyncLocalStorage` belonging to one copy of
|
|
68
|
+
// `@uniflowed/server`, and the copy that matters is the one inside the
|
|
69
|
+
// application bundle. A host that resolved its own would begin a request the
|
|
70
|
+
// application cannot see, and nothing would fail loudly — the guard would run,
|
|
71
|
+
// the page would render, and every `cookies()` in it would throw as though no
|
|
72
|
+
// host had run at all. See ubugeeei-prod/uf#389.
|
|
73
|
+
|
|
74
|
+
import { readFile, stat } from "node:fs/promises";
|
|
75
|
+
import path from "node:path";
|
|
76
|
+
import { pathToFileURL } from "node:url";
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* `@uniflowed/server`'s two halves, loaded once.
|
|
80
|
+
*
|
|
81
|
+
* Cached as the promise rather than the modules, so two concurrent callers
|
|
82
|
+
* share one import rather than racing to start two.
|
|
83
|
+
*/
|
|
84
|
+
let deploymentModules = null;
|
|
85
|
+
function deployment() {
|
|
86
|
+
deploymentModules ??= Promise.all([
|
|
87
|
+
import("@uniflowed/server/fetch"),
|
|
88
|
+
import("@uniflowed/server/node"),
|
|
89
|
+
import("@uniflowed/server/cache"),
|
|
90
|
+
]).then(([application, host, cache]) => ({ ...application, ...host, ...cache }));
|
|
91
|
+
return deploymentModules;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* The cache `rendering.cache` describes, or `undefined` for no cache at all.
|
|
96
|
+
*
|
|
97
|
+
* `undefined` rather than a store with both switches off, and the difference is
|
|
98
|
+
* visible from an application: a handler with no `cache` installs nothing on
|
|
99
|
+
* the request, so `revalidateTag()` raises "there is not one here" instead of
|
|
100
|
+
* reporting that it expired nothing. A project that turned the cache off should
|
|
101
|
+
* be told that it did, not handed a cache that quietly does nothing.
|
|
102
|
+
*
|
|
103
|
+
* One store per server process, built when the handler is, which is the whole
|
|
104
|
+
* of what "in memory, per process" means in practice: `uf preview` and
|
|
105
|
+
* `uf start` each hold one, and two of them running at once share nothing.
|
|
106
|
+
*
|
|
107
|
+
* The store's own options are not configurable from `uf.config.js` and are not
|
|
108
|
+
* named here: `rendering.cache` has four keys and no fifth, and a parameter
|
|
109
|
+
* threaded through for a setting nobody can set would be the shape of
|
|
110
|
+
* configurability with none of the substance.
|
|
111
|
+
*
|
|
112
|
+
* @param {{route?: boolean, fetch?: boolean} | undefined} declared
|
|
113
|
+
* @param {(options?: object) => object} createCacheStore
|
|
114
|
+
*/
|
|
115
|
+
function cacheFor(declared, createCacheStore) {
|
|
116
|
+
const route = declared?.route === true;
|
|
117
|
+
const fetchCache = declared?.fetch === true;
|
|
118
|
+
if (!route && !fetchCache) return undefined;
|
|
119
|
+
return { store: createCacheStore(), route, fetch: fetchCache };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Everything a served build consists of.
|
|
124
|
+
*
|
|
125
|
+
* Read once at startup rather than per request: the manifest does not change
|
|
126
|
+
* while the server runs, and importing the server bundle again per request
|
|
127
|
+
* would re-evaluate every module in the application.
|
|
128
|
+
*
|
|
129
|
+
* `@uniflowed/server` is loaded here too, and not lazily on the first request:
|
|
130
|
+
* a missing or broken install should fail the command that starts the server,
|
|
131
|
+
* with the message the import raises, rather than a minute later inside
|
|
132
|
+
* whichever request happened to arrive first.
|
|
133
|
+
*
|
|
134
|
+
* @param {{root: string, outDir: string, serverDir: string}} build
|
|
135
|
+
*/
|
|
136
|
+
export async function loadBuild({ root, outDir, serverDir }) {
|
|
137
|
+
const distDir = path.resolve(root, outDir);
|
|
138
|
+
const entryFile = path.join(path.resolve(root, serverDir), "server.js");
|
|
139
|
+
|
|
140
|
+
// Named separately, because the two failures have different fixes and a
|
|
141
|
+
// combined "run uf build" would be wrong for one of them: a `dist/` with no
|
|
142
|
+
// server bundle beside it is what a `.uf/` that was cleaned looks like.
|
|
143
|
+
await readable(entryFile, `the server bundle is missing at ${entryFile}`);
|
|
144
|
+
const manifestFile = path.join(distDir, ".vite", "manifest.json");
|
|
145
|
+
await readable(manifestFile, `the client manifest is missing at ${manifestFile}`);
|
|
146
|
+
|
|
147
|
+
const manifest = JSON.parse(await readFile(manifestFile, "utf8"));
|
|
148
|
+
const entry = await import(pathToFileURL(entryFile).href);
|
|
149
|
+
await deployment();
|
|
150
|
+
return { entry, assets: assetsFromManifest(manifest), distDir };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function readable(file, message) {
|
|
154
|
+
try {
|
|
155
|
+
await stat(file);
|
|
156
|
+
} catch {
|
|
157
|
+
throw new Error(`uf: ${message}; run \`uf build\` first`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* The tags a rendered document needs, from the client build's manifest.
|
|
163
|
+
*
|
|
164
|
+
* Two walks over the manifest, because the two answers are different. A
|
|
165
|
+
* `modulepreload` is worth emitting only for a chunk this document will
|
|
166
|
+
* certainly load, which is the entry's *static* imports. A stylesheet has to
|
|
167
|
+
* be emitted for anything the page might render, and the router loads every
|
|
168
|
+
* route module dynamically — so a stylesheet imported by a layout is reached
|
|
169
|
+
* through `dynamicImports` and through nothing else. Following only the static
|
|
170
|
+
* graph, as this did, meant a layout could import a stylesheet and the built
|
|
171
|
+
* HTML would silently ship without it.
|
|
172
|
+
*
|
|
173
|
+
* The cost is that a project with per-route stylesheets links all of them on
|
|
174
|
+
* every page. Narrowing that needs the route table to say which chunk each
|
|
175
|
+
* route came from, which the manifest alone cannot tell us.
|
|
176
|
+
*
|
|
177
|
+
* The entry is found by its `isEntry` flag rather than by key, because a
|
|
178
|
+
* virtual module's manifest key is an implementation detail of the bundler.
|
|
179
|
+
*
|
|
180
|
+
* This is the one piece of serving a build that is genuinely Vite's — a Vite
|
|
181
|
+
* manifest, read the way Vite writes it — which is why it stayed behind when
|
|
182
|
+
* the rest moved to `@uniflowed/server`. `uf build --adapter` calls it too,
|
|
183
|
+
* at build time, and bakes the answer into what it emits.
|
|
184
|
+
*/
|
|
185
|
+
export function assetsFromManifest(manifest) {
|
|
186
|
+
const entry = Object.values(manifest).find((chunk) => chunk.isEntry);
|
|
187
|
+
if (entry == null) throw new Error("uf: the client manifest has no entry chunk");
|
|
188
|
+
|
|
189
|
+
const styles = new Set(entry.css ?? []);
|
|
190
|
+
const seen = new Set();
|
|
191
|
+
const collectStyles = (chunk) => {
|
|
192
|
+
for (const imported of [...(chunk.imports ?? []), ...(chunk.dynamicImports ?? [])]) {
|
|
193
|
+
if (seen.has(imported)) continue;
|
|
194
|
+
seen.add(imported);
|
|
195
|
+
const dependency = manifest[imported];
|
|
196
|
+
if (dependency == null) continue;
|
|
197
|
+
for (const css of dependency.css ?? []) styles.add(css);
|
|
198
|
+
collectStyles(dependency);
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
collectStyles(entry);
|
|
202
|
+
|
|
203
|
+
const preloads = new Set();
|
|
204
|
+
const collectPreloads = (chunk) => {
|
|
205
|
+
for (const imported of chunk.imports ?? []) {
|
|
206
|
+
const dependency = manifest[imported];
|
|
207
|
+
if (dependency == null || preloads.has(dependency.file)) continue;
|
|
208
|
+
preloads.add(dependency.file);
|
|
209
|
+
collectPreloads(dependency);
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
collectPreloads(entry);
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
scripts: [`/${entry.file}`],
|
|
216
|
+
styles: [...styles].map((file) => `/${file}`),
|
|
217
|
+
preloads: [...preloads].map((file) => `/${file}`),
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Answer one request inside it, and settle it when the answer has been written.
|
|
223
|
+
*
|
|
224
|
+
* `body` is everything that decides the response *and writes it*; this is the
|
|
225
|
+
* line after. `settle` is in a `finally` because a request that failed is
|
|
226
|
+
* still a request that happened: a middleware that logged the arrival is owed
|
|
227
|
+
* its callback whether the render threw or not, and `drainDeferred` already
|
|
228
|
+
* reports a failing task rather than propagating it.
|
|
229
|
+
*
|
|
230
|
+
* `entry.beginRequest` and not an import: the request lives in an
|
|
231
|
+
* `AsyncLocalStorage` belonging to one copy of `@uniflowed/server`, and the
|
|
232
|
+
* copy that matters is the one inside the application bundle. See
|
|
233
|
+
* `serverModuleSource` in `./routes.js`.
|
|
234
|
+
*
|
|
235
|
+
* The one case this cannot be exact about is a request uf hands back rather
|
|
236
|
+
* than answers: a caller whose `catch` is `next(error)` gives the response to
|
|
237
|
+
* Vite's chain, which writes a 500 at a moment nothing here can observe, so
|
|
238
|
+
* such a request settles when uf lets go of it. `nodeListener` and the
|
|
239
|
+
* compiled binary write their own failures and settle after them. It is worth
|
|
240
|
+
* naming rather than papering over, and it is the failure path of a request
|
|
241
|
+
* that already went wrong — not the ordinary one this exists for.
|
|
242
|
+
*
|
|
243
|
+
* @param {{beginRequest: (request: Request) => {run: <T>(body: () => Promise<T>) => Promise<T>, settle: () => Promise<void>}}} entry
|
|
244
|
+
* @param {Request} request
|
|
245
|
+
* @param {() => Promise<mixed>} body
|
|
246
|
+
*/
|
|
247
|
+
export async function withRequest(entry, request, body) {
|
|
248
|
+
const { run, settle } = entry.beginRequest(request);
|
|
249
|
+
try {
|
|
250
|
+
return await run(body);
|
|
251
|
+
} finally {
|
|
252
|
+
await settle();
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* The application half: route handlers, then rendering.
|
|
258
|
+
*
|
|
259
|
+
* `@uniflowed/server/fetch`'s `createFetchHandler`, reached through the
|
|
260
|
+
* dynamic import above. Kept as a function here — rather than making every
|
|
261
|
+
* caller await the module — because the two servers construct their handler
|
|
262
|
+
* before they take a socket, and an `await` in that position would put the
|
|
263
|
+
* import between the port and the first request rather than before both.
|
|
264
|
+
*
|
|
265
|
+
* It must be called inside a request its caller began; it begins none, because
|
|
266
|
+
* it has a `Response` in hand and not a response on the wire. A caller that
|
|
267
|
+
* forgets is not left to discover it: `entry.runMiddleware` refuses outside a
|
|
268
|
+
* request and names what establishes one. See "Who owns the request" above.
|
|
269
|
+
*
|
|
270
|
+
* `cache` is `rendering.cache` from `uf.config.js`, straight through: this is
|
|
271
|
+
* the point where four switches that used to reach a JSON file and nothing else
|
|
272
|
+
* become a store a request can hit. See ubugeeei-prod/uf#277.
|
|
273
|
+
*
|
|
274
|
+
* @param {{entry: object, assets: object, cache?: object}} build
|
|
275
|
+
*/
|
|
276
|
+
export function createApplicationHandler({ entry, assets, cache }) {
|
|
277
|
+
const ready = deployment().then(({ createFetchHandler, createCacheStore }) =>
|
|
278
|
+
createFetchHandler({
|
|
279
|
+
app: entry,
|
|
280
|
+
document: assets,
|
|
281
|
+
cache: cacheFor(cache, createCacheStore),
|
|
282
|
+
}),
|
|
283
|
+
);
|
|
284
|
+
return async function handle(request) {
|
|
285
|
+
return (await ready)(request);
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* The static half: a file under `root`, or `null` for the caller to carry on.
|
|
291
|
+
*
|
|
292
|
+
* `@uniflowed/server/node`'s, for the same reason as above: what a deployment
|
|
293
|
+
* runs and what `uf start` runs have to be the same code, not the same idea.
|
|
294
|
+
*/
|
|
295
|
+
export function createStaticHandler({ root }) {
|
|
296
|
+
const ready = deployment().then(({ createStaticHandler: create }) => create({ root }));
|
|
297
|
+
return async function serveStatic(request) {
|
|
298
|
+
return (await ready)(request);
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Static files, then the application: the whole of what a built uf app serves.
|
|
304
|
+
*
|
|
305
|
+
* Static first, and that ordering is a compatibility requirement rather than a
|
|
306
|
+
* preference. Vite's preview server runs its own file middleware before
|
|
307
|
+
* anything added afterwards can see the request, so `uf preview` serves a file
|
|
308
|
+
* first whether or not this agrees — and `uf start` disagreeing would mean a
|
|
309
|
+
* project whose handler path collides with a file in `public/` behaves one way
|
|
310
|
+
* when it is checked and the other way when it is deployed.
|
|
311
|
+
*
|
|
312
|
+
* @param {{entry: object, assets: object, distDir: string, cache?: object}} build
|
|
313
|
+
*/
|
|
314
|
+
export function createServeHandler({ entry, assets, distDir, cache }) {
|
|
315
|
+
const serveStatic = createStaticHandler({ root: distDir });
|
|
316
|
+
const application = createApplicationHandler({ entry, assets, cache });
|
|
317
|
+
return async function handle(request) {
|
|
318
|
+
return (await serveStatic(request)) ?? (await application(request));
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* A `Request`/`Response` handler as a Node request listener.
|
|
324
|
+
*
|
|
325
|
+
* `@uniflowed/server/node`'s, which is also what the `server.js` an adapter
|
|
326
|
+
* writes runs — so a request reaching `uf start` and the same request reaching
|
|
327
|
+
* a deployed directory go through one translation rather than two, and settle
|
|
328
|
+
* at one moment rather than at two.
|
|
329
|
+
*
|
|
330
|
+
* `entry` is the second argument rather than something this reaches for: it is
|
|
331
|
+
* the application bundle's own `beginRequest` that has to own the request, for
|
|
332
|
+
* the reason in "Who owns the request" above. It is required, and a listener
|
|
333
|
+
* built without one fails on its first request — the same trade
|
|
334
|
+
* `createFetchHandler` makes about `app.runMiddleware`, and for the same
|
|
335
|
+
* reason: an optional lifecycle is a lifecycle somebody forgets, and what is
|
|
336
|
+
* lost when they do is every `after()` in the application.
|
|
337
|
+
*/
|
|
338
|
+
export function nodeListener(handle, entry) {
|
|
339
|
+
const ready = deployment().then(({ nodeListener: create }) =>
|
|
340
|
+
create(handle, { beginRequest: entry.beginRequest }),
|
|
341
|
+
);
|
|
342
|
+
return async function listener(incoming, outgoing) {
|
|
343
|
+
return (await ready)(incoming, outgoing);
|
|
344
|
+
};
|
|
345
|
+
}
|
package/merge.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// @noflow
|
|
2
|
+
//
|
|
3
|
+
// Plain JavaScript: executed by the host that runs Vite, before any transform.
|
|
4
|
+
//
|
|
5
|
+
// Merging a project's own Vite configuration over the one uf generates.
|
|
6
|
+
//
|
|
7
|
+
// This exists because of a specific failure. uf's config re-declared Vite's
|
|
8
|
+
// options one at a time — `host`, `port`, `strictPort`, `outDir`, `sourcemap`
|
|
9
|
+
// — and the driver copied them across by hand. Anything Vite could do that uf
|
|
10
|
+
// had not enumerated was unreachable until uf shipped a release naming it.
|
|
11
|
+
// That is `react-scripts`: an integrated tool becoming the chokepoint every
|
|
12
|
+
// upgrade in the ecosystem has to pass through.
|
|
13
|
+
//
|
|
14
|
+
// So `vite` in `uf.config.js` is Vite's own configuration, merged over uf's,
|
|
15
|
+
// and uf makes no attempt to understand it. An option added to Vite tomorrow
|
|
16
|
+
// works in a uf project tomorrow.
|
|
17
|
+
//
|
|
18
|
+
// # What uf still decides
|
|
19
|
+
//
|
|
20
|
+
// Three things are uf's rather than the project's, and the merge protects
|
|
21
|
+
// them:
|
|
22
|
+
//
|
|
23
|
+
// * `plugins`, which are concatenated rather than replaced — dropping uf's
|
|
24
|
+
// Flow transform would leave a project whose source no longer compiles,
|
|
25
|
+
// which is not a thing anyone means to configure.
|
|
26
|
+
// * `configFile`, because a `vite.config.ts` beside `uf.config.js` is two
|
|
27
|
+
// files disagreeing about one project.
|
|
28
|
+
// * `root`, which is the project uf resolved.
|
|
29
|
+
//
|
|
30
|
+
// Everything else is the project's to set, including options uf sets itself:
|
|
31
|
+
// a default is a convenience, not an architecture.
|
|
32
|
+
|
|
33
|
+
/** Keys uf owns outright, whatever the project's Vite config says. */
|
|
34
|
+
const RESERVED = ["root", "configFile"];
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Deep-merge `overrides` onto `base`.
|
|
38
|
+
*
|
|
39
|
+
* Plain objects merge key by key; arrays and everything else replace, which is
|
|
40
|
+
* what a caller setting `build.rollupOptions.input` means. `plugins` is the
|
|
41
|
+
* exception and is handled by the caller, because concatenating is right there
|
|
42
|
+
* and replacing is right everywhere else.
|
|
43
|
+
*/
|
|
44
|
+
export function mergeConfig(base, overrides) {
|
|
45
|
+
if (overrides == null) return base;
|
|
46
|
+
const merged = { ...base };
|
|
47
|
+
for (const key of Object.keys(overrides)) {
|
|
48
|
+
const value = overrides[key];
|
|
49
|
+
if (value === undefined) continue;
|
|
50
|
+
merged[key] =
|
|
51
|
+
isPlainObject(value) && isPlainObject(base[key]) ? mergeConfig(base[key], value) : value;
|
|
52
|
+
}
|
|
53
|
+
return merged;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* uf's generated config, with the project's Vite config merged over it.
|
|
58
|
+
*
|
|
59
|
+
* @param {object} generated what uf built from the semantics it owns
|
|
60
|
+
* @param {object | undefined} overrides `vite` from `uf.config.js`
|
|
61
|
+
*/
|
|
62
|
+
export function withProjectConfig(generated, overrides) {
|
|
63
|
+
if (overrides == null) return generated;
|
|
64
|
+
|
|
65
|
+
const owned = {};
|
|
66
|
+
for (const key of RESERVED) owned[key] = generated[key];
|
|
67
|
+
|
|
68
|
+
const merged = mergeConfig(generated, { ...overrides, ...owned });
|
|
69
|
+
|
|
70
|
+
// uf's plugins first, then the project's. First because the Flow transform
|
|
71
|
+
// has to see a module before anything that expects JavaScript does.
|
|
72
|
+
merged.plugins = [
|
|
73
|
+
...(generated.plugins ?? []),
|
|
74
|
+
...(Array.isArray(overrides.plugins) ? overrides.plugins : []),
|
|
75
|
+
];
|
|
76
|
+
return merged;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function isPlainObject(value) {
|
|
80
|
+
return (
|
|
81
|
+
typeof value === "object" &&
|
|
82
|
+
value !== null &&
|
|
83
|
+
!Array.isArray(value) &&
|
|
84
|
+
// A plugin, a logger or a URL is a value to replace, not a shape to merge.
|
|
85
|
+
(Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null)
|
|
86
|
+
);
|
|
87
|
+
}
|
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.11",
|
|
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",
|
|
@@ -13,25 +13,25 @@
|
|
|
13
13
|
"exports": {
|
|
14
14
|
".": "./index.js",
|
|
15
15
|
"./driver": "./driver.js",
|
|
16
|
-
"./
|
|
17
|
-
"./
|
|
18
|
-
"./transform": "./transform.js",
|
|
19
|
-
"./package.json": "./package.json"
|
|
16
|
+
"./package.json": "./package.json",
|
|
17
|
+
"./merge": "./merge.js"
|
|
20
18
|
},
|
|
21
19
|
"files": [
|
|
22
|
-
"index.js",
|
|
23
20
|
"driver.js",
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"internal"
|
|
21
|
+
"index.js",
|
|
22
|
+
"internal",
|
|
23
|
+
"merge.js"
|
|
28
24
|
],
|
|
29
25
|
"dependencies": {
|
|
30
26
|
"@mdx-js/rollup": "^3.1.1",
|
|
27
|
+
"@shikijs/rehype": "^3.23.0",
|
|
28
|
+
"@uniflowed/host": "0.0.0-alpha.11",
|
|
29
|
+
"@uniflowed/server": "0.0.0-alpha.11",
|
|
31
30
|
"rehype-slug": "^6.0.0",
|
|
32
31
|
"remark-frontmatter": "^5.0.0",
|
|
33
32
|
"remark-gfm": "^4.0.1",
|
|
34
33
|
"remark-mdx-frontmatter": "^5.2.0",
|
|
34
|
+
"shiki": "^3.23.0",
|
|
35
35
|
"vite": "^8.2.2"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
package/bun-preload.js
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
// Plain JavaScript: this file registers the loader, so it cannot need one.
|
|
2
|
-
//
|
|
3
|
-
// `bun --preload @uniflowed/vite/bun-preload app.js` runs a Flow project on
|
|
4
|
-
// Bun without a build step, through Bun's own plugin API: every module uf is
|
|
5
|
-
// responsible for is transformed by `uf transform` as Bun loads it. It is the
|
|
6
|
-
// Bun counterpart of `./register.js`, and the policy of which files count is
|
|
7
|
-
// the same `isFlowModule`.
|
|
8
|
-
|
|
9
|
-
import { isFlowModule, transformFlow } from "./transform.js";
|
|
10
|
-
|
|
11
|
-
Bun.plugin({
|
|
12
|
-
name: "uniflowed-flow",
|
|
13
|
-
setup(build) {
|
|
14
|
-
build.onLoad({ filter: /\.(js|jsx|mjs)$/ }, async (args) => {
|
|
15
|
-
if (!isFlowModule(args.path)) return undefined;
|
|
16
|
-
const source = await Bun.file(args.path).text();
|
|
17
|
-
const out = await transformFlow(source, args.path, { development: true, sourceMap: false });
|
|
18
|
-
if (out == null) return undefined;
|
|
19
|
-
return { contents: out.code, loader: "js" };
|
|
20
|
-
});
|
|
21
|
-
},
|
|
22
|
-
});
|