@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/index.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// @noflow
|
|
2
|
+
//
|
|
1
3
|
// Plain JavaScript: Vite imports this module directly, before any transform.
|
|
2
4
|
//
|
|
3
5
|
// `@uniflowed/vite` — uf, as Vite plugins.
|
|
@@ -14,9 +16,21 @@
|
|
|
14
16
|
// that hydrates it, and the server entry that renders it. In
|
|
15
17
|
// development it also renders every HTML request on the
|
|
16
18
|
// server, so `uf dev` serves the same markup `uf build` writes.
|
|
19
|
+
// The client's copy of the route table is not the server's:
|
|
20
|
+
// `internal/rsc.js` reads the RSC analysis and leaves out the
|
|
21
|
+
// page of every route no client boundary reaches, so that
|
|
22
|
+
// route's modules never enter the browser bundle.
|
|
17
23
|
// * `uf:mdx` — `@mdx-js/rollup`, configured for React with GitHub-flavoured
|
|
18
|
-
// markdown, front matter
|
|
24
|
+
// markdown, front matter, heading ids and build-time syntax
|
|
25
|
+
// highlighting, so `.mdx` works with
|
|
19
26
|
// no configuration.
|
|
27
|
+
// * `uf:asset` — an imported image is decoded, resized to the widths the
|
|
28
|
+
// project declares and re-encoded by `uf assets`, and an
|
|
29
|
+
// imported font is self-hosted with the `@font-face` and the
|
|
30
|
+
// metric-matched fallback that stop the swap moving the page.
|
|
31
|
+
// The import evaluates to what `Image` and `Font` need — the
|
|
32
|
+
// intrinsic size, every emitted variant, the placeholder —
|
|
33
|
+
// rather than to a URL string. See `internal/assets.js`.
|
|
20
34
|
//
|
|
21
35
|
// `uniflowed(options)` returns the array; a project that wants to add a plugin
|
|
22
36
|
// declares it in `uf.config.js` and the driver appends it after these.
|
|
@@ -26,6 +40,10 @@ import path from "node:path";
|
|
|
26
40
|
|
|
27
41
|
import mdx from "@mdx-js/rollup";
|
|
28
42
|
import rehypeSlug from "rehype-slug";
|
|
43
|
+
|
|
44
|
+
import { assetPlugin } from "./internal/assets.js";
|
|
45
|
+
import { emit, reportRenderError } from "./internal/events.js";
|
|
46
|
+
import { highlightPlugin } from "./internal/highlight.js";
|
|
29
47
|
import remarkFrontmatter from "remark-frontmatter";
|
|
30
48
|
import remarkGfm from "remark-gfm";
|
|
31
49
|
import remarkMdxFrontmatter from "remark-mdx-frontmatter";
|
|
@@ -37,19 +55,33 @@ import {
|
|
|
37
55
|
preambleCode,
|
|
38
56
|
refreshRuntimeSource,
|
|
39
57
|
} from "./internal/refresh.js";
|
|
58
|
+
import { RSC_MANIFEST_ENV, clientRouteFilter, readRscManifest } from "./internal/rsc.js";
|
|
40
59
|
import {
|
|
60
|
+
RESERVED,
|
|
41
61
|
VIRTUAL,
|
|
42
62
|
clientModuleSource,
|
|
43
63
|
routesModuleSource,
|
|
44
64
|
scanRoutes,
|
|
45
65
|
serverModuleSource,
|
|
46
66
|
} from "./internal/routes.js";
|
|
47
|
-
import { TransformService, isFlowModule } from "
|
|
67
|
+
import { TransformService, isFlowModule } from "@uniflowed/host/transform";
|
|
68
|
+
import { send, toRequest } from "./internal/http.js";
|
|
69
|
+
import { withRequest } from "./internal/serve.js";
|
|
48
70
|
|
|
49
71
|
/** A resolved virtual id: Vite's convention is a leading NUL byte. */
|
|
50
72
|
const resolved = (id) => `\0${id}`;
|
|
51
73
|
const VIRTUAL_IDS = new Set(Object.values(VIRTUAL));
|
|
52
74
|
|
|
75
|
+
/**
|
|
76
|
+
* Prefix of the virtual module that carries one source module's StyleX rules.
|
|
77
|
+
*
|
|
78
|
+
* Not NUL-prefixed, unlike the virtual modules above: Vite's CSS pipeline keys
|
|
79
|
+
* off the `.css` extension of a *resolvable* id, and a NUL-prefixed id is
|
|
80
|
+
* excluded from it. The prefix is distinctive enough that nothing else can
|
|
81
|
+
* collide with it.
|
|
82
|
+
*/
|
|
83
|
+
const STYLE_PREFIX = "uf-style:";
|
|
84
|
+
|
|
53
85
|
/** The URL a NUL-prefixed module is served at in development. */
|
|
54
86
|
export function devUrlFor(id) {
|
|
55
87
|
return `/@id/__x00__${id}`;
|
|
@@ -75,8 +107,17 @@ export default function uniflowed(options = {}) {
|
|
|
75
107
|
const routerRoot = app.router?.root ?? "app";
|
|
76
108
|
const appEntry = app.router?.entry ?? ufConfig.build?.entries?.[0] ?? "app.js";
|
|
77
109
|
const markdown = app.builtins?.markdown ?? {};
|
|
110
|
+
const builtins = app.builtins ?? {};
|
|
78
111
|
|
|
79
|
-
return [
|
|
112
|
+
return [
|
|
113
|
+
flowPlugin({ routerRoot, appEntry, command: options.command }),
|
|
114
|
+
mdxPlugin(markdown),
|
|
115
|
+
assetPlugin({
|
|
116
|
+
images: builtins.images ?? {},
|
|
117
|
+
fonts: builtins.fonts ?? {},
|
|
118
|
+
command: options.command,
|
|
119
|
+
}),
|
|
120
|
+
];
|
|
80
121
|
}
|
|
81
122
|
|
|
82
123
|
function flowPlugin({ routerRoot, appEntry, command }) {
|
|
@@ -89,12 +130,64 @@ function flowPlugin({ routerRoot, appEntry, command }) {
|
|
|
89
130
|
let server = null;
|
|
90
131
|
/** @type {TransformService | null} */
|
|
91
132
|
let service = null;
|
|
133
|
+
/**
|
|
134
|
+
* Each module's compiled stylesheet, keyed by the virtual id serving it.
|
|
135
|
+
*
|
|
136
|
+
* A map rather than one accumulated sheet: Vite asks for a module's CSS when
|
|
137
|
+
* it loads that module, re-asks when the module changes, and drops it when
|
|
138
|
+
* the module goes away. One shared sheet would have to be invalidated by
|
|
139
|
+
* hand, which is the part that goes wrong.
|
|
140
|
+
*/
|
|
141
|
+
const styles = new Map();
|
|
142
|
+
/**
|
|
143
|
+
* The React Compiler findings already reported, so each is said once.
|
|
144
|
+
*
|
|
145
|
+
* `uf build` runs Vite twice — once for the browser bundle and once for the
|
|
146
|
+
* server one — over the same modules, so every finding was made twice and
|
|
147
|
+
* printed twice. An entry records which environment reported a module's
|
|
148
|
+
* findings first: a re-transform in *that* environment (a dev server, after
|
|
149
|
+
* an edit) clears it and reports again, and the other environment's pass over
|
|
150
|
+
* the same module stays quiet. Keying on the environment rather than on a
|
|
151
|
+
* flag is what keeps the second half true without making the first half
|
|
152
|
+
* false.
|
|
153
|
+
*
|
|
154
|
+
* @type {Map<string, { environment: string, signatures: Set<string> }>}
|
|
155
|
+
*/
|
|
156
|
+
const reported = new Map();
|
|
157
|
+
/** Findings held back as a dependency's, waiting to be counted out loud. */
|
|
158
|
+
let suppressed = [];
|
|
92
159
|
|
|
93
160
|
const ensureService = () => {
|
|
94
161
|
service ??= new TransformService({ command, root });
|
|
95
162
|
return service;
|
|
96
163
|
};
|
|
97
164
|
|
|
165
|
+
/**
|
|
166
|
+
* The browser's copy of the route table.
|
|
167
|
+
*
|
|
168
|
+
* The manifest is read here rather than once at start-up because `uf dev`
|
|
169
|
+
* rewrites it whenever the graph moves, and this hook runs again when it
|
|
170
|
+
* does — a table built from a manifest read at start-up would be the answer
|
|
171
|
+
* for the project as it was when the server started.
|
|
172
|
+
*
|
|
173
|
+
* The count is emitted rather than computed on the Rust side, and that is
|
|
174
|
+
* the point of it: `uf build` prints what the table it just generated
|
|
175
|
+
* contains, not what a second implementation of this decision predicted it
|
|
176
|
+
* would. Only for a build — a dev server has no summary to be true in.
|
|
177
|
+
*/
|
|
178
|
+
const clientRoutesModule = (table) => {
|
|
179
|
+
const shipsPage = clientRouteFilter(
|
|
180
|
+
readRscManifest(process.env[RSC_MANIFEST_ENV]),
|
|
181
|
+
root,
|
|
182
|
+
table,
|
|
183
|
+
);
|
|
184
|
+
const kept = new Set(table.routes.filter(shipsPage));
|
|
185
|
+
if (server == null) {
|
|
186
|
+
emit("rsc-split", { pages: kept.size, routes: table.routes.length });
|
|
187
|
+
}
|
|
188
|
+
return routesModuleSource(table, { shipsPage: (route) => kept.has(route) });
|
|
189
|
+
};
|
|
190
|
+
|
|
98
191
|
return {
|
|
99
192
|
name: "uf:flow",
|
|
100
193
|
enforce: "pre",
|
|
@@ -144,14 +237,26 @@ function flowPlugin({ routerRoot, appEntry, command }) {
|
|
|
144
237
|
resolveId(id) {
|
|
145
238
|
if (id === RUNTIME_PUBLIC_PATH) return RUNTIME_RESOLVED_ID;
|
|
146
239
|
if (VIRTUAL_IDS.has(id)) return resolved(id);
|
|
240
|
+
// A module's own stylesheet, which `transform` below asked for by
|
|
241
|
+
// importing this id. Returning it unchanged marks it resolved without
|
|
242
|
+
// Vite going to the filesystem for a file that does not exist.
|
|
243
|
+
if (id.startsWith(STYLE_PREFIX)) return id;
|
|
147
244
|
return null;
|
|
148
245
|
},
|
|
149
246
|
|
|
150
|
-
load(id) {
|
|
247
|
+
load(id, loadOptions) {
|
|
151
248
|
if (id === RUNTIME_RESOLVED_ID) return refreshRuntimeSource();
|
|
152
|
-
if (id === resolved(VIRTUAL.routes))
|
|
249
|
+
if (id === resolved(VIRTUAL.routes)) {
|
|
250
|
+
const table = scanRoutes(appRoot);
|
|
251
|
+
// The server renders every route, so the server's table is the whole
|
|
252
|
+
// one and is generated with no filter at all. Only the browser's copy
|
|
253
|
+
// is split.
|
|
254
|
+
if (isSsr(this, loadOptions)) return routesModuleSource(table);
|
|
255
|
+
return clientRoutesModule(table);
|
|
256
|
+
}
|
|
153
257
|
if (id === resolved(VIRTUAL.client)) return clientModuleSource(entryPath);
|
|
154
258
|
if (id === resolved(VIRTUAL.server)) return serverModuleSource(entryPath);
|
|
259
|
+
if (id.startsWith(STYLE_PREFIX)) return styles.get(id) ?? "";
|
|
155
260
|
return null;
|
|
156
261
|
},
|
|
157
262
|
|
|
@@ -165,16 +270,52 @@ function flowPlugin({ routerRoot, appEntry, command }) {
|
|
|
165
270
|
sourceMap: true,
|
|
166
271
|
});
|
|
167
272
|
if (out == null) return null;
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
273
|
+
reportDiagnostics(this, {
|
|
274
|
+
id: cleanId(id),
|
|
275
|
+
root,
|
|
276
|
+
diagnostics: out.diagnostics,
|
|
277
|
+
environment: ssr ? "ssr" : "client",
|
|
278
|
+
reported,
|
|
279
|
+
suppressed,
|
|
280
|
+
});
|
|
171
281
|
const map = out.map == null ? null : JSON.parse(out.map);
|
|
172
|
-
|
|
282
|
+
// StyleX. `uf transform` compiled the module's `stylex.create` calls into
|
|
283
|
+
// class names and handed back the rules they declared; the rules become a
|
|
284
|
+
// module of their own that this one imports.
|
|
285
|
+
//
|
|
286
|
+
// Handing the CSS to Vite as a module, rather than collecting it here and
|
|
287
|
+
// writing a stylesheet at the end, is what keeps uf out of the CSS
|
|
288
|
+
// business: Vite already injects a stylesheet in dev, extracts it in a
|
|
289
|
+
// build, code-splits it per chunk, and replaces it over HMR. A module
|
|
290
|
+
// whose styles are gone stops importing it, and Vite notices.
|
|
291
|
+
const styled = out.css != null && out.css !== "";
|
|
292
|
+
let output = out.code;
|
|
293
|
+
if (styled) {
|
|
294
|
+
const styleId = `${STYLE_PREFIX}${cleanId(id)}.css`;
|
|
295
|
+
styles.set(styleId, out.css);
|
|
296
|
+
output = `import ${JSON.stringify(styleId)};\n${output}`;
|
|
297
|
+
}
|
|
298
|
+
// A module that compiled a stylesheet has a side effect, whatever its
|
|
299
|
+
// package says. `@uniflowed/stylex` declares `sideEffects: false` and is
|
|
300
|
+
// right about its source: `tokens.stylex.js` only exports a token set.
|
|
301
|
+
// What it exports after this transform is a token set *and* a `:root`
|
|
302
|
+
// block, and the page that imports `ufTokens` no longer names it at
|
|
303
|
+
// runtime — the compiler turned every read into the `var(--…)` it minted.
|
|
304
|
+
// So the import was unused, a side-effect-free module with no used
|
|
305
|
+
// exports was dropped, and the custom properties every one of those
|
|
306
|
+
// `var()`s resolves against went with it: rules that referred to nothing.
|
|
307
|
+
// Declaring the side effect here rather than editing the package is
|
|
308
|
+
// deliberate — the side effect is one this plugin added, so it is this
|
|
309
|
+
// plugin's to admit to. See ubugeeei-prod/uf#306.
|
|
310
|
+
const moduleSideEffects = styled ? true : undefined;
|
|
311
|
+
if (!refresh) return { code: output, map, moduleSideEffects };
|
|
173
312
|
const relative = path.relative(root, cleanId(id)).split(path.sep).join("/");
|
|
174
|
-
return addRefreshWrapper(
|
|
313
|
+
return { ...addRefreshWrapper(output, map, relative), moduleSideEffects };
|
|
175
314
|
},
|
|
176
315
|
|
|
177
316
|
buildEnd() {
|
|
317
|
+
summariseSuppressed(this, suppressed);
|
|
318
|
+
suppressed = [];
|
|
178
319
|
// A dev server keeps its service for the whole session; a build is
|
|
179
320
|
// done with it here.
|
|
180
321
|
if (server == null) {
|
|
@@ -202,9 +343,19 @@ function flowPlugin({ routerRoot, appEntry, command }) {
|
|
|
202
343
|
service = null;
|
|
203
344
|
});
|
|
204
345
|
|
|
205
|
-
// A
|
|
346
|
+
// A reserved file appearing or disappearing changes the route table,
|
|
206
347
|
// which lives in a virtual module the watcher knows nothing about.
|
|
207
|
-
|
|
348
|
+
//
|
|
349
|
+
// Built from `RESERVED` rather than written out. It used to be the
|
|
350
|
+
// literal `(page|layout|middleware|not-found)`, which is a fourth
|
|
351
|
+
// spelling of a grammar that already has three, and it was already
|
|
352
|
+
// missing `route` — so adding a route handler to a running dev server
|
|
353
|
+
// did not rebuild the table and the handler stayed invisible until a
|
|
354
|
+
// restart. A list that has to match another list has to be that list.
|
|
355
|
+
const stems = Object.values(RESERVED)
|
|
356
|
+
.map((stem) => stem.replaceAll(".", "\\."))
|
|
357
|
+
.join("|");
|
|
358
|
+
const reserved = new RegExp(`/(${stems})(\\.[a-z]+)?\\.(js|jsx|mdx)$`);
|
|
208
359
|
const onRouteFile = (file) => {
|
|
209
360
|
if (!reserved.test(file) || !file.startsWith(appRoot)) return;
|
|
210
361
|
const routes = devServer.moduleGraph.getModuleById(resolved(VIRTUAL.routes));
|
|
@@ -214,6 +365,27 @@ function flowPlugin({ routerRoot, appEntry, command }) {
|
|
|
214
365
|
devServer.watcher.on("add", onRouteFile);
|
|
215
366
|
devServer.watcher.on("unlink", onRouteFile);
|
|
216
367
|
|
|
368
|
+
// The same problem one level up. Adding `"use client"` to a module, or
|
|
369
|
+
// deleting the import that reached it, changes which routes the browser
|
|
370
|
+
// is given a page for — and touches no reserved file name, so nothing
|
|
371
|
+
// above notices. `uf dev` rewrites the RSC manifest when the analysis
|
|
372
|
+
// moves and only then, so this fires when the answer changed rather than
|
|
373
|
+
// on every keystroke. Watched explicitly because the file is uf's own
|
|
374
|
+
// artefact and is in no module graph.
|
|
375
|
+
const manifestFile = process.env[RSC_MANIFEST_ENV];
|
|
376
|
+
if (manifestFile != null && manifestFile !== "") {
|
|
377
|
+
const manifestPath = path.resolve(manifestFile);
|
|
378
|
+
devServer.watcher.add(manifestPath);
|
|
379
|
+
const onManifest = (file) => {
|
|
380
|
+
if (path.resolve(file) !== manifestPath) return;
|
|
381
|
+
const routes = devServer.moduleGraph.getModuleById(resolved(VIRTUAL.routes));
|
|
382
|
+
if (routes) devServer.moduleGraph.invalidateModule(routes);
|
|
383
|
+
devServer.ws.send({ type: "full-reload", path: "*" });
|
|
384
|
+
};
|
|
385
|
+
devServer.watcher.on("add", onManifest);
|
|
386
|
+
devServer.watcher.on("change", onManifest);
|
|
387
|
+
}
|
|
388
|
+
|
|
217
389
|
// After Vite's own middlewares, so `/@vite/client`, `/@id/...` and
|
|
218
390
|
// static files are served first and only a document request reaches
|
|
219
391
|
// the renderer.
|
|
@@ -222,19 +394,66 @@ function flowPlugin({ routerRoot, appEntry, command }) {
|
|
|
222
394
|
if (!wantsDocument(request)) return next();
|
|
223
395
|
try {
|
|
224
396
|
const url = request.url ?? "/";
|
|
225
|
-
const
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
397
|
+
const entry = await importServerEntry(devServer);
|
|
398
|
+
const asRequest = await toRequest(request, devServer.config);
|
|
399
|
+
|
|
400
|
+
// One request, owned here and settled once the document has been
|
|
401
|
+
// written — the same lifecycle `driver.js` gives `uf dev` and
|
|
402
|
+
// `internal/serve.js` gives `uf preview` and `uf start`. A project
|
|
403
|
+
// driving Vite itself must not get a different answer about when
|
|
404
|
+
// `after()` runs than the same project run through `uf dev`; see
|
|
405
|
+
// `internal/serve.js` and ubugeeei-prod/uf#389.
|
|
406
|
+
//
|
|
407
|
+
// Only requests that look like a document reach here, so unlike
|
|
408
|
+
// `driver.js` there is no path where uf hands the response back to
|
|
409
|
+
// Vite's chain: what is below either writes it or throws.
|
|
410
|
+
await withRequest(entry, asRequest, async () => {
|
|
411
|
+
// Before anything answers: a middleware guards a subtree, and a
|
|
412
|
+
// page rendered while the guard on it had not run is the whole of
|
|
413
|
+
// ubugeeei-prod/uf#260. `driver.js` makes the same call, for
|
|
414
|
+
// every method.
|
|
415
|
+
const guarded = await entry.runMiddleware(asRequest);
|
|
416
|
+
if (guarded != null) {
|
|
417
|
+
await send(response, guarded);
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Then the route handlers, above the renderer and for the same
|
|
422
|
+
// reason `driver.js` puts them there: a path that answers a
|
|
423
|
+
// request is not a document, whatever the client said it would
|
|
424
|
+
// accept. `curl /api/thing` and a `<form action>` navigation both
|
|
425
|
+
// send `Accept: text/html`, and both want the handler's answer.
|
|
426
|
+
//
|
|
427
|
+
// This step is not a duplicate of the dispatcher in `driver.js`,
|
|
428
|
+
// it is the only one that can run: this middleware is mounted by
|
|
429
|
+
// `configureServer`, which Vite calls while it is building the
|
|
430
|
+
// server, and `uf dev` adds its own after `createServer` has
|
|
431
|
+
// returned — so for every request this one claims, it is the one
|
|
432
|
+
// that decides. Without it a route handler under `uf dev` was
|
|
433
|
+
// reachable only by a client that asked for something other than
|
|
434
|
+
// HTML, and answered the 404 page to everyone else.
|
|
435
|
+
const handled = await entry.dispatch(asRequest);
|
|
436
|
+
if (handled != null) {
|
|
437
|
+
await send(response, handled);
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const result = await entry.render(
|
|
442
|
+
url,
|
|
443
|
+
{ scripts: [devUrlFor(VIRTUAL.client)], styles: [], preloads: [] },
|
|
444
|
+
{ onError: (error) => reportRenderError(devServer, url, error) },
|
|
445
|
+
);
|
|
446
|
+
if (result.error != null) reportRenderError(devServer, url, result.error);
|
|
447
|
+
// Collected rather than piped, for the reason `driver.js` gives at
|
|
448
|
+
// step 4: `transformIndexHtml` is a whole-document hook.
|
|
449
|
+
const html = await devServer.transformIndexHtml(url, await result.text());
|
|
450
|
+
response.statusCode = result.status;
|
|
451
|
+
response.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
452
|
+
for (const [name, value] of Object.entries(result.headers ?? {})) {
|
|
453
|
+
response.setHeader(name, value);
|
|
454
|
+
}
|
|
455
|
+
response.end(html);
|
|
230
456
|
});
|
|
231
|
-
const html = await devServer.transformIndexHtml(url, result.html);
|
|
232
|
-
response.statusCode = result.status;
|
|
233
|
-
response.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
234
|
-
for (const [name, value] of Object.entries(result.headers ?? {})) {
|
|
235
|
-
response.setHeader(name, value);
|
|
236
|
-
}
|
|
237
|
-
response.end(html);
|
|
238
457
|
} catch (error) {
|
|
239
458
|
devServer.ssrFixStacktrace(error);
|
|
240
459
|
next(error);
|
|
@@ -248,12 +467,23 @@ function flowPlugin({ routerRoot, appEntry, command }) {
|
|
|
248
467
|
function mdxPlugin(markdown) {
|
|
249
468
|
const mdxConfig = markdown.mdx ?? {};
|
|
250
469
|
if (mdxConfig.enabled === false) return { name: "uf:mdx" };
|
|
470
|
+
|
|
471
|
+
// Highlighting is on unless a project turns it off, and it happens here
|
|
472
|
+
// rather than in the browser: the colours are in the HTML, so a code sample
|
|
473
|
+
// is readable before any JavaScript loads and no highlighter is shipped.
|
|
474
|
+
const highlight = highlightPlugin(mdxConfig.highlight);
|
|
475
|
+
const rehypePlugins = highlight == null ? [rehypeSlug] : [rehypeSlug, highlight];
|
|
476
|
+
|
|
251
477
|
return {
|
|
252
478
|
enforce: "pre",
|
|
253
479
|
...mdx({
|
|
254
480
|
jsxImportSource: "react",
|
|
255
|
-
remarkPlugins: [
|
|
256
|
-
|
|
481
|
+
remarkPlugins: [
|
|
482
|
+
remarkGfm,
|
|
483
|
+
remarkFrontmatter,
|
|
484
|
+
[remarkMdxFrontmatter, { name: "frontmatter" }],
|
|
485
|
+
],
|
|
486
|
+
rehypePlugins,
|
|
257
487
|
}),
|
|
258
488
|
name: "uf:mdx",
|
|
259
489
|
};
|
|
@@ -289,6 +519,134 @@ function wantsDocument(request) {
|
|
|
289
519
|
return !/\.[a-z0-9]+$/i.test(pathname);
|
|
290
520
|
}
|
|
291
521
|
|
|
522
|
+
/** The name of the environment variable that turns every finding back on. */
|
|
523
|
+
const ALL_DIAGNOSTICS = "UF_REACT_COMPILER_DIAGNOSTICS";
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Report what the React Compiler said about one module.
|
|
527
|
+
*
|
|
528
|
+
* Every finding used to be printed as `a function: <message>` — no file, no
|
|
529
|
+
* line, no column, and the fallback string doing all the work because the
|
|
530
|
+
* compiler names an inner function about as often as not. The transform hook
|
|
531
|
+
* knows the module and the compiler gives a position for most findings, so
|
|
532
|
+
* both go into the message: Vite prints a plugin warning's `message` and
|
|
533
|
+
* nothing else, so a location that is not in the string is a location the
|
|
534
|
+
* reader never sees. `id` and `loc` go along for anything reading the log
|
|
535
|
+
* object rather than the line. See ubugeeei-prod/uf#307.
|
|
536
|
+
*
|
|
537
|
+
* A dependency's findings are held back. A React Compiler bailout inside
|
|
538
|
+
* `@uniflowed/form` is not something the person running the build can fix, and
|
|
539
|
+
* a channel carrying forty of them on every build is a channel people stop
|
|
540
|
+
* reading — which costs them the one finding that was theirs. They are counted
|
|
541
|
+
* and said once instead, and `UF_REACT_COMPILER_DIAGNOSTICS=all` prints every
|
|
542
|
+
* one for whoever is fixing the dependency.
|
|
543
|
+
*/
|
|
544
|
+
function reportDiagnostics(context, { id, root, diagnostics, environment, reported, suppressed }) {
|
|
545
|
+
if (diagnostics.length === 0) return;
|
|
546
|
+
// A module transformed again by the environment that first reported it has
|
|
547
|
+
// been edited; anything else is the second bundle passing over the same file.
|
|
548
|
+
const previous = reported.get(id);
|
|
549
|
+
const ledger =
|
|
550
|
+
previous != null && previous.environment !== environment
|
|
551
|
+
? previous
|
|
552
|
+
: { environment, signatures: new Set() };
|
|
553
|
+
reported.set(id, ledger);
|
|
554
|
+
|
|
555
|
+
const file = relativeId(root, id);
|
|
556
|
+
const mine = isProjectModule(root, id) || process.env[ALL_DIAGNOSTICS] === "all";
|
|
557
|
+
for (const diagnostic of diagnostics) {
|
|
558
|
+
// Everything a reader would be shown, so two findings that would print as
|
|
559
|
+
// the same line collapse into one. The compiler reports "Cannot access refs
|
|
560
|
+
// during render" once per pass that noticed it — three times for one `ref`
|
|
561
|
+
// — and three identical lines are not three things to fix.
|
|
562
|
+
const signature = `${diagnostic.kind}\0${diagnostic.line}\0${diagnostic.column}\0${diagnostic.message}`;
|
|
563
|
+
if (ledger.signatures.has(signature)) continue;
|
|
564
|
+
ledger.signatures.add(signature);
|
|
565
|
+
if (!mine) {
|
|
566
|
+
suppressed.push(file);
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
// Two conventions, both honoured. uf's own frames count columns from one
|
|
570
|
+
// (`uf_term::diagnostic`) and so does every editor a reader will paste
|
|
571
|
+
// `file:line:column` into; Rollup's `loc.column` counts from zero, which is
|
|
572
|
+
// what the compiler already gave us. The string gets the reader's number
|
|
573
|
+
// and the log object gets Rollup's.
|
|
574
|
+
const at = diagnostic.line == null ? "" : `:${diagnostic.line}:${(diagnostic.column ?? 0) + 1}`;
|
|
575
|
+
const who = diagnostic.function == null ? "" : ` (in ${diagnostic.function})`;
|
|
576
|
+
context.warn?.({
|
|
577
|
+
message: `${file}${at}: ${diagnostic.message}${who}`,
|
|
578
|
+
id,
|
|
579
|
+
loc:
|
|
580
|
+
diagnostic.line == null
|
|
581
|
+
? undefined
|
|
582
|
+
: { file: id, line: diagnostic.line, column: diagnostic.column ?? 0 },
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Say how many findings were a dependency's, and whose.
|
|
589
|
+
*
|
|
590
|
+
* Held back is not the same as hidden: a build that quietly drops forty
|
|
591
|
+
* findings is a build that has decided for the reader that uf has no bugs. One
|
|
592
|
+
* line names the packages and how to see the rest.
|
|
593
|
+
*/
|
|
594
|
+
function summariseSuppressed(context, suppressed) {
|
|
595
|
+
if (suppressed.length === 0) return;
|
|
596
|
+
const packages = [...new Set(suppressed.map(packageOf))].sort();
|
|
597
|
+
const count = suppressed.length;
|
|
598
|
+
context.warn?.(
|
|
599
|
+
`${count} React Compiler ${count === 1 ? "finding" : "findings"} in ` +
|
|
600
|
+
`${packages.join(", ")} — not this application's to fix; ` +
|
|
601
|
+
`set ${ALL_DIAGNOSTICS}=all to see them`,
|
|
602
|
+
);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* Whether a finding about this module is the application author's to act on.
|
|
607
|
+
*
|
|
608
|
+
* Inside the project root *and* outside `node_modules`, rather than
|
|
609
|
+
* `node_modules` alone: uf's own packages reach an application through a
|
|
610
|
+
* workspace link in this repository and through `node_modules` everywhere
|
|
611
|
+
* else, and they are no more the reader's code in one case than the other.
|
|
612
|
+
*/
|
|
613
|
+
function isProjectModule(root, id) {
|
|
614
|
+
const relative = path.relative(root, id);
|
|
615
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) return false;
|
|
616
|
+
return !relative.split(path.sep).includes("node_modules");
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/** A module's path as a reader would write it: relative, with forward slashes. */
|
|
620
|
+
function relativeId(root, id) {
|
|
621
|
+
const relative = path.relative(root, id);
|
|
622
|
+
return relative === "" ? id : relative.split(path.sep).join("/");
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/** The package a module belongs to, for the one line that names them. */
|
|
626
|
+
function packageOf(file) {
|
|
627
|
+
const parts = file.split("/");
|
|
628
|
+
const at = parts.lastIndexOf("node_modules");
|
|
629
|
+
if (at !== -1) {
|
|
630
|
+
const scoped = parts[at + 1]?.startsWith("@");
|
|
631
|
+
return parts.slice(at + 1, at + (scoped ? 3 : 2)).join("/");
|
|
632
|
+
}
|
|
633
|
+
// No `node_modules` in the path: a workspace link, resolved to a checkout.
|
|
634
|
+
// The directory the module hangs off is the closest thing to a package name
|
|
635
|
+
// that is true without reading its `package.json` from a warning path.
|
|
636
|
+
const up = parts.lastIndexOf("packages");
|
|
637
|
+
return up === -1 ? parts.slice(0, -1).join("/") || file : parts.slice(up, up + 2).join("/");
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* Whether a hook is running for the server environment.
|
|
642
|
+
*
|
|
643
|
+
* Both spellings, for the reason `transform` above checks both: Vite 6 moved
|
|
644
|
+
* the answer onto the plugin context and the `ssr` option is the older one.
|
|
645
|
+
*/
|
|
646
|
+
function isSsr(context, options) {
|
|
647
|
+
return options?.ssr === true || context?.environment?.name === "ssr";
|
|
648
|
+
}
|
|
649
|
+
|
|
292
650
|
function cleanId(id) {
|
|
293
651
|
const at = id.indexOf("?");
|
|
294
652
|
return at === -1 ? id : id.slice(0, at);
|