@uniflowed/vite 0.0.0-alpha.18 → 0.0.0-alpha.21
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 +380 -46
- package/index.js +101 -15
- package/internal/a11y.js +6 -4
- package/internal/config.js +30 -3
- package/internal/devtools.js +2 -2
- package/internal/diagnostics.js +2 -2
- package/internal/flow-keywords.js +1 -1
- package/internal/openapi.js +218 -0
- package/internal/routes.js +529 -70
- package/internal/rsc.js +43 -10
- package/internal/serve.js +158 -28
- package/package.json +7 -4
package/internal/rsc.js
CHANGED
|
@@ -17,10 +17,13 @@
|
|
|
17
17
|
//
|
|
18
18
|
// Dropping a single Server Component from the client bundle is what Next.js
|
|
19
19
|
// does, and it works there because the browser is handed a Flight payload
|
|
20
|
-
// describing the tree the server rendered. uf
|
|
21
|
-
// `packages/router/
|
|
22
|
-
//
|
|
23
|
-
//
|
|
20
|
+
// describing the tree the server rendered. uf's payload
|
|
21
|
+
// (`packages/router/internal/payload.js`) carries the route's *data* and not
|
|
22
|
+
// its tree — that half needs a second React module graph, see
|
|
23
|
+
// ubugeeei-prod/uf#519 — so `packages/router/client.js` still hydrates by
|
|
24
|
+
// re-rendering the matched tree from the same modules the server rendered it
|
|
25
|
+
// from, and a module missing from the client bundle is a module React cannot
|
|
26
|
+
// hydrate. What *can* be dropped is a
|
|
24
27
|
// route the browser never renders at all — one where no client boundary is
|
|
25
28
|
// reachable from the page, its layouts, its loading fallbacks or the
|
|
26
29
|
// boundaries that cover it. Nothing under it is ever re-rendered in the
|
|
@@ -44,13 +47,12 @@ export const RSC_MANIFEST_ENV = "UF_RSC_MANIFEST";
|
|
|
44
47
|
/**
|
|
45
48
|
* The manifest schema this understands.
|
|
46
49
|
*
|
|
47
|
-
* Version
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
* `"reaches-boundary"`, and quietly drop every route from the client bundle.
|
|
50
|
+
* Version 3 lets client boundaries name package specifiers as well as project
|
|
51
|
+
* paths. An older manifest is therefore refused rather than read
|
|
52
|
+
* optimistically: it cannot know a route imports a package client module, and
|
|
53
|
+
* quietly dropping that route would be the worst possible answer.
|
|
52
54
|
*/
|
|
53
|
-
const SUPPORTED_VERSION =
|
|
55
|
+
const SUPPORTED_VERSION = 3;
|
|
54
56
|
|
|
55
57
|
/**
|
|
56
58
|
* Read the RSC manifest, or `null` when there is nothing usable to read.
|
|
@@ -141,6 +143,12 @@ export function clientRouteFilter(manifest, root, boundaries = {}) {
|
|
|
141
143
|
if (route.layouts.some(needed)) return true;
|
|
142
144
|
if ((route.loading ?? []).some((entry) => needed(entry.module))) return true;
|
|
143
145
|
if ((route.templates ?? []).some((entry) => needed(entry.module))) return true;
|
|
146
|
+
// A slot renders inside this route, so a `"use client"` anywhere in one is
|
|
147
|
+
// this route's reason to ship. Without this line a page whose only
|
|
148
|
+
// interactive part is in a slot would be dropped from the client bundle
|
|
149
|
+
// and served as a document — rendered correctly and never hydrated, which
|
|
150
|
+
// is the quietest way a feature can be half-implemented.
|
|
151
|
+
if (slotNeeds(route.slots ?? [], needed)) return true;
|
|
144
152
|
// A boundary with no module of its own is the record the scan synthesises
|
|
145
153
|
// at the router root, and what renders there is the framework's own page —
|
|
146
154
|
// already in `@uniflowed/router`, reaching nothing this project wrote. It
|
|
@@ -161,6 +169,31 @@ export function clientRouteFilter(manifest, root, boundaries = {}) {
|
|
|
161
169
|
};
|
|
162
170
|
}
|
|
163
171
|
|
|
172
|
+
/**
|
|
173
|
+
* Whether anything in a slot tree has to reach the browser.
|
|
174
|
+
*
|
|
175
|
+
* Recursive because slots nest: a slot's own layout may declare slots of its
|
|
176
|
+
* own, and a `"use client"` at any depth is still inside the page this route
|
|
177
|
+
* renders.
|
|
178
|
+
*
|
|
179
|
+
* @param {ReadonlyArray<{
|
|
180
|
+
* defaultPage: ?string,
|
|
181
|
+
* routes: ReadonlyArray<{page: string, layouts: ReadonlyArray<string>, slots: ReadonlyArray<*>}>,
|
|
182
|
+
* }>} slots
|
|
183
|
+
* @param {(file: ?string) => boolean} needed
|
|
184
|
+
*/
|
|
185
|
+
function slotNeeds(slots, needed) {
|
|
186
|
+
for (const slot of slots) {
|
|
187
|
+
if (slot.defaultPage != null && needed(slot.defaultPage)) return true;
|
|
188
|
+
for (const route of slot.routes) {
|
|
189
|
+
if (needed(route.page)) return true;
|
|
190
|
+
if (route.layouts.some(needed)) return true;
|
|
191
|
+
if (slotNeeds(route.slots, needed)) return true;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
|
|
164
197
|
// ---------------------------------------------------------------------------
|
|
165
198
|
// Server actions
|
|
166
199
|
//
|
package/internal/serve.js
CHANGED
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
// module is the part that is genuinely Vite's: finding the build on disk and
|
|
33
33
|
// reading the manifest a Vite build wrote.
|
|
34
34
|
//
|
|
35
|
-
// It moved because of `uf build --adapter`. `
|
|
35
|
+
// It moved because of `uf build --adapter`. `packages/server/serve.test.js` said
|
|
36
36
|
// what was wrong with the old arrangement while it was still the only one:
|
|
37
37
|
// "`internal/serve.js` is the seam a deploy adapter will need, and naming it
|
|
38
38
|
// in `exports` before one exists would be promising an interface nothing has
|
|
@@ -100,23 +100,88 @@ function deployment() {
|
|
|
100
100
|
* reporting that it expired nothing. A project that turned the cache off should
|
|
101
101
|
* be told that it did, not handed a cache that quietly does nothing.
|
|
102
102
|
*
|
|
103
|
-
* One store per server process, built when the handler is
|
|
104
|
-
* of what "in memory, per process"
|
|
105
|
-
* `uf start` each hold one, and two of them
|
|
103
|
+
* One store per server process, built when the handler is. With no
|
|
104
|
+
* `rendering.cache.store` that is the whole of what "in memory, per process"
|
|
105
|
+
* means in practice: `uf preview` and `uf start` each hold one, and two of them
|
|
106
|
+
* running at once share nothing. With one, the two processes share whatever the
|
|
107
|
+
* provider is in front of — see [`providerFor`].
|
|
106
108
|
*
|
|
107
|
-
* The store's
|
|
108
|
-
* named here: `
|
|
109
|
-
*
|
|
110
|
-
* configurability with none of
|
|
109
|
+
* The store's remaining options are still not configurable from `uf.config.js`
|
|
110
|
+
* and are still not named here: `maxEntries` and `now` are facts about one
|
|
111
|
+
* process's heap and one process's clock, and a parameter threaded through for
|
|
112
|
+
* a setting nobody can set would be the shape of configurability with none of
|
|
113
|
+
* the substance.
|
|
111
114
|
*
|
|
112
|
-
* @param {{route?: boolean, fetch?: boolean} | undefined} declared
|
|
115
|
+
* @param {{route?: boolean, fetch?: boolean, store?: string, storeDir?: string} | undefined} declared
|
|
113
116
|
* @param {(options?: object) => object} createCacheStore
|
|
117
|
+
* @param {{root: string, build: string | null}} where
|
|
114
118
|
*/
|
|
115
|
-
function cacheFor(declared, createCacheStore) {
|
|
119
|
+
async function cacheFor(declared, createCacheStore, where) {
|
|
116
120
|
const route = declared?.route === true;
|
|
117
121
|
const fetchCache = declared?.fetch === true;
|
|
118
122
|
if (!route && !fetchCache) return undefined;
|
|
119
|
-
|
|
123
|
+
const provider = await providerFor(declared, where);
|
|
124
|
+
const store =
|
|
125
|
+
provider == null ? createCacheStore() : createCacheStore({ provider, build: where.build });
|
|
126
|
+
return { store, route, fetch: fetchCache };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* The durable provider `rendering.cache.store` names, or `null` for memory.
|
|
131
|
+
*
|
|
132
|
+
* Three answers, and the third is the one that matters to
|
|
133
|
+
* `docs/red-lines.md`'s third line. `"memory"` — the default, and what every
|
|
134
|
+
* project that says nothing gets — keeps the store exactly as it was.
|
|
135
|
+
* `"filesystem"` is uf's built-in, and it is a convenience rather than an
|
|
136
|
+
* architecture. Anything else is a **module specifier**, resolved from the
|
|
137
|
+
* project, exporting `createCacheProvider`: the same shape `builder.module`
|
|
138
|
+
* has, chosen for the same reason that document gives — "a provider a project
|
|
139
|
+
* can replace has to be a name it can write, and an enum with one variant
|
|
140
|
+
* cannot become one without a release of uf".
|
|
141
|
+
*
|
|
142
|
+
* So a project with a Redis, a KV namespace or an S3 bucket writes twenty lines
|
|
143
|
+
* against `@uniflowed/server/cache`'s `CacheProvider` type, names the module
|
|
144
|
+
* here, and uf never learns which of those it was.
|
|
145
|
+
*
|
|
146
|
+
* # Why a missing build identity is a refusal
|
|
147
|
+
*
|
|
148
|
+
* Because the alternatives are both worse. Falling back to memory would give a
|
|
149
|
+
* project that asked for a cache surviving restarts one that does not, and the
|
|
150
|
+
* symptom is a `MISS` on every cold request — indistinguishable from a cache
|
|
151
|
+
* that is simply cold. Generating an identity per process would be worse again:
|
|
152
|
+
* four servers would write four copies of everything into one directory and
|
|
153
|
+
* read none of each other's. The deployment rules say a target that cannot
|
|
154
|
+
* provide a durable store has to say so, and this is a host saying so.
|
|
155
|
+
*
|
|
156
|
+
* @param {{store?: string, storeDir?: string} | undefined} declared
|
|
157
|
+
* @param {{root: string, build: string | null}} where
|
|
158
|
+
*/
|
|
159
|
+
async function providerFor(declared, { root, build }) {
|
|
160
|
+
const named = declared?.store ?? "memory";
|
|
161
|
+
if (named === "memory") return null;
|
|
162
|
+
if (build == null) {
|
|
163
|
+
throw new Error(
|
|
164
|
+
`uf: rendering.cache.store is ${JSON.stringify(named)}, which keeps entries between ` +
|
|
165
|
+
"restarts, and there is no build identity to key them by. `uf build` writes one " +
|
|
166
|
+
"beside the server bundle; set UF_BUILD_ID to name it yourself. Without one, a " +
|
|
167
|
+
"deploy would answer the new build's URLs with the previous build's documents.",
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
const directory = path.resolve(root, declared?.storeDir ?? path.join(".uf", "cache", "route"));
|
|
171
|
+
if (named === "filesystem") {
|
|
172
|
+
const { createFilesystemCache } = await import("@uniflowed/server/cache/filesystem");
|
|
173
|
+
return createFilesystemCache({ directory });
|
|
174
|
+
}
|
|
175
|
+
const provider = await import(providerSpecifier(root, named));
|
|
176
|
+
const create = provider.createCacheProvider ?? provider.default;
|
|
177
|
+
if (typeof create !== "function") {
|
|
178
|
+
throw new Error(
|
|
179
|
+
`uf: rendering.cache.store names ${JSON.stringify(named)}, which exports no ` +
|
|
180
|
+
"`createCacheProvider`. A durable cache provider is a module exporting that " +
|
|
181
|
+
"function; see @uniflowed/server/cache's CacheProvider type for what it returns.",
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
return create({ build, directory });
|
|
120
185
|
}
|
|
121
186
|
|
|
122
187
|
/**
|
|
@@ -147,7 +212,63 @@ export async function loadBuild({ root, outDir, serverDir }) {
|
|
|
147
212
|
const manifest = JSON.parse(await readFile(manifestFile, "utf8"));
|
|
148
213
|
const entry = await import(pathToFileURL(entryFile).href);
|
|
149
214
|
await deployment();
|
|
150
|
-
|
|
215
|
+
const build = await buildIdentity(root, serverDir);
|
|
216
|
+
return { entry, assets: assetsFromManifest(manifest), distDir, root, build };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* What `import()` should be given for a provider a project named.
|
|
221
|
+
*
|
|
222
|
+
* A relative path in `uf.config.js` is relative to *the project*, which is what
|
|
223
|
+
* anybody writing `"./cache/redis.js"` means and is not what `import()` from
|
|
224
|
+
* this module would do — it would look beside `@uniflowed/vite`, find nothing,
|
|
225
|
+
* and report a missing module the config file does not mention. `builder.module`
|
|
226
|
+
* settled the same question the same way in `uf_cli`'s `project_directory`.
|
|
227
|
+
*
|
|
228
|
+
* A bare specifier is left alone: `"@acme/uf-cache-redis"` is a package, and
|
|
229
|
+
* resolving it is Node's job and not this function's.
|
|
230
|
+
*
|
|
231
|
+
* @param {string} root
|
|
232
|
+
* @param {string} named
|
|
233
|
+
*/
|
|
234
|
+
export function providerSpecifier(root, named) {
|
|
235
|
+
if (!named.startsWith(".") && !path.isAbsolute(named)) return named;
|
|
236
|
+
return pathToFileURL(path.resolve(root, named)).href;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** What `uf build` writes its identity into, beside the server bundle. */
|
|
240
|
+
export const BUILD_ID_FILE = "uf-build-id";
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* The identity of the build being served, or `null`.
|
|
244
|
+
*
|
|
245
|
+
* Only a durable cache reads it, and only a durable cache needs it: an entry
|
|
246
|
+
* that cannot outlive the process cannot outlive the build either, so every
|
|
247
|
+
* command that keeps its cache in memory is entitled to `null` here and never
|
|
248
|
+
* looks. `packages/server/internal/cache-key.js` argues the rest.
|
|
249
|
+
*
|
|
250
|
+
* `UF_BUILD_ID` first, then the file `uf build` wrote. The environment wins for
|
|
251
|
+
* the reason it wins in `crates/uf_rsc`'s `BuildId::from_env_or_generate`,
|
|
252
|
+
* which reads the same variable for the same kind of fact: a deployment that
|
|
253
|
+
* needs two artefacts to *be* one build — a blue/green pair, a rebuild of a
|
|
254
|
+
* tagged commit — has no other way to say so.
|
|
255
|
+
*
|
|
256
|
+
* `null` rather than a generated fallback, and that is the whole point of the
|
|
257
|
+
* function. A per-process identity would give four servers four caches with a
|
|
258
|
+
* shared disk between them, which is worse than four memories: it would write
|
|
259
|
+
* four copies of everything and read none of them. Whoever asked for a durable
|
|
260
|
+
* store is told there is no build to key it by, and gets to fix it.
|
|
261
|
+
*/
|
|
262
|
+
export async function buildIdentity(root, serverDir) {
|
|
263
|
+
const named = process.env.UF_BUILD_ID;
|
|
264
|
+
if (typeof named === "string" && named !== "") return named;
|
|
265
|
+
try {
|
|
266
|
+
const file = path.join(path.resolve(root, serverDir), BUILD_ID_FILE);
|
|
267
|
+
const value = (await readFile(file, "utf8")).trim();
|
|
268
|
+
return value === "" ? null : value;
|
|
269
|
+
} catch {
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
151
272
|
}
|
|
152
273
|
|
|
153
274
|
async function readable(file, message) {
|
|
@@ -309,20 +430,29 @@ export async function beginRequest(entry, request) {
|
|
|
309
430
|
* the point where four switches that used to reach a JSON file and nothing else
|
|
310
431
|
* become a store a request can hit. See ubugeeei-prod/uf#277.
|
|
311
432
|
*
|
|
312
|
-
*
|
|
433
|
+
* `root` and `build` come with it, and only the cache reads either: `root` is
|
|
434
|
+
* where a `storeDir` is resolved from and `build` is what a durable entry is
|
|
435
|
+
* keyed by. Both are `undefined` for a caller that constructs a handler by
|
|
436
|
+
* hand, which is the memory-only store and needs neither.
|
|
437
|
+
*
|
|
438
|
+
* @param {{entry: object, assets: object, cache?: object, root?: string, build?: string | null}} build
|
|
313
439
|
*/
|
|
314
|
-
export function createApplicationHandler({ entry, assets, cache }) {
|
|
315
|
-
const ready = deployment().then(
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
440
|
+
export function createApplicationHandler({ entry, assets, cache, root, build }) {
|
|
441
|
+
const ready = deployment().then(
|
|
442
|
+
async ({ createFetchHandler, createCacheStore, nodeCapabilities }) =>
|
|
443
|
+
createFetchHandler({
|
|
444
|
+
app: entry,
|
|
445
|
+
document: assets,
|
|
446
|
+
cache: await cacheFor(cache, createCacheStore, {
|
|
447
|
+
root: root ?? process.cwd(),
|
|
448
|
+
build: build ?? null,
|
|
449
|
+
}),
|
|
450
|
+
// `uf preview` and `uf start` are a Node process with a socket, which is
|
|
451
|
+
// what a deployed `--adapter node` build is too — so a route handler
|
|
452
|
+
// that streams events answers the same way in the preview it is checked
|
|
453
|
+
// in and in the deployment it ends up as. See `withRequest` above.
|
|
454
|
+
capabilities: nodeCapabilities(),
|
|
455
|
+
}),
|
|
326
456
|
);
|
|
327
457
|
return async function handle(request) {
|
|
328
458
|
return (await ready)(request);
|
|
@@ -352,11 +482,11 @@ export function createStaticHandler({ root }) {
|
|
|
352
482
|
* project whose handler path collides with a file in `public/` behaves one way
|
|
353
483
|
* when it is checked and the other way when it is deployed.
|
|
354
484
|
*
|
|
355
|
-
* @param {{entry: object, assets: object, distDir: string, cache?: object}} build
|
|
485
|
+
* @param {{entry: object, assets: object, distDir: string, cache?: object, root?: string, build?: string | null}} build
|
|
356
486
|
*/
|
|
357
|
-
export function createServeHandler({ entry, assets, distDir, cache }) {
|
|
487
|
+
export function createServeHandler({ entry, assets, distDir, cache, root, build }) {
|
|
358
488
|
const serveStatic = createStaticHandler({ root: distDir });
|
|
359
|
-
const application = createApplicationHandler({ entry, assets, cache });
|
|
489
|
+
const application = createApplicationHandler({ entry, assets, cache, root, build });
|
|
360
490
|
return async function handle(request) {
|
|
361
491
|
return (await serveStatic(request)) ?? (await application(request));
|
|
362
492
|
};
|
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.21",
|
|
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",
|
|
@@ -28,13 +28,16 @@
|
|
|
28
28
|
"driver.js",
|
|
29
29
|
"index.js",
|
|
30
30
|
"internal",
|
|
31
|
-
"merge.js"
|
|
31
|
+
"merge.js",
|
|
32
|
+
"!*.test.js"
|
|
32
33
|
],
|
|
33
34
|
"dependencies": {
|
|
34
35
|
"@mdx-js/rollup": "^3.1.1",
|
|
35
36
|
"@shikijs/rehype": "^3.23.0",
|
|
36
|
-
"@uniflowed/host": "0.0.0-alpha.
|
|
37
|
-
"@uniflowed/
|
|
37
|
+
"@uniflowed/host": "0.0.0-alpha.21",
|
|
38
|
+
"@uniflowed/router": "0.0.0-alpha.21",
|
|
39
|
+
"@uniflowed/server": "0.0.0-alpha.21",
|
|
40
|
+
"@uniflowed/validator": "0.0.0-alpha.21",
|
|
38
41
|
"rehype-slug": "^6.0.0",
|
|
39
42
|
"remark-frontmatter": "^5.0.0",
|
|
40
43
|
"remark-gfm": "^4.0.1",
|