@uniflowed/vite 0.0.0-alpha.8 → 0.1.0
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 +2014 -212
- package/index.js +953 -107
- package/internal/a11y-runtime.js +234 -0
- package/internal/a11y.js +102 -0
- package/internal/assets.js +744 -0
- package/internal/barrel-imports.js +459 -0
- package/internal/compile-assets.js +67 -0
- package/internal/config.js +30 -3
- package/internal/dev-state.js +161 -0
- package/internal/devtools.js +117 -0
- package/internal/diagnostics.js +369 -0
- package/internal/events.js +8 -5
- package/internal/flight.js +803 -0
- package/internal/flow-keywords.js +1 -1
- package/internal/frontmatter.js +33 -0
- package/internal/http.js +21 -2
- package/internal/image-endpoint.js +118 -0
- package/internal/instrumentation.js +23 -0
- package/internal/module-graph.js +134 -0
- package/internal/native-web.js +26 -0
- package/internal/openapi.js +218 -0
- package/internal/relay.js +74 -0
- package/internal/routes.js +1294 -81
- package/internal/rsc.js +335 -11
- package/internal/serve.js +583 -29
- package/internal/server-components.js +109 -0
- package/internal/worker-builtins.js +170 -0
- package/package.json +28 -6
package/index.js
CHANGED
|
@@ -14,8 +14,12 @@
|
|
|
14
14
|
// development and the virtual modules that make a directory
|
|
15
15
|
// of pages an application: the route table, the client entry
|
|
16
16
|
// that hydrates it, and the server entry that renders it. In
|
|
17
|
-
// development it also
|
|
18
|
-
//
|
|
17
|
+
// development it also answers every request the way a
|
|
18
|
+
// deployment does — the guard, a server action, a route
|
|
19
|
+
// handler, then the renderer — so `uf dev` serves what
|
|
20
|
+
// `uf start` serves rather than an approximation of it, and it
|
|
21
|
+
// serves the two `/__uf/` paths a browser reports to (see
|
|
22
|
+
// `internal/diagnostics.js`).
|
|
19
23
|
// The client's copy of the route table is not the server's:
|
|
20
24
|
// `internal/rsc.js` reads the RSC analysis and leaves out the
|
|
21
25
|
// page of every route no client boundary reaches, so that
|
|
@@ -24,21 +28,46 @@
|
|
|
24
28
|
// markdown, front matter, heading ids and build-time syntax
|
|
25
29
|
// highlighting, so `.mdx` works with
|
|
26
30
|
// no configuration.
|
|
31
|
+
// * `uf:barrel-imports` — a named import from `@uniflowed/ui` becomes an import
|
|
32
|
+
// from the file that defines the name, in every environment,
|
|
33
|
+
// so a page using one component loads that component's module
|
|
34
|
+
// rather than every module the barrel re-exports. See
|
|
35
|
+
// `internal/barrel-imports.js`.
|
|
36
|
+
// * `uf:asset` — an imported image is decoded, resized to the widths the
|
|
37
|
+
// project declares and re-encoded by `uf assets`, and an
|
|
38
|
+
// imported font is self-hosted with the `@font-face` and the
|
|
39
|
+
// metric-matched fallback that stop the swap moving the page.
|
|
40
|
+
// The import evaluates to what `Image` and `Font` need — the
|
|
41
|
+
// intrinsic size, every emitted variant, the placeholder —
|
|
42
|
+
// rather than to a URL string. See `internal/assets.js`.
|
|
27
43
|
//
|
|
28
44
|
// `uniflowed(options)` returns the array; a project that wants to add a plugin
|
|
29
45
|
// declares it in `uf.config.js` and the driver appends it after these.
|
|
30
46
|
|
|
31
|
-
import {
|
|
47
|
+
import { instrumentationFile } from "./internal/instrumentation.js";
|
|
48
|
+
|
|
32
49
|
import path from "node:path";
|
|
50
|
+
import { nativeWebPlugin } from "./internal/native-web.js";
|
|
33
51
|
|
|
34
52
|
import mdx from "@mdx-js/rollup";
|
|
35
53
|
import rehypeSlug from "rehype-slug";
|
|
54
|
+
import { relayConfig, relayDependencies, transformRelay } from "./internal/relay.js";
|
|
36
55
|
|
|
37
|
-
import {
|
|
56
|
+
import {
|
|
57
|
+
AUDIT_PUBLIC_PATH,
|
|
58
|
+
AUDIT_RESOLVED_ID,
|
|
59
|
+
auditAvailable,
|
|
60
|
+
auditRuntimeSource,
|
|
61
|
+
auditTag,
|
|
62
|
+
} from "./internal/a11y.js";
|
|
63
|
+
import { assetPlugin } from "./internal/assets.js";
|
|
64
|
+
import { barrelImportsPlugin, namespaceViewOf } from "./internal/barrel-imports.js";
|
|
65
|
+
import { emit, reportRenderError, errorEvent } from "./internal/events.js";
|
|
66
|
+
import remarkFrontmatterExport from "./internal/frontmatter.js";
|
|
38
67
|
import { highlightPlugin } from "./internal/highlight.js";
|
|
68
|
+
import { moduleId } from "./internal/module-graph.js";
|
|
39
69
|
import remarkFrontmatter from "remark-frontmatter";
|
|
40
70
|
import remarkGfm from "remark-gfm";
|
|
41
|
-
import remarkMdxFrontmatter from "remark-mdx-frontmatter";
|
|
42
71
|
|
|
43
72
|
import {
|
|
44
73
|
RUNTIME_PUBLIC_PATH,
|
|
@@ -47,22 +76,86 @@ import {
|
|
|
47
76
|
preambleCode,
|
|
48
77
|
refreshRuntimeSource,
|
|
49
78
|
} from "./internal/refresh.js";
|
|
50
|
-
import {
|
|
79
|
+
import {
|
|
80
|
+
RSC_MANIFEST_ENV,
|
|
81
|
+
actionReferenceSource,
|
|
82
|
+
actionsModuleSource,
|
|
83
|
+
clientRouteFilter,
|
|
84
|
+
readRscManifest,
|
|
85
|
+
rscManifestKey,
|
|
86
|
+
serverActionModules,
|
|
87
|
+
serverActionTable,
|
|
88
|
+
} from "./internal/rsc.js";
|
|
51
89
|
import {
|
|
52
90
|
RESERVED,
|
|
53
91
|
VIRTUAL,
|
|
54
92
|
clientModuleSource,
|
|
93
|
+
resolveRouteTarget,
|
|
55
94
|
routesModuleSource,
|
|
95
|
+
routingRulesOf,
|
|
56
96
|
scanRoutes,
|
|
57
97
|
serverModuleSource,
|
|
58
98
|
} from "./internal/routes.js";
|
|
59
|
-
import { TransformService, isFlowModule } from "@uniflowed/host/transform";
|
|
60
|
-
import {
|
|
61
|
-
|
|
99
|
+
import { TransformService, isFlowModule, uniflowedPackages } from "@uniflowed/host/transform";
|
|
100
|
+
import {
|
|
101
|
+
DEV_RSC_HOOK,
|
|
102
|
+
FLIGHT_BROWSER_DEPENDENCIES,
|
|
103
|
+
FLIGHT_VIRTUAL,
|
|
104
|
+
INTERCEPTED_FROM_HEADER,
|
|
105
|
+
RSC_ENVIRONMENT,
|
|
106
|
+
builtBridgeSource,
|
|
107
|
+
builtReferencesSource,
|
|
108
|
+
clientManifestSource,
|
|
109
|
+
clientModuleUrlPlugin,
|
|
110
|
+
clientReferencePlugin,
|
|
111
|
+
compilerRuntimeSource,
|
|
112
|
+
createFlightState,
|
|
113
|
+
devBridgeSource,
|
|
114
|
+
devReferencesSource,
|
|
115
|
+
devStylesheets,
|
|
116
|
+
flightClientSource,
|
|
117
|
+
flightDocumentPath,
|
|
118
|
+
flightServerSource,
|
|
119
|
+
linkStylesheets,
|
|
120
|
+
rendersFlight,
|
|
121
|
+
rscEntrySource,
|
|
122
|
+
rscEnvironment,
|
|
123
|
+
} from "./internal/flight.js";
|
|
124
|
+
import { createChannelMiddleware } from "./internal/diagnostics.js";
|
|
125
|
+
import { startDevState } from "./internal/dev-state.js";
|
|
126
|
+
import { devtoolsPreamble } from "./internal/devtools.js";
|
|
127
|
+
import { send, toAddressRequest, toRequest } from "./internal/http.js";
|
|
128
|
+
import {
|
|
129
|
+
answerRouting,
|
|
130
|
+
answersInFrontOfFiles,
|
|
131
|
+
beginRequest,
|
|
132
|
+
forViteBase,
|
|
133
|
+
rewriteRouting,
|
|
134
|
+
} from "./internal/serve.js";
|
|
135
|
+
import { serverComponentsProblem } from "./internal/server-components.js";
|
|
62
136
|
|
|
63
137
|
/** A resolved virtual id: Vite's convention is a leading NUL byte. */
|
|
64
138
|
const resolved = (id) => `\0${id}`;
|
|
65
|
-
const VIRTUAL_IDS = new Set(Object.values(VIRTUAL));
|
|
139
|
+
const VIRTUAL_IDS = new Set([...Object.values(VIRTUAL), ...Object.values(FLIGHT_VIRTUAL)]);
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* CommonJS packages the browser can reach only through an installed
|
|
143
|
+
* `@uniflowed/*` package.
|
|
144
|
+
*
|
|
145
|
+
* uf excludes its own packages from Vite's dependency optimizer because they
|
|
146
|
+
* ship Flow, so Vite will not discover a CommonJS peer through them once they
|
|
147
|
+
* live under a project's `node_modules`. React Testing's `happy-dom` edge is
|
|
148
|
+
* ESM and browser-mapped to a local stub, and the optional `axe-core` edge is
|
|
149
|
+
* named separately below only when the project actually installed it.
|
|
150
|
+
*/
|
|
151
|
+
const CLIENT_COMMONJS_DEPENDENCIES = Object.freeze([
|
|
152
|
+
"react",
|
|
153
|
+
"react/jsx-runtime",
|
|
154
|
+
"react/jsx-dev-runtime",
|
|
155
|
+
"react/compiler-runtime",
|
|
156
|
+
"react-dom",
|
|
157
|
+
"react-dom/client",
|
|
158
|
+
]);
|
|
66
159
|
|
|
67
160
|
/**
|
|
68
161
|
* Prefix of the virtual module that carries one source module's StyleX rules.
|
|
@@ -85,6 +178,7 @@ export function devUrlFor(id) {
|
|
|
85
178
|
* @typedef {object} UniflowedOptions
|
|
86
179
|
* @property {string} [root] absolute project root; Vite's root by default
|
|
87
180
|
* @property {object} [config] the loaded `uf.config.js` object
|
|
181
|
+
* @property {"web" | "native" | "ios" | "android"} [target] app target
|
|
88
182
|
* @property {string} [command] the `uf` binary to transform through
|
|
89
183
|
*/
|
|
90
184
|
|
|
@@ -96,16 +190,112 @@ export function devUrlFor(id) {
|
|
|
96
190
|
export default function uniflowed(options = {}) {
|
|
97
191
|
const ufConfig = options.config ?? {};
|
|
98
192
|
const app = ufConfig.app ?? {};
|
|
193
|
+
const routeTarget = resolveRouteTarget(ufConfig, options.target);
|
|
99
194
|
const routerRoot = app.router?.root ?? "app";
|
|
100
195
|
const appEntry = app.router?.entry ?? ufConfig.build?.entries?.[0] ?? "app.js";
|
|
101
196
|
const markdown = app.builtins?.markdown ?? {};
|
|
197
|
+
const builtins = app.builtins ?? {};
|
|
198
|
+
// On unless the project says otherwise, and read as `!== false` rather than
|
|
199
|
+
// `=== true` because that is what "on by default" means for a field almost
|
|
200
|
+
// no `uf.config.js` will mention. It only ever reaches the *development*
|
|
201
|
+
// client entry; see `flowPlugin`'s `load`. ubugeeei-prod/uf#516.
|
|
202
|
+
const strictMode = app.react?.strictMode !== false;
|
|
203
|
+
// What the browser does with a link, read here for the reason Strict Mode is
|
|
204
|
+
// and honoured in every command rather than in the build alone: it is the
|
|
205
|
+
// one setting whose whole effect is what happens on a click, so a dev server
|
|
206
|
+
// that disagreed with the deployment would be the wrong application to look
|
|
207
|
+
// at. Anything but `"document"` is the client router, which is what every
|
|
208
|
+
// project that has not heard of the key has.
|
|
209
|
+
const navigation = app.rendering?.navigation === "document" ? "document" : "client";
|
|
210
|
+
// How long, in seconds, the client router shows a route it already fetched
|
|
211
|
+
// without asking the server again, written into the client entry beside
|
|
212
|
+
// `navigation` and honoured by every command for the same reason. Anything
|
|
213
|
+
// but a positive number is `0`, which keeps nothing.
|
|
214
|
+
const staleTime =
|
|
215
|
+
typeof app.rendering?.staleTime === "number" &&
|
|
216
|
+
Number.isFinite(app.rendering.staleTime) &&
|
|
217
|
+
app.rendering.staleTime > 0
|
|
218
|
+
? app.rendering.staleTime
|
|
219
|
+
: 0;
|
|
220
|
+
// Whether this application starts by attaching to markup or by rendering
|
|
221
|
+
// into an empty root. `["csr"]` is the only list that means the second, and
|
|
222
|
+
// `uf` refuses that value beside any other while the config is read — so the
|
|
223
|
+
// question here is "is it in the list", not "is it the only thing in it",
|
|
224
|
+
// and a driver started by hand on a config `uf` never validated gets the same
|
|
225
|
+
// answer for the same reason a project would want.
|
|
226
|
+
const mount = (app.rendering?.modes ?? []).includes("csr") ? "render" : "hydrate";
|
|
227
|
+
// Whether routes render as React Server Components, which is the default: a
|
|
228
|
+
// second module graph resolved under `react-server`, a document carrying the
|
|
229
|
+
// Flight payload it was rendered from, and a browser that hydrates that
|
|
230
|
+
// payload rather than importing routes. `app.rsc: false` is the application
|
|
231
|
+
// rendered from its modules, as every uf application was before
|
|
232
|
+
// ubugeeei-prod/uf#519. See `./internal/flight.js`.
|
|
233
|
+
const flightState = rendersFlight(app, { mount, routeTarget })
|
|
234
|
+
? createFlightState({ root: options.root ?? process.cwd() })
|
|
235
|
+
: null;
|
|
102
236
|
|
|
103
|
-
|
|
237
|
+
const accessibility = ufConfig.accessibility ?? {};
|
|
238
|
+
// `app.router.redirects`, `rewrites` and `headers`: written into the server
|
|
239
|
+
// bundle for every host that serves a build, and asked by `uf dev` itself
|
|
240
|
+
// from the same object. See `@uniflowed/server`'s `internal/routing.js`.
|
|
241
|
+
const routing = routingRulesOf(app.router);
|
|
242
|
+
|
|
243
|
+
return [
|
|
244
|
+
flowPlugin({
|
|
245
|
+
routerRoot,
|
|
246
|
+
appEntry,
|
|
247
|
+
routeTarget,
|
|
248
|
+
strictMode,
|
|
249
|
+
navigation,
|
|
250
|
+
staleTime,
|
|
251
|
+
mount,
|
|
252
|
+
flightState,
|
|
253
|
+
routing,
|
|
254
|
+
command: options.command,
|
|
255
|
+
accessibility,
|
|
256
|
+
relayEnabled: builtins.relay !== false,
|
|
257
|
+
}),
|
|
258
|
+
...(flightState == null ? [] : [clientReferencePlugin(flightState), clientModuleUrlPlugin()]),
|
|
259
|
+
// After the references, so a client module the rsc graph has already
|
|
260
|
+
// replaced is not read for imports it no longer has; see the file.
|
|
261
|
+
barrelImportsPlugin(),
|
|
262
|
+
mdxPlugin(markdown),
|
|
263
|
+
assetPlugin({
|
|
264
|
+
images: builtins.images ?? {},
|
|
265
|
+
fonts: builtins.fonts ?? {},
|
|
266
|
+
icons: builtins.icons ?? {},
|
|
267
|
+
og: builtins.og ?? {},
|
|
268
|
+
command: options.command,
|
|
269
|
+
}),
|
|
270
|
+
nativeWebPlugin(routeTarget),
|
|
271
|
+
];
|
|
104
272
|
}
|
|
105
273
|
|
|
106
|
-
function flowPlugin({
|
|
274
|
+
function flowPlugin({
|
|
275
|
+
routerRoot,
|
|
276
|
+
appEntry,
|
|
277
|
+
routeTarget,
|
|
278
|
+
strictMode,
|
|
279
|
+
navigation,
|
|
280
|
+
staleTime,
|
|
281
|
+
mount,
|
|
282
|
+
flightState,
|
|
283
|
+
routing,
|
|
284
|
+
command,
|
|
285
|
+
accessibility,
|
|
286
|
+
relayEnabled,
|
|
287
|
+
}) {
|
|
107
288
|
let root = process.cwd();
|
|
108
289
|
let isProduction = false;
|
|
290
|
+
/**
|
|
291
|
+
* Whether this project has axe-core, so the audit has an engine.
|
|
292
|
+
*
|
|
293
|
+
* Answered once in `configResolved` rather than per document: it is a
|
|
294
|
+
* question about `node_modules`, the answer cannot change while the server
|
|
295
|
+
* runs without a restart anyway, and asking it per render would put a module
|
|
296
|
+
* resolution on the path of every page.
|
|
297
|
+
*/
|
|
298
|
+
let auditsPage = false;
|
|
109
299
|
let base = "/";
|
|
110
300
|
let appRoot = "";
|
|
111
301
|
let entryPath = "";
|
|
@@ -113,6 +303,7 @@ function flowPlugin({ routerRoot, appEntry, command }) {
|
|
|
113
303
|
let server = null;
|
|
114
304
|
/** @type {TransformService | null} */
|
|
115
305
|
let service = null;
|
|
306
|
+
let relayOptions = null;
|
|
116
307
|
/**
|
|
117
308
|
* Each module's compiled stylesheet, keyed by the virtual id serving it.
|
|
118
309
|
*
|
|
@@ -168,30 +359,107 @@ function flowPlugin({ routerRoot, appEntry, command }) {
|
|
|
168
359
|
if (server == null) {
|
|
169
360
|
emit("rsc-split", { pages: kept.size, routes: table.routes.length });
|
|
170
361
|
}
|
|
171
|
-
|
|
362
|
+
// `relativeTo` only here, and never for the server's copy below. Every
|
|
363
|
+
// `import()` in this table becomes a chunk URL, but `file` is a string and
|
|
364
|
+
// survives the build — so the browser's table was shipping the absolute
|
|
365
|
+
// path of every page on the machine that built the site, to every visitor.
|
|
366
|
+
// The server's table is read where those files are and keeps them.
|
|
367
|
+
// HTTP handlers are exclusively server capabilities. Even an unused
|
|
368
|
+
// dynamic import makes Vite traverse the handler's database/auth imports.
|
|
369
|
+
return routesModuleSource(
|
|
370
|
+
{ ...table, handlers: [] },
|
|
371
|
+
{
|
|
372
|
+
shipsPage: (route) => kept.has(route),
|
|
373
|
+
relativeTo: root,
|
|
374
|
+
},
|
|
375
|
+
);
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* The manifest's two action tables, re-read only when the file changes.
|
|
380
|
+
*
|
|
381
|
+
* `load` runs for every module in the graph and has to ask "is this a
|
|
382
|
+
* `"use server"` module?" about each one, so parsing the manifest per call
|
|
383
|
+
* would put a JSON parse of the whole graph between Vite and every file it
|
|
384
|
+
* opens. Size and modification time are the identity — the same pair
|
|
385
|
+
* `.uf/cache/transform` keys on — and `uf dev` drops the memo outright when
|
|
386
|
+
* its watcher sees the file change, so a rewrite inside one millisecond is
|
|
387
|
+
* still seen.
|
|
388
|
+
*/
|
|
389
|
+
let actionMemo = null;
|
|
390
|
+
const forgetActions = () => {
|
|
391
|
+
actionMemo = null;
|
|
392
|
+
};
|
|
393
|
+
const invalidateActionModules = (moduleGraph, modules) => {
|
|
394
|
+
for (const id of modules.keys()) {
|
|
395
|
+
const module = moduleGraph.getModuleById(id);
|
|
396
|
+
if (module) moduleGraph.invalidateModule(module);
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
const actionTables = () => {
|
|
400
|
+
const file = process.env[RSC_MANIFEST_ENV];
|
|
401
|
+
const key = rscManifestKey(file);
|
|
402
|
+
if (actionMemo == null || actionMemo.key !== key) {
|
|
403
|
+
const manifest = readRscManifest(file);
|
|
404
|
+
actionMemo = {
|
|
405
|
+
key,
|
|
406
|
+
modules: serverActionModules(manifest, root),
|
|
407
|
+
table: serverActionTable(manifest, root),
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
return actionMemo;
|
|
172
411
|
};
|
|
173
412
|
|
|
174
413
|
return {
|
|
175
414
|
name: "uf:flow",
|
|
176
415
|
enforce: "pre",
|
|
416
|
+
// What `driver.js` runs the three builds with; `null` for an application
|
|
417
|
+
// rendered from its modules. See `./internal/flight.js`.
|
|
418
|
+
api: { flight: flightState },
|
|
177
419
|
|
|
178
420
|
config(userConfig, env) {
|
|
179
421
|
const projectRoot = path.resolve(userConfig.root ?? process.cwd());
|
|
422
|
+
// Before anything is resolved. Routes that render as React Server
|
|
423
|
+
// Components need `react-server-dom-parcel` and React 19.3, and the router
|
|
424
|
+
// installs without either (ubugeeei-prod/uf#992). Said here, once, rather
|
|
425
|
+
// than as an unresolved import deep in a build or a failure inside a render.
|
|
426
|
+
if (flightState != null) {
|
|
427
|
+
const problem = serverComponentsProblem(projectRoot);
|
|
428
|
+
if (problem != null) {
|
|
429
|
+
throw new Error(problem);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
180
432
|
isProduction = env.mode === "production" || env.command === "build";
|
|
433
|
+
// A reference names the client manifest only in a build, where a client
|
|
434
|
+
// build writes the chunks it names; a dev server names the URL it serves.
|
|
435
|
+
if (flightState != null) flightState.production = env.command === "build";
|
|
436
|
+
// Decided here rather than in `configResolved`, because the answer has to
|
|
437
|
+
// reach `optimizeDeps.include` below and that is written in this hook.
|
|
438
|
+
auditsPage =
|
|
439
|
+
!isProduction && (accessibility?.devAudit ?? true) && auditAvailable(projectRoot);
|
|
181
440
|
return {
|
|
182
441
|
// uf serves HTML itself; there is no index.html to fall back to.
|
|
183
442
|
appType: "custom",
|
|
184
443
|
resolve: {
|
|
185
|
-
dedupe: ["react", "react-dom"],
|
|
444
|
+
dedupe: ["react", "react-dom", "react-relay", "relay-runtime"],
|
|
186
445
|
},
|
|
187
446
|
optimizeDeps: {
|
|
188
447
|
include: [
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
448
|
+
...CLIENT_COMMONJS_DEPENDENCIES,
|
|
449
|
+
...relayDependencies(projectRoot),
|
|
450
|
+
// React's Flight client, for an application whose routes render as
|
|
451
|
+
// Server Components; `FLIGHT_BROWSER_DEPENDENCIES` says why.
|
|
452
|
+
...(flightState == null ? [] : FLIGHT_BROWSER_DEPENDENCIES),
|
|
453
|
+
// The accessibility audit's engine, when this project has one.
|
|
454
|
+
//
|
|
455
|
+
// Named up front rather than left to be discovered. axe-core is
|
|
456
|
+
// CommonJS, and the only thing that imports it is a *virtual*
|
|
457
|
+
// module reached from the document — so Vite would meet it for the
|
|
458
|
+
// first time after a page had already loaded, optimise it then, and
|
|
459
|
+
// reload the page it had just served. A full reload in the middle
|
|
460
|
+
// of the first render of every `uf dev` session is a high price for
|
|
461
|
+
// a feature whose whole manner is to be quiet.
|
|
462
|
+
...(auditsPage ? ["axe-core"] : []),
|
|
195
463
|
],
|
|
196
464
|
// uf's packages ship Flow. The dependency optimiser pre-bundles
|
|
197
465
|
// with a JavaScript parser and would reject every one of them.
|
|
@@ -203,12 +471,30 @@ function flowPlugin({ routerRoot, appEntry, command }) {
|
|
|
203
471
|
// externalised.
|
|
204
472
|
noExternal: [/^@uniflowed\//],
|
|
205
473
|
},
|
|
474
|
+
// The graph React Server Components render in, beside the two Vite
|
|
475
|
+
// always has — declared only for an application that renders them, so
|
|
476
|
+
// `app.rsc: false` is a Vite configuration with nothing added.
|
|
477
|
+
...(flightState == null
|
|
478
|
+
? {}
|
|
479
|
+
: {
|
|
480
|
+
environments: {
|
|
481
|
+
[RSC_ENVIRONMENT]: rscEnvironment({
|
|
482
|
+
root: projectRoot,
|
|
483
|
+
production: env.command === "build",
|
|
484
|
+
exclude: uniflowedPackages(projectRoot),
|
|
485
|
+
}),
|
|
486
|
+
},
|
|
487
|
+
}),
|
|
206
488
|
};
|
|
207
489
|
},
|
|
208
490
|
|
|
209
491
|
configResolved(config) {
|
|
210
492
|
root = config.root;
|
|
211
493
|
base = config.base;
|
|
494
|
+
if (flightState != null) {
|
|
495
|
+
flightState.root = config.root;
|
|
496
|
+
flightState.base = config.base;
|
|
497
|
+
}
|
|
212
498
|
appRoot = path.resolve(root, routerRoot);
|
|
213
499
|
entryPath = path.resolve(root, appEntry);
|
|
214
500
|
},
|
|
@@ -217,8 +503,43 @@ function flowPlugin({ routerRoot, appEntry, command }) {
|
|
|
217
503
|
ensureService();
|
|
218
504
|
},
|
|
219
505
|
|
|
220
|
-
resolveId(id) {
|
|
506
|
+
resolveId(id, importer, resolveOptions) {
|
|
507
|
+
// The client's graph, and the rsc graph too. `@uniflowed/react` is
|
|
508
|
+
// `export * from "react"`, and the rsc graph pre-bundles React's CommonJS
|
|
509
|
+
// under `react-server`: a star re-export of that namespace names nothing
|
|
510
|
+
// Vite's module runner can forward, so under `uf dev` every hook a server
|
|
511
|
+
// component imported from `@uniflowed/react` was `undefined` — `use` first.
|
|
512
|
+
// Importing `react` itself is the module those names are on. The ssr graph
|
|
513
|
+
// keeps its own resolution, because React is external there and Node's
|
|
514
|
+
// interop forwards the names.
|
|
515
|
+
if (
|
|
516
|
+
id === "@uniflowed/react" &&
|
|
517
|
+
(this.environment?.name === RSC_ENVIRONMENT || !isSsr(this, resolveOptions))
|
|
518
|
+
) {
|
|
519
|
+
return this.resolve("react", importer, { ...resolveOptions, skipSelf: true });
|
|
520
|
+
}
|
|
221
521
|
if (id === RUNTIME_PUBLIC_PATH) return RUNTIME_RESOLVED_ID;
|
|
522
|
+
if (id === AUDIT_PUBLIC_PATH) return AUDIT_RESOLVED_ID;
|
|
523
|
+
// The rsc build cannot know a client chunk's URL, because the client
|
|
524
|
+
// build that writes the chunks runs after it. It leaves the manifest as an
|
|
525
|
+
// import, and the ssr build — which bundles the rsc output in — resolves it.
|
|
526
|
+
if (
|
|
527
|
+
id === FLIGHT_VIRTUAL.manifest &&
|
|
528
|
+
flightState?.production === true &&
|
|
529
|
+
this.environment?.name === RSC_ENVIRONMENT
|
|
530
|
+
) {
|
|
531
|
+
return { id, external: true };
|
|
532
|
+
}
|
|
533
|
+
// The React Compiler's runtime, which reads the client's internals and so
|
|
534
|
+
// cannot run where `react` resolved under `react-server`; see
|
|
535
|
+
// `compilerRuntimeSource`.
|
|
536
|
+
if (
|
|
537
|
+
id === "react/compiler-runtime" &&
|
|
538
|
+
flightState != null &&
|
|
539
|
+
this.environment?.name === RSC_ENVIRONMENT
|
|
540
|
+
) {
|
|
541
|
+
return resolved(FLIGHT_VIRTUAL.compilerRuntime);
|
|
542
|
+
}
|
|
222
543
|
if (VIRTUAL_IDS.has(id)) return resolved(id);
|
|
223
544
|
// A module's own stylesheet, which `transform` below asked for by
|
|
224
545
|
// importing this id. Returning it unchanged marks it resolved without
|
|
@@ -229,23 +550,132 @@ function flowPlugin({ routerRoot, appEntry, command }) {
|
|
|
229
550
|
|
|
230
551
|
load(id, loadOptions) {
|
|
231
552
|
if (id === RUNTIME_RESOLVED_ID) return refreshRuntimeSource();
|
|
553
|
+
if (id === AUDIT_RESOLVED_ID) return auditRuntimeSource(accessibility?.axe);
|
|
232
554
|
if (id === resolved(VIRTUAL.routes)) {
|
|
233
|
-
const table = scanRoutes(appRoot);
|
|
555
|
+
const table = scanRoutes(appRoot, { target: routeTarget });
|
|
556
|
+
// Under React Server Components the table is split by graph rather than
|
|
557
|
+
// filtered. The rsc graph renders routes, so it gets every route and
|
|
558
|
+
// boundary and no handler or middleware: those answer a request, and
|
|
559
|
+
// importing one here would resolve its dependencies under
|
|
560
|
+
// `react-server` for nothing. The ssr graph gets exactly those two,
|
|
561
|
+
// because every route it renders reaches it as a payload.
|
|
562
|
+
if (flightState != null && this.environment?.name === RSC_ENVIRONMENT) {
|
|
563
|
+
return routesModuleSource({ ...table, handlers: [], middleware: [] });
|
|
564
|
+
}
|
|
565
|
+
if (flightState != null && isSsr(this, loadOptions)) {
|
|
566
|
+
return routesModuleSource({ ...table, routes: [], notFound: [], errors: [] });
|
|
567
|
+
}
|
|
234
568
|
// The server renders every route, so the server's table is the whole
|
|
235
569
|
// one and is generated with no filter at all. Only the browser's copy
|
|
236
570
|
// is split.
|
|
237
571
|
if (isSsr(this, loadOptions)) return routesModuleSource(table);
|
|
238
572
|
return clientRoutesModule(table);
|
|
239
573
|
}
|
|
240
|
-
|
|
241
|
-
|
|
574
|
+
// Strict Mode belongs to the client entry and to development only: a
|
|
575
|
+
// build passes `false`, so the generated module is the one that existed
|
|
576
|
+
// before #516 and a visitor's browser renders once.
|
|
577
|
+
//
|
|
578
|
+
// `navigation` is in the same entry and has no `isProduction` beside it,
|
|
579
|
+
// deliberately: it is what a link does, and a dev server whose links
|
|
580
|
+
// behave differently from the deployment is the wrong thing to be
|
|
581
|
+
// looking at. See `clientModuleSource`.
|
|
582
|
+
if (id === resolved(VIRTUAL.client) && flightState != null) {
|
|
583
|
+
return flightClientSource(entryPath, {
|
|
584
|
+
instrumentation: instrumentationFile(appRoot, true),
|
|
585
|
+
strictMode: strictMode && !isProduction,
|
|
586
|
+
navigation,
|
|
587
|
+
staleTime,
|
|
588
|
+
routing,
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
if (id === resolved(VIRTUAL.client)) {
|
|
592
|
+
return clientModuleSource(entryPath, {
|
|
593
|
+
instrumentation: instrumentationFile(appRoot, true),
|
|
594
|
+
strictMode: strictMode && !isProduction,
|
|
595
|
+
navigation,
|
|
596
|
+
staleTime,
|
|
597
|
+
mount,
|
|
598
|
+
routing,
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
if (id === resolved(VIRTUAL.server)) {
|
|
602
|
+
return flightState == null
|
|
603
|
+
? serverModuleSource(entryPath, routing, instrumentationFile(appRoot))
|
|
604
|
+
: flightServerSource(
|
|
605
|
+
entryPath,
|
|
606
|
+
VIRTUAL.routes,
|
|
607
|
+
VIRTUAL.actions,
|
|
608
|
+
routing,
|
|
609
|
+
instrumentationFile(appRoot),
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
if (flightState != null) {
|
|
613
|
+
if (id === resolved(FLIGHT_VIRTUAL.entry))
|
|
614
|
+
return rscEntrySource(VIRTUAL.routes, routing, flightState.deployment);
|
|
615
|
+
if (id === resolved(FLIGHT_VIRTUAL.compilerRuntime)) return compilerRuntimeSource();
|
|
616
|
+
if (id === resolved(FLIGHT_VIRTUAL.bridge)) {
|
|
617
|
+
if (server != null) return devBridgeSource();
|
|
618
|
+
if (flightState.rscOutput == null) {
|
|
619
|
+
throw new Error(
|
|
620
|
+
"uf: the server bundle was asked to bundle the rsc graph before it was built. " +
|
|
621
|
+
"`uf build` builds `virtual:uf/rsc` first; a build started some other way has to as well.",
|
|
622
|
+
);
|
|
623
|
+
}
|
|
624
|
+
return builtBridgeSource(flightState.rscOutput);
|
|
625
|
+
}
|
|
626
|
+
if (id === resolved(FLIGHT_VIRTUAL.manifest)) {
|
|
627
|
+
return clientManifestSource(flightState.chunkUrls);
|
|
628
|
+
}
|
|
629
|
+
if (id === resolved(FLIGHT_VIRTUAL.references)) {
|
|
630
|
+
return server != null
|
|
631
|
+
? devReferencesSource(root, base)
|
|
632
|
+
: builtReferencesSource(flightState.chunkUrls);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
// Only `virtual:uf/server` imports this, so it is only ever asked for in
|
|
636
|
+
// the server environment — but the table it carries is every callable
|
|
637
|
+
// endpoint of the build, so it is worth saying that a browser asking for
|
|
638
|
+
// it gets nothing rather than getting the list.
|
|
639
|
+
if (id === resolved(VIRTUAL.actions)) {
|
|
640
|
+
if (!isSsr(this, loadOptions))
|
|
641
|
+
return "export const actions = [];\nexport default actions;\n";
|
|
642
|
+
return actionsModuleSource(actionTables().table);
|
|
643
|
+
}
|
|
242
644
|
if (id.startsWith(STYLE_PREFIX)) return styles.get(id) ?? "";
|
|
645
|
+
|
|
646
|
+
// A `"use server"` module, in the browser's graph only: what the client
|
|
647
|
+
// gets is one `createServerReference` per callable export, and never the
|
|
648
|
+
// file. This is where the second half of the RSC split actually happens
|
|
649
|
+
// — the route filter above decides which *pages* the browser is given,
|
|
650
|
+
// and this decides that an action module's body, its imports and
|
|
651
|
+
// everything only they reached are not the browser's business at all.
|
|
652
|
+
//
|
|
653
|
+
// Substituting the source rather than rewriting it: a transform that
|
|
654
|
+
// stripped the body would have to be right about every way a module can
|
|
655
|
+
// name something, and being wrong once means shipping a database handle.
|
|
656
|
+
// The exports the reference module declares come from the manifest, so
|
|
657
|
+
// they are exactly the exports `uf_rsc` decided are callable endpoints
|
|
658
|
+
// and an import of anything else is a build error rather than a silent
|
|
659
|
+
// `undefined`. `crates/uf_rsc/src/graph/build.rs` colours these modules
|
|
660
|
+
// server for the same reason, so the analysis and the bundle agree.
|
|
661
|
+
//
|
|
662
|
+
// Nor in the rsc graph: a server component that calls an action is calling
|
|
663
|
+
// a function on the server, and it gets the function.
|
|
664
|
+
if (!isSsr(this, loadOptions) && this.environment?.name !== RSC_ENVIRONMENT) {
|
|
665
|
+
const references = actionTables().modules.get(cleanId(id));
|
|
666
|
+
if (references != null) return actionReferenceSource(references);
|
|
667
|
+
}
|
|
243
668
|
return null;
|
|
244
669
|
},
|
|
245
670
|
|
|
246
671
|
async transform(code, id, transformOptions) {
|
|
247
|
-
|
|
248
|
-
|
|
672
|
+
// A view of a barrel's namespace has the barrel's path and none of its
|
|
673
|
+
// source: `uf:barrel-imports` generates it as JavaScript.
|
|
674
|
+
if (!isFlowModule(id) || namespaceViewOf(id) != null) return null;
|
|
675
|
+
// Both server graphs: neither gets a refresh wrapper, and the rsc graph's
|
|
676
|
+
// findings are reported as that graph's.
|
|
677
|
+
const rsc = this.environment?.name === RSC_ENVIRONMENT;
|
|
678
|
+
const ssr = transformOptions?.ssr === true || this.environment?.name === "ssr" || rsc;
|
|
249
679
|
const refresh = !isProduction && !ssr && server != null;
|
|
250
680
|
const out = await ensureService().transform(cleanId(id), code, {
|
|
251
681
|
development: !isProduction,
|
|
@@ -257,11 +687,20 @@ function flowPlugin({ routerRoot, appEntry, command }) {
|
|
|
257
687
|
id: cleanId(id),
|
|
258
688
|
root,
|
|
259
689
|
diagnostics: out.diagnostics,
|
|
260
|
-
environment: ssr ? "ssr" : "client",
|
|
690
|
+
environment: rsc ? RSC_ENVIRONMENT : ssr ? "ssr" : "client",
|
|
261
691
|
reported,
|
|
262
692
|
suppressed,
|
|
263
693
|
});
|
|
264
|
-
|
|
694
|
+
let map = out.map == null ? null : JSON.parse(out.map);
|
|
695
|
+
let output = out.code;
|
|
696
|
+
if (relayEnabled && output.includes("graphql")) {
|
|
697
|
+
relayOptions ??= relayConfig(root);
|
|
698
|
+
const transformed = await transformRelay(output, cleanId(id), map, await relayOptions);
|
|
699
|
+
if (transformed != null) {
|
|
700
|
+
output = transformed.code;
|
|
701
|
+
map = transformed.map;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
265
704
|
// StyleX. `uf transform` compiled the module's `stylex.create` calls into
|
|
266
705
|
// class names and handed back the rules they declared; the rules become a
|
|
267
706
|
// module of their own that this one imports.
|
|
@@ -271,10 +710,16 @@ function flowPlugin({ routerRoot, appEntry, command }) {
|
|
|
271
710
|
// business: Vite already injects a stylesheet in dev, extracts it in a
|
|
272
711
|
// build, code-splits it per chunk, and replaces it over HMR. A module
|
|
273
712
|
// whose styles are gone stops importing it, and Vite notices.
|
|
713
|
+
//
|
|
714
|
+
// The stylesheet is named by the module's path from the project root, the
|
|
715
|
+
// spelling `moduleId` gives the module graph report, and not by its
|
|
716
|
+
// absolute path. In a client chunk the stylesheet is one of the chunk's
|
|
717
|
+
// sources, so its name is written into the source map a site publishes,
|
|
718
|
+
// and an absolute name would publish where the machine that built it
|
|
719
|
+
// keeps its files. A module outside the root climbs out with `../`.
|
|
274
720
|
const styled = out.css != null && out.css !== "";
|
|
275
|
-
let output = out.code;
|
|
276
721
|
if (styled) {
|
|
277
|
-
const styleId = `${STYLE_PREFIX}${cleanId(id)}.css`;
|
|
722
|
+
const styleId = `${STYLE_PREFIX}${moduleId(root, cleanId(id))}.css`;
|
|
278
723
|
styles.set(styleId, out.css);
|
|
279
724
|
output = `import ${JSON.stringify(styleId)};\n${output}`;
|
|
280
725
|
}
|
|
@@ -307,25 +752,115 @@ function flowPlugin({ routerRoot, appEntry, command }) {
|
|
|
307
752
|
}
|
|
308
753
|
},
|
|
309
754
|
|
|
755
|
+
// The two scripts a development document loads before its own, and
|
|
756
|
+
// nothing at all in a build — which is the whole of "the hook is out of a
|
|
757
|
+
// production build" (ubugeeei-prod/uf#503) and of "production does not run
|
|
758
|
+
// under Strict Mode" (#516, whose flag is generated into
|
|
759
|
+
// `virtual:uf/client` rather than injected here).
|
|
760
|
+
//
|
|
761
|
+
// DevTools first, and as a *classic* script rather than a module: React
|
|
762
|
+
// registers itself with `__REACT_DEVTOOLS_GLOBAL_HOOK__` while `react-dom`
|
|
763
|
+
// is evaluated and never again, so the hook has to exist before any module
|
|
764
|
+
// runs. A classic inline script runs while the parser is on it; a module
|
|
765
|
+
// waits for the document. `internal/devtools.js` has the rest of the
|
|
766
|
+
// argument, and the three conditions DevTools needs.
|
|
310
767
|
transformIndexHtml() {
|
|
311
768
|
if (isProduction) return [];
|
|
312
|
-
|
|
769
|
+
const tags = [
|
|
313
770
|
{
|
|
314
771
|
tag: "script",
|
|
315
|
-
attrs: {
|
|
772
|
+
attrs: { "data-uf-dev-head-preamble": "react-devtools" },
|
|
773
|
+
children: devtoolsPreamble(),
|
|
774
|
+
injectTo: "head-prepend",
|
|
775
|
+
},
|
|
776
|
+
{
|
|
777
|
+
tag: "script",
|
|
778
|
+
attrs: { type: "module", "data-uf-dev-head-preamble": "react-refresh" },
|
|
316
779
|
children: preambleCode(base),
|
|
317
780
|
injectTo: "head-prepend",
|
|
318
781
|
},
|
|
319
782
|
];
|
|
783
|
+
// Only when the project has the engine. A tag pointing at a module that
|
|
784
|
+
// cannot resolve `axe-core` would be a red console on every page of a
|
|
785
|
+
// project that never asked for an audit, which is a worse default than
|
|
786
|
+
// no audit.
|
|
787
|
+
const audit = auditTag(base, auditsPage);
|
|
788
|
+
if (audit != null) tags.push(audit);
|
|
789
|
+
return tags;
|
|
790
|
+
},
|
|
791
|
+
|
|
792
|
+
// An edit to a server component changes what the rsc graph renders and no
|
|
793
|
+
// module the browser holds, so Vite has nothing to tell the browser. It is
|
|
794
|
+
// reloaded, which renders the edit; an edit to a client module — which the
|
|
795
|
+
// rsc graph only holds references to — is left to Fast Refresh.
|
|
796
|
+
hotUpdate({ modules }) {
|
|
797
|
+
if (flightState == null || server == null) return;
|
|
798
|
+
if (this.environment?.name !== RSC_ENVIRONMENT) return;
|
|
799
|
+
const serverSide = modules.some((module) => {
|
|
800
|
+
const file = module.file ?? cleanId(module.id ?? "");
|
|
801
|
+
return file !== "" && !flightState.clientModules.has(file);
|
|
802
|
+
});
|
|
803
|
+
if (serverSide) server.environments.client.hot.send({ type: "full-reload", path: "*" });
|
|
320
804
|
},
|
|
321
805
|
|
|
322
806
|
configureServer(devServer) {
|
|
323
807
|
server = devServer;
|
|
808
|
+
const diagnostics = startDevState(root, () => ({
|
|
809
|
+
routes: scanRoutes(appRoot, { target: routeTarget }).routes.map((route) => ({
|
|
810
|
+
path: route.path,
|
|
811
|
+
page: route.page,
|
|
812
|
+
layouts: route.layouts,
|
|
813
|
+
})),
|
|
814
|
+
actions: actionTables().table,
|
|
815
|
+
}));
|
|
816
|
+
devServer.middlewares.use(diagnostics.middleware);
|
|
817
|
+
devServer.httpServer?.once("close", diagnostics.close);
|
|
818
|
+
// The ssr graph's way into the rsc graph; see `devBridgeSource`.
|
|
819
|
+
if (flightState != null) {
|
|
820
|
+
globalThis[Symbol.for(DEV_RSC_HOOK)] = () =>
|
|
821
|
+
devServer.environments[RSC_ENVIRONMENT].runner.import(FLIGHT_VIRTUAL.entry);
|
|
822
|
+
}
|
|
324
823
|
devServer.httpServer?.once("close", () => {
|
|
325
824
|
service?.close();
|
|
326
825
|
service = null;
|
|
327
826
|
});
|
|
328
827
|
|
|
828
|
+
// `app.router.headers` and `redirects`, in front of Vite's own middleware
|
|
829
|
+
// — the hook's body runs before those are installed — so a redirect
|
|
830
|
+
// answers before `public/` is looked in and a header reaches a file Vite
|
|
831
|
+
// serves, as both do in front of the static half of every other door.
|
|
832
|
+
// Vite's module server is left alone: `/@vite/client` and `/@fs/…` are
|
|
833
|
+
// development plumbing no deployment has, and `/__uf/` is the browser's
|
|
834
|
+
// channel back. Not mounted at all for a project with neither list.
|
|
835
|
+
if (answersInFrontOfFiles(routing)) {
|
|
836
|
+
devServer.middlewares.use((request, response, next) => {
|
|
837
|
+
const url = request.url ?? "/";
|
|
838
|
+
// Vite serves its own paths under the base path too, so they are
|
|
839
|
+
// recognised once it is taken off.
|
|
840
|
+
const under =
|
|
841
|
+
routing.basePath !== "" && url.startsWith(`${routing.basePath}/`)
|
|
842
|
+
? url.slice(routing.basePath.length)
|
|
843
|
+
: url;
|
|
844
|
+
if (
|
|
845
|
+
under.startsWith("/@") ||
|
|
846
|
+
under.startsWith("/node_modules/") ||
|
|
847
|
+
under.startsWith("/__uf/")
|
|
848
|
+
) {
|
|
849
|
+
next();
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
answerRouting(routing, toAddressRequest(request), response)
|
|
853
|
+
.then((answered) => {
|
|
854
|
+
if (answered) return;
|
|
855
|
+
// The bare base path is the root, which Vite only knows as
|
|
856
|
+
// `/docs/`; see `forViteBase`.
|
|
857
|
+
forViteBase(routing, request);
|
|
858
|
+
next();
|
|
859
|
+
})
|
|
860
|
+
.catch(next);
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
|
|
329
864
|
// A reserved file appearing or disappearing changes the route table,
|
|
330
865
|
// which lives in a virtual module the watcher knows nothing about.
|
|
331
866
|
//
|
|
@@ -335,14 +870,15 @@ function flowPlugin({ routerRoot, appEntry, command }) {
|
|
|
335
870
|
// missing `route` — so adding a route handler to a running dev server
|
|
336
871
|
// did not rebuild the table and the handler stayed invisible until a
|
|
337
872
|
// restart. A list that has to match another list has to be that list.
|
|
338
|
-
const
|
|
339
|
-
|
|
340
|
-
.join("|");
|
|
873
|
+
const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
874
|
+
const stems = Object.values(RESERVED).map(escapeRegExp).join("|");
|
|
341
875
|
const reserved = new RegExp(`/(${stems})(\\.[a-z]+)?\\.(js|jsx|mdx)$`);
|
|
342
876
|
const onRouteFile = (file) => {
|
|
343
877
|
if (!reserved.test(file) || !file.startsWith(appRoot)) return;
|
|
344
|
-
const
|
|
345
|
-
|
|
878
|
+
for (const id of [VIRTUAL.routes, VIRTUAL.server, VIRTUAL.client]) {
|
|
879
|
+
const module = devServer.moduleGraph.getModuleById(resolved(id));
|
|
880
|
+
if (module) devServer.moduleGraph.invalidateModule(module);
|
|
881
|
+
}
|
|
346
882
|
devServer.ws.send({ type: "full-reload", path: "*" });
|
|
347
883
|
};
|
|
348
884
|
devServer.watcher.on("add", onRouteFile);
|
|
@@ -361,8 +897,19 @@ function flowPlugin({ routerRoot, appEntry, command }) {
|
|
|
361
897
|
devServer.watcher.add(manifestPath);
|
|
362
898
|
const onManifest = (file) => {
|
|
363
899
|
if (path.resolve(file) !== manifestPath) return;
|
|
900
|
+
const previousActions = actionTables().modules;
|
|
901
|
+
// The action tables are read from the same file and are memoised on
|
|
902
|
+
// its size and modification time, which is a pair two writes inside
|
|
903
|
+
// one millisecond can share. This is the answer that does not
|
|
904
|
+
// depend on a clock.
|
|
905
|
+
forgetActions();
|
|
906
|
+
const nextActions = actionTables().modules;
|
|
907
|
+
invalidateActionModules(devServer.moduleGraph, previousActions);
|
|
908
|
+
invalidateActionModules(devServer.moduleGraph, nextActions);
|
|
364
909
|
const routes = devServer.moduleGraph.getModuleById(resolved(VIRTUAL.routes));
|
|
365
910
|
if (routes) devServer.moduleGraph.invalidateModule(routes);
|
|
911
|
+
const actions = devServer.moduleGraph.getModuleById(resolved(VIRTUAL.actions));
|
|
912
|
+
if (actions) devServer.moduleGraph.invalidateModule(actions);
|
|
366
913
|
devServer.ws.send({ type: "full-reload", path: "*" });
|
|
367
914
|
};
|
|
368
915
|
devServer.watcher.on("add", onManifest);
|
|
@@ -370,56 +917,299 @@ function flowPlugin({ routerRoot, appEntry, command }) {
|
|
|
370
917
|
}
|
|
371
918
|
|
|
372
919
|
// After Vite's own middlewares, so `/@vite/client`, `/@id/...` and
|
|
373
|
-
// static files are served first and only
|
|
374
|
-
//
|
|
920
|
+
// static files are served first and only what Vite declined reaches uf.
|
|
921
|
+
//
|
|
922
|
+
// # One renderer
|
|
923
|
+
//
|
|
924
|
+
// This is the only middleware that renders a document under `uf dev`,
|
|
925
|
+
// and that is worth stating because there were two. `driver.js` added a
|
|
926
|
+
// second one after `createServer` had returned, which put it *later* in
|
|
927
|
+
// the connect stack than this one — so for every request this one
|
|
928
|
+
// claimed, this one decided, and the other was reached only for what
|
|
929
|
+
// this one declined. Route-handler dispatch was in the other. A
|
|
930
|
+
// `GET /feed` from a browser is `Accept: text/html` with no extension,
|
|
931
|
+
// so it looked like a document, so `app/feed/$route.js` was never
|
|
932
|
+
// asked and the reader got the route table's page — or the not-found
|
|
933
|
+
// page — for a path that had a handler. See ubugeeei-prod/uf#349.
|
|
934
|
+
//
|
|
935
|
+
// Two renderers is also how the two came to disagree about the render
|
|
936
|
+
// result's `headers`: this one wrote them and the driver's did not, so a
|
|
937
|
+
// loader calling `redirect()` answered a browser with a `307` carrying
|
|
938
|
+
// no `Location` and a meta-refresh body — a redirect that works when you
|
|
939
|
+
// deploy it and not while you are writing it. See
|
|
940
|
+
// ubugeeei-prod/uf#338.
|
|
941
|
+
//
|
|
942
|
+
// So the driver's middleware is gone and everything it did is here: it
|
|
943
|
+
// claims every request, runs the guard, the action endpoint and the
|
|
944
|
+
// dispatcher for every method, and hands back to Vite's chain what none
|
|
945
|
+
// of them answered.
|
|
946
|
+
//
|
|
947
|
+
// # What is still not `createFetchHandler`
|
|
948
|
+
//
|
|
949
|
+
// One middleware rather than two, and still not the function every
|
|
950
|
+
// deployment runs. It cannot be: `transformIndexHtml` takes a whole
|
|
951
|
+
// document, so the render has to be collected here rather than streamed
|
|
952
|
+
// (ubugeeei-prod/uf#374), and a request nothing claimed has to go back
|
|
953
|
+
// to Vite's chain rather than become a 404 — neither of which a handler
|
|
954
|
+
// that always answers with a `Response` can do.
|
|
955
|
+
//
|
|
956
|
+
// What that still costs, written down so the next reader does not have
|
|
957
|
+
// to find it: `createFetchHandler` renders inside a cache scope even
|
|
958
|
+
// with no cache configured, so a component calling `cacheLife` states a
|
|
959
|
+
// lifetime nobody honours; here there is no scope, so the same component
|
|
960
|
+
// throws under `uf dev` and renders under `uf start`. Closing that means
|
|
961
|
+
// the generated server entry handing the host a scope the way it already
|
|
962
|
+
// hands it `beginRequest` — see `serverModuleSource` in
|
|
963
|
+
// `./internal/routes.js` — and it is the next thing to remove from this
|
|
964
|
+
// list rather than something this middleware can decide on its own.
|
|
375
965
|
return () => {
|
|
966
|
+
// The browser's own reporting channel, mounted above the application
|
|
967
|
+
// so that a report never reaches a project's `$middleware.js` or
|
|
968
|
+
// its route table. See `internal/diagnostics.js`.
|
|
969
|
+
devServer.middlewares.use(
|
|
970
|
+
createChannelMiddleware((diagnostic) => emit("diagnostic", diagnostic)),
|
|
971
|
+
);
|
|
972
|
+
|
|
376
973
|
devServer.middlewares.use(async (request, response, next) => {
|
|
377
|
-
|
|
974
|
+
// `request.url` and not `originalUrl`, which is the URL Vite's base
|
|
975
|
+
// middleware has already stripped the base from — and the route
|
|
976
|
+
// table's paths have no base in them either.
|
|
977
|
+
//
|
|
978
|
+
// `let`, because a rewrite moves it: from here on it is the address
|
|
979
|
+
// the application answers for, which is what the render, the payload
|
|
980
|
+
// and the document test below all have to agree on.
|
|
981
|
+
let url = request.url ?? "/";
|
|
982
|
+
// Declared out here so the catch below can still settle: a request
|
|
983
|
+
// that failed is a request that happened, and a middleware that
|
|
984
|
+
// logged its arrival is owed its callback either way.
|
|
985
|
+
let lifecycle = null;
|
|
378
986
|
try {
|
|
379
|
-
const url = request.url ?? "/";
|
|
380
987
|
const entry = await importServerEntry(devServer);
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
//
|
|
384
|
-
|
|
385
|
-
// `
|
|
386
|
-
//
|
|
387
|
-
//
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
//
|
|
392
|
-
//
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
988
|
+
// At `request.url`, which Vite's base middleware has already taken
|
|
989
|
+
// `app.router.basePath` off: the application path, as every other
|
|
990
|
+
// front door hands the application once it has admitted a request.
|
|
991
|
+
const arrived = await toRequest(request, devServer.config, url);
|
|
992
|
+
// `app.router.rewrites`, where `createFetchHandler` applies them for
|
|
993
|
+
// every other door: after the files Vite already served, before the
|
|
994
|
+
// guard — so the guard that runs is the destination's.
|
|
995
|
+
let asRequest = (await rewriteRouting(entry.routing, arrived)) ?? arrived;
|
|
996
|
+
if (asRequest !== arrived) url = addressOf(asRequest);
|
|
997
|
+
|
|
998
|
+
// The request begins here and ends when the response has been
|
|
999
|
+
// written, which is what `after()` promises and what `uf preview`,
|
|
1000
|
+
// `uf start` and a compiled binary all do too — a middleware that
|
|
1001
|
+
// logs a response's status has to mean the same thing in
|
|
1002
|
+
// development as in production. See `internal/serve.js` and
|
|
1003
|
+
// ubugeeei-prod/uf#389.
|
|
1004
|
+
lifecycle = await beginRequest(entry, arrived);
|
|
1005
|
+
const answered = await lifecycle.run(async () => {
|
|
1006
|
+
// Before anything answers: a middleware guards a subtree, so it
|
|
1007
|
+
// has to run for a page, for a route handler, and for a path
|
|
1008
|
+
// under it that matches neither. Running it inside the
|
|
1009
|
+
// dispatcher and again inside the renderer would have left
|
|
1010
|
+
// `/dashboard/typo` unguarded and run it twice for a path that
|
|
1011
|
+
// is both. See ubugeeei-prod/uf#260.
|
|
1012
|
+
//
|
|
1013
|
+
// A `Request` back is a middleware's `rewrite()`, already past
|
|
1014
|
+
// the destination's own middleware.
|
|
399
1015
|
const guarded = await entry.runMiddleware(asRequest);
|
|
400
|
-
if (guarded
|
|
1016
|
+
if (guarded instanceof Request) {
|
|
1017
|
+
asRequest = guarded;
|
|
1018
|
+
url = addressOf(guarded);
|
|
1019
|
+
} else if (guarded != null) {
|
|
401
1020
|
await send(response, guarded);
|
|
402
|
-
return;
|
|
1021
|
+
return true;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
// Then a server action, below the guard and above the handlers.
|
|
1025
|
+
// It declines every request that carries no action id, so this
|
|
1026
|
+
// costs an ordinary request one `headers.get`; and it answers
|
|
1027
|
+
// every request that carries one, refusals included, so an
|
|
1028
|
+
// action can never fall through to a route handler that happens
|
|
1029
|
+
// to sit at the URL it was posted to. The same two lines are in
|
|
1030
|
+
// `@uniflowed/server`'s `fetch.js`, which is what every
|
|
1031
|
+
// deployment runs.
|
|
1032
|
+
const acted = await entry.callAction(asRequest);
|
|
1033
|
+
if (acted != null) {
|
|
1034
|
+
await send(response, acted);
|
|
1035
|
+
return true;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
// Then the route handlers, above the renderer and for every
|
|
1039
|
+
// method: a path that answers a request is not a document,
|
|
1040
|
+
// whatever the client said it would accept. `curl /api/thing`
|
|
1041
|
+
// and a `<form action>` navigation both send `Accept:
|
|
1042
|
+
// text/html`, and both want the handler's answer — which is the
|
|
1043
|
+
// whole of ubugeeei-prod/uf#349.
|
|
1044
|
+
// A browser that is navigating, asking for the next route's
|
|
1045
|
+
// payload: after the guard and before the handlers, for the
|
|
1046
|
+
// reasons `@uniflowed/server`'s `internal/flight.js` gives.
|
|
1047
|
+
const payloadFor =
|
|
1048
|
+
entry.flight == null ? null : flightDocumentPath(url.split("?")[0]);
|
|
1049
|
+
if (payloadFor != null && (request.method === "GET" || request.method === "HEAD")) {
|
|
1050
|
+
const query = url.includes("?") ? url.slice(url.indexOf("?")) : "";
|
|
1051
|
+
const target = payloadFor + query;
|
|
1052
|
+
const fromHeader = request.headers[INTERCEPTED_FROM_HEADER];
|
|
1053
|
+
const answered = await entry.flight(target, {
|
|
1054
|
+
onError: (error) => reportRenderError(devServer, target, error),
|
|
1055
|
+
interceptedFrom: Array.isArray(fromHeader) ? fromHeader[0] : fromHeader,
|
|
1056
|
+
});
|
|
1057
|
+
if (answered.error != null) reportRenderError(devServer, target, answered.error);
|
|
1058
|
+
if (request.method === "HEAD") await answered.stream?.cancel();
|
|
1059
|
+
await send(
|
|
1060
|
+
response,
|
|
1061
|
+
new Response(request.method === "HEAD" ? null : answered.stream, {
|
|
1062
|
+
status: answered.status,
|
|
1063
|
+
headers: answered.headers,
|
|
1064
|
+
}),
|
|
1065
|
+
);
|
|
1066
|
+
return true;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
const handled = await entry.dispatch(asRequest);
|
|
1070
|
+
if (handled != null) {
|
|
1071
|
+
await send(response, handled);
|
|
1072
|
+
return true;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
// Only a navigation reaches the renderer. A page cannot answer a
|
|
1076
|
+
// `POST`, and letting one try would turn a missing handler into
|
|
1077
|
+
// a rendered page with a 200 rather than a 404.
|
|
1078
|
+
//
|
|
1079
|
+
// `notADocumentBecause` is stricter than the production handler, which
|
|
1080
|
+
// renders anything a static file did not answer, and the
|
|
1081
|
+
// difference is Vite's chain: `/@id/…`, `/node_modules/…` and
|
|
1082
|
+
// any path with an extension belong to the module server, and a
|
|
1083
|
+
// request one of those declined has to go back to it rather than
|
|
1084
|
+
// become a rendered 404 page. What it costs is that
|
|
1085
|
+
// `/favicon.svg` on a project that has none is a bare 404 here
|
|
1086
|
+
// and the project's own not-found *page* under `uf preview` and
|
|
1087
|
+
// `uf start` — a difference in the body of a 404 for a path that
|
|
1088
|
+
// is an asset request in the first place.
|
|
1089
|
+
const notDocument = notADocumentBecause(request, url);
|
|
1090
|
+
if (notDocument === "accept") {
|
|
1091
|
+
// Everything about this is a navigation except the header, and
|
|
1092
|
+
// the path is one uf renders. Say so, rather than letting the
|
|
1093
|
+
// chain below answer `Cannot GET /` in somebody else's words.
|
|
1094
|
+
documentNeedsHtml(request, response);
|
|
1095
|
+
return true;
|
|
1096
|
+
}
|
|
1097
|
+
if (notDocument != null) return false;
|
|
1098
|
+
|
|
1099
|
+
// A single-page project's deployment answers every navigation
|
|
1100
|
+
// with the same empty shell, so this does too. Rendering the
|
|
1101
|
+
// route here instead would have been the better-looking dev
|
|
1102
|
+
// server and the wrong one: a page that only works because the
|
|
1103
|
+
// server rendered it would work all through development and be
|
|
1104
|
+
// blank the day it shipped. It is the same argument
|
|
1105
|
+
// `app.rendering.navigation` makes about a link, one level up.
|
|
1106
|
+
//
|
|
1107
|
+
// The three steps above still ran — the guard, the action, the
|
|
1108
|
+
// handler — and each of them is something `uf build` refuses in
|
|
1109
|
+
// a `["csr"]` project by name. A dev server that skipped them
|
|
1110
|
+
// would hide the very thing the build is going to stop.
|
|
1111
|
+
if (mount === "render") {
|
|
1112
|
+
response.statusCode = 200;
|
|
1113
|
+
response.setHeader("content-type", "text/html; charset=utf-8");
|
|
1114
|
+
const shell = entry.shellDocument({
|
|
1115
|
+
scripts: [devUrlFor(VIRTUAL.client)],
|
|
1116
|
+
styles: [],
|
|
1117
|
+
preloads: [],
|
|
1118
|
+
});
|
|
1119
|
+
response.end(await devServer.transformIndexHtml(url, shell));
|
|
1120
|
+
return true;
|
|
403
1121
|
}
|
|
404
1122
|
|
|
405
1123
|
const result = await entry.render(
|
|
406
1124
|
url,
|
|
407
1125
|
{ scripts: [devUrlFor(VIRTUAL.client)], styles: [], preloads: [] },
|
|
408
|
-
{
|
|
1126
|
+
{
|
|
1127
|
+
// A boundary that threw after the shell went out.
|
|
1128
|
+
// `result.error` cannot carry it — the caller already has the
|
|
1129
|
+
// result by then — so the terminal hears about it here or not
|
|
1130
|
+
// at all.
|
|
1131
|
+
onError: (error) => reportRenderError(devServer, url, error),
|
|
1132
|
+
// Vite sees the head and only the head. That is what lets the
|
|
1133
|
+
// development server stream like every other host — see below.
|
|
1134
|
+
//
|
|
1135
|
+
// Under React Server Components it is also where a server
|
|
1136
|
+
// component's stylesheets are linked in development: they are
|
|
1137
|
+
// in the rsc graph, which the browser never loads, and they
|
|
1138
|
+
// enter it when the route's modules are imported — after this
|
|
1139
|
+
// call and before the head is written, which is when this
|
|
1140
|
+
// runs. See `devStylesheets`.
|
|
1141
|
+
transformHead: (head) =>
|
|
1142
|
+
devServer.transformIndexHtml(
|
|
1143
|
+
url,
|
|
1144
|
+
flightState == null ? head : linkStylesheets(head, devStylesheets(devServer)),
|
|
1145
|
+
),
|
|
1146
|
+
// And what the streaming actually did, when it changed. The
|
|
1147
|
+
// router has already decided there is something worth saying
|
|
1148
|
+
// and written the words — see its `internal/inspector.js`,
|
|
1149
|
+
// which cannot be imported from here because this file is
|
|
1150
|
+
// plain JavaScript that Vite loads before any Flow transform
|
|
1151
|
+
// exists. `info`, because a page that streamed is a
|
|
1152
|
+
// measurement and not a problem; `origin`, because a report
|
|
1153
|
+
// that does not say which page produced it is one somebody
|
|
1154
|
+
// has to reproduce before they can act on it.
|
|
1155
|
+
onStream: (diagnostic) =>
|
|
1156
|
+
emit("diagnostic", {
|
|
1157
|
+
severity: "info",
|
|
1158
|
+
origin: url,
|
|
1159
|
+
message: diagnostic.message,
|
|
1160
|
+
detail: diagnostic.detail,
|
|
1161
|
+
}),
|
|
1162
|
+
},
|
|
409
1163
|
);
|
|
410
1164
|
if (result.error != null) reportRenderError(devServer, url, result.error);
|
|
411
|
-
//
|
|
412
|
-
//
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
1165
|
+
// Piped, like every other host. This used to collect the whole
|
|
1166
|
+
// document and transform it at the end, because
|
|
1167
|
+
// `transformIndexHtml` is a *whole document* hook — which made the
|
|
1168
|
+
// one place a developer would notice streaming the one place it
|
|
1169
|
+
// did not happen: a slow page showed nothing until it was finished
|
|
1170
|
+
// and `$loading.js` looked broken.
|
|
1171
|
+
//
|
|
1172
|
+
// `transformHead` above is the seam. `internal/stream.js` already
|
|
1173
|
+
// held the opening chunk back until the head was complete and
|
|
1174
|
+
// forwarded everything after it untouched, so the hook only ever
|
|
1175
|
+
// sees the head — and Vite's dev hook handles a document that ends
|
|
1176
|
+
// mid-`<body>` without complaint, which was the open question on
|
|
1177
|
+
// ubugeeei-prod/uf#374.
|
|
1178
|
+
response.statusCode = result.status ?? 200;
|
|
1179
|
+
// The render's own headers, then the content type over the top:
|
|
1180
|
+
// exactly the order `@uniflowed/server`'s `fetch.js` writes them
|
|
1181
|
+
// in, so a `Location` from `redirect()` survives here and a
|
|
1182
|
+
// render cannot claim to be something other than a document.
|
|
416
1183
|
for (const [name, value] of Object.entries(result.headers ?? {})) {
|
|
417
1184
|
response.setHeader(name, value);
|
|
418
1185
|
}
|
|
419
|
-
response.
|
|
1186
|
+
response.setHeader("content-type", "text/html; charset=utf-8");
|
|
1187
|
+
await result.pipe(response);
|
|
1188
|
+
return true;
|
|
420
1189
|
});
|
|
1190
|
+
|
|
1191
|
+
if (!answered) {
|
|
1192
|
+
// The one path where uf is not the one writing the response: a
|
|
1193
|
+
// request nothing claimed goes back to Vite's chain. The guard
|
|
1194
|
+
// has still run and may have deferred work, so `close` — the
|
|
1195
|
+
// socket saying the response is over, however it ended — is the
|
|
1196
|
+
// only honest signal left that the bytes are out.
|
|
1197
|
+
response.once("close", lifecycle.settle);
|
|
1198
|
+
next();
|
|
1199
|
+
return;
|
|
1200
|
+
}
|
|
1201
|
+
await lifecycle.settle();
|
|
421
1202
|
} catch (error) {
|
|
422
|
-
|
|
1203
|
+
if (lifecycle != null) await lifecycle.settle();
|
|
1204
|
+
// Map the stack back onto the Flow source before it reaches the
|
|
1205
|
+
// overlay.
|
|
1206
|
+
if (error instanceof Error) devServer.ssrFixStacktrace(error);
|
|
1207
|
+
emit("diagnostic", {
|
|
1208
|
+
severity: "error",
|
|
1209
|
+
kind: "runtime",
|
|
1210
|
+
origin: url,
|
|
1211
|
+
...errorEvent(error),
|
|
1212
|
+
});
|
|
423
1213
|
next(error);
|
|
424
1214
|
}
|
|
425
1215
|
});
|
|
@@ -442,11 +1232,7 @@ function mdxPlugin(markdown) {
|
|
|
442
1232
|
enforce: "pre",
|
|
443
1233
|
...mdx({
|
|
444
1234
|
jsxImportSource: "react",
|
|
445
|
-
remarkPlugins: [
|
|
446
|
-
remarkGfm,
|
|
447
|
-
remarkFrontmatter,
|
|
448
|
-
[remarkMdxFrontmatter, { name: "frontmatter" }],
|
|
449
|
-
],
|
|
1235
|
+
remarkPlugins: [remarkGfm, remarkFrontmatter, remarkFrontmatterExport],
|
|
450
1236
|
rehypePlugins,
|
|
451
1237
|
}),
|
|
452
1238
|
name: "uf:mdx",
|
|
@@ -471,16 +1257,75 @@ async function importServerEntry(devServer) {
|
|
|
471
1257
|
return devServer.ssrLoadModule(VIRTUAL.server);
|
|
472
1258
|
}
|
|
473
1259
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
1260
|
+
/**
|
|
1261
|
+
* The path and query a `Request` is for, which is what the dev server's
|
|
1262
|
+
* renderer and payload test are keyed on.
|
|
1263
|
+
*
|
|
1264
|
+
* @param {Request} request
|
|
1265
|
+
*/
|
|
1266
|
+
function addressOf(request) {
|
|
1267
|
+
const url = new URL(request.url);
|
|
1268
|
+
return url.pathname + url.search;
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
/**
|
|
1272
|
+
* Why `request` is not a document request, or `null` when it is one.
|
|
1273
|
+
*
|
|
1274
|
+
* `url` is the address the application answers for, which a rewrite may have
|
|
1275
|
+
* moved away from the one the request line named.
|
|
1276
|
+
*
|
|
1277
|
+
* A reason rather than a boolean because one of the four is worth saying out
|
|
1278
|
+
* loud. Three of them mean the request belongs to somebody else — Vite's module
|
|
1279
|
+
* server, a static file, or a method a page cannot answer — and handing it back
|
|
1280
|
+
* is the whole point. The fourth means the client asked for a page uf would
|
|
1281
|
+
* have rendered and did not say it accepts HTML, and `curl` is the client that
|
|
1282
|
+
* does that. See ubugeeei-prod/uf#675.
|
|
1283
|
+
*
|
|
1284
|
+
* The extension test moved above the `accept` test so the two cannot be
|
|
1285
|
+
* confused: `/favicon.svg` with `Accept: *\/*` is an asset, not a navigation
|
|
1286
|
+
* with the wrong header, and still goes back to Vite's chain untouched.
|
|
1287
|
+
*/
|
|
1288
|
+
function notADocumentBecause(request, url = request.url ?? "/") {
|
|
1289
|
+
if (request.method !== "GET" && request.method !== "HEAD") return "method";
|
|
1290
|
+
if (url.startsWith("/@") || url.startsWith("/node_modules/")) return "module-server";
|
|
480
1291
|
const pathname = url.split("?")[0];
|
|
481
1292
|
// A request for a file — `/favicon.svg`, `/assets/x.js` — that no static
|
|
482
1293
|
// middleware answered is a 404, not a page.
|
|
483
|
-
|
|
1294
|
+
if (/\.[a-z0-9]+$/i.test(pathname)) return "asset";
|
|
1295
|
+
const accept = request.headers.accept ?? "";
|
|
1296
|
+
if (!accept.includes("text/html")) return "accept";
|
|
1297
|
+
return null;
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
/**
|
|
1301
|
+
* The 404 for a navigation that did not ask for HTML.
|
|
1302
|
+
*
|
|
1303
|
+
* uf's own words rather than the framework underneath saying `Cannot GET /` in
|
|
1304
|
+
* its: the path is one uf would have rendered, and the only thing that stopped
|
|
1305
|
+
* it is a header the reader cannot see from the terminal. Development only —
|
|
1306
|
+
* this middleware is the dev server — and `uf preview` and `uf start` answer a
|
|
1307
|
+
* 404 with nothing in it, as `docs/security.md` requires.
|
|
1308
|
+
*
|
|
1309
|
+
* The client's `Accept` is quoted back, so it is treated the way every other
|
|
1310
|
+
* piece of foreign text here is treated: control characters removed and the
|
|
1311
|
+
* length capped. A header is not a thing a terminal should be asked to run.
|
|
1312
|
+
* See ubugeeei-prod/uf#649.
|
|
1313
|
+
*/
|
|
1314
|
+
function documentNeedsHtml(request, response) {
|
|
1315
|
+
const accept = String(request.headers.accept ?? "")
|
|
1316
|
+
// eslint-disable-next-line no-control-regex
|
|
1317
|
+
.replace(/[\u0000-\u001f\u007f]/g, " ")
|
|
1318
|
+
.slice(0, 80);
|
|
1319
|
+
const path = String(request.url ?? "/").slice(0, 200);
|
|
1320
|
+
const body =
|
|
1321
|
+
`404 ${request.method} ${path}\n\n` +
|
|
1322
|
+
"This path is a page, and a page is rendered for a request that accepts\n" +
|
|
1323
|
+
`HTML. This request sent \`Accept: ${accept || "(none)"}\`.\n\n` +
|
|
1324
|
+
` curl -H 'Accept: text/html' http://${request.headers.host ?? "localhost"}${path}\n\n` +
|
|
1325
|
+
"A browser sends it; `curl` does not. Nothing is wrong with the route.\n";
|
|
1326
|
+
response.statusCode = 404;
|
|
1327
|
+
response.setHeader("content-type", "text/plain; charset=utf-8");
|
|
1328
|
+
response.end(body);
|
|
484
1329
|
}
|
|
485
1330
|
|
|
486
1331
|
/** The name of the environment variable that turns every finding back on. */
|
|
@@ -490,14 +1335,25 @@ const ALL_DIAGNOSTICS = "UF_REACT_COMPILER_DIAGNOSTICS";
|
|
|
490
1335
|
* Report what the React Compiler said about one module.
|
|
491
1336
|
*
|
|
492
1337
|
* Every finding used to be printed as `a function: <message>` — no file, no
|
|
493
|
-
* line, no column, and the fallback string doing all the work because
|
|
494
|
-
*
|
|
1338
|
+
* line, no column, and the fallback string doing all the work, because
|
|
1339
|
+
* `diagnostic.function` was read out of a field only a *success* event carries
|
|
1340
|
+
* and so was null for every finding there has ever been (#371), not because
|
|
1341
|
+
* the compiler was reporting on anonymous inner functions. The transform hook
|
|
495
1342
|
* knows the module and the compiler gives a position for most findings, so
|
|
496
1343
|
* both go into the message: Vite prints a plugin warning's `message` and
|
|
497
1344
|
* nothing else, so a location that is not in the string is a location the
|
|
498
1345
|
* reader never sees. `id` and `loc` go along for anything reading the log
|
|
499
1346
|
* object rather than the line. See ubugeeei-prod/uf#307.
|
|
500
1347
|
*
|
|
1348
|
+
* `(in Form)` is real now: `uf transform` recovers the name from the tree it
|
|
1349
|
+
* compiled, and leaves it off the diagnostic when the function genuinely has
|
|
1350
|
+
* no name to give.
|
|
1351
|
+
*
|
|
1352
|
+
* The position is real too, and is the expression the compiler objected to
|
|
1353
|
+
* rather than the function containing it, so the two halves of the line say
|
|
1354
|
+
* different things — `packages/hooks/dom.js:195:7: This value cannot be
|
|
1355
|
+
* modified (in useLongPress)` locates the write and names the hook to look in.
|
|
1356
|
+
*
|
|
501
1357
|
* A dependency's findings are held back. A React Compiler bailout inside
|
|
502
1358
|
* `@uniflowed/form` is not something the person running the build can fix, and
|
|
503
1359
|
* a channel carrying forty of them on every build is a channel people stop
|
|
@@ -520,9 +1376,20 @@ function reportDiagnostics(context, { id, root, diagnostics, environment, report
|
|
|
520
1376
|
const mine = isProjectModule(root, id) || process.env[ALL_DIAGNOSTICS] === "all";
|
|
521
1377
|
for (const diagnostic of diagnostics) {
|
|
522
1378
|
// Everything a reader would be shown, so two findings that would print as
|
|
523
|
-
// the same line collapse into one.
|
|
524
|
-
//
|
|
525
|
-
//
|
|
1379
|
+
// the same line collapse into one.
|
|
1380
|
+
//
|
|
1381
|
+
// This used to collapse far more than that, and the note here used to say
|
|
1382
|
+
// the compiler reports "Cannot access refs during render" once per pass
|
|
1383
|
+
// that noticed it — three times for one `ref`. That was the wrong reading
|
|
1384
|
+
// of the evidence. The position in a finding was the position of the
|
|
1385
|
+
// *function* it was found in, so every finding in one function shared a
|
|
1386
|
+
// line and a column and any two with the same message were, to this
|
|
1387
|
+
// signature, the same finding. They were not: over this repository's own
|
|
1388
|
+
// packages it collapsed 173 findings into 119 lines, and the five that
|
|
1389
|
+
// became one line in `DatePickerInput` are five different reads of a ref
|
|
1390
|
+
// on five different lines. `uf transform` now reports where the compiler
|
|
1391
|
+
// actually objected, so the signature separates them, and what it still
|
|
1392
|
+
// collapses is a genuine repeat of one site.
|
|
526
1393
|
const signature = `${diagnostic.kind}\0${diagnostic.line}\0${diagnostic.column}\0${diagnostic.message}`;
|
|
527
1394
|
if (ledger.signatures.has(signature)) continue;
|
|
528
1395
|
ledger.signatures.add(signature);
|
|
@@ -615,24 +1482,3 @@ function cleanId(id) {
|
|
|
615
1482
|
const at = id.indexOf("?");
|
|
616
1483
|
return at === -1 ? id : id.slice(0, at);
|
|
617
1484
|
}
|
|
618
|
-
|
|
619
|
-
/**
|
|
620
|
-
* Every `@uniflowed/*` package the project can resolve, for
|
|
621
|
-
* `optimizeDeps.exclude`, which takes names rather than patterns.
|
|
622
|
-
*/
|
|
623
|
-
function uniflowedPackages(root) {
|
|
624
|
-
const names = new Set();
|
|
625
|
-
let directory = root;
|
|
626
|
-
for (let depth = 0; depth < 16; depth += 1) {
|
|
627
|
-
const scope = path.join(directory, "node_modules", "@uniflowed");
|
|
628
|
-
try {
|
|
629
|
-
for (const entry of readdirSync(scope)) names.add(`@uniflowed/${entry}`);
|
|
630
|
-
} catch {
|
|
631
|
-
// no packages at this level
|
|
632
|
-
}
|
|
633
|
-
const parent = path.dirname(directory);
|
|
634
|
-
if (parent === directory) break;
|
|
635
|
-
directory = parent;
|
|
636
|
-
}
|
|
637
|
-
return [...names].sort();
|
|
638
|
-
}
|