@uniflowed/vite 0.0.0-alpha.34 → 0.0.0-alpha.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js CHANGED
@@ -28,6 +28,11 @@
28
28
  // markdown, front matter, heading ids and build-time syntax
29
29
  // highlighting, so `.mdx` works with
30
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`.
31
36
  // * `uf:asset` — an imported image is decoded, resized to the widths the
32
37
  // project declares and re-encoded by `uf assets`, and an
33
38
  // imported font is self-hosted with the `@font-face` and the
@@ -53,11 +58,13 @@ import {
53
58
  auditTag,
54
59
  } from "./internal/a11y.js";
55
60
  import { assetPlugin } from "./internal/assets.js";
61
+ import { barrelImportsPlugin, namespaceViewOf } from "./internal/barrel-imports.js";
56
62
  import { emit, reportRenderError } from "./internal/events.js";
63
+ import remarkFrontmatterExport from "./internal/frontmatter.js";
57
64
  import { highlightPlugin } from "./internal/highlight.js";
65
+ import { moduleId } from "./internal/module-graph.js";
58
66
  import remarkFrontmatter from "remark-frontmatter";
59
67
  import remarkGfm from "remark-gfm";
60
- import remarkMdxFrontmatter from "remark-mdx-frontmatter";
61
68
 
62
69
  import {
63
70
  RUNTIME_PUBLIC_PATH,
@@ -82,18 +89,49 @@ import {
82
89
  clientModuleSource,
83
90
  resolveRouteTarget,
84
91
  routesModuleSource,
92
+ routingRulesOf,
85
93
  scanRoutes,
86
94
  serverModuleSource,
87
95
  } from "./internal/routes.js";
88
96
  import { TransformService, isFlowModule } from "@uniflowed/host/transform";
97
+ import {
98
+ DEV_RSC_HOOK,
99
+ FLIGHT_BROWSER_DEPENDENCIES,
100
+ FLIGHT_VIRTUAL,
101
+ RSC_ENVIRONMENT,
102
+ builtBridgeSource,
103
+ builtReferencesSource,
104
+ clientManifestSource,
105
+ clientModuleUrlPlugin,
106
+ clientReferencePlugin,
107
+ compilerRuntimeSource,
108
+ createFlightState,
109
+ devBridgeSource,
110
+ devReferencesSource,
111
+ devStylesheets,
112
+ flightClientSource,
113
+ flightDocumentPath,
114
+ flightServerSource,
115
+ linkStylesheets,
116
+ rendersFlight,
117
+ rscEntrySource,
118
+ rscEnvironment,
119
+ } from "./internal/flight.js";
89
120
  import { createChannelMiddleware } from "./internal/diagnostics.js";
90
121
  import { devtoolsPreamble } from "./internal/devtools.js";
91
- import { send, toRequest } from "./internal/http.js";
92
- import { beginRequest } from "./internal/serve.js";
122
+ import { send, toAddressRequest, toRequest } from "./internal/http.js";
123
+ import {
124
+ answerRouting,
125
+ answersInFrontOfFiles,
126
+ beginRequest,
127
+ forViteBase,
128
+ rewriteRouting,
129
+ } from "./internal/serve.js";
130
+ import { serverComponentsProblem } from "./internal/server-components.js";
93
131
 
94
132
  /** A resolved virtual id: Vite's convention is a leading NUL byte. */
95
133
  const resolved = (id) => `\0${id}`;
96
- const VIRTUAL_IDS = new Set(Object.values(VIRTUAL));
134
+ const VIRTUAL_IDS = new Set([...Object.values(VIRTUAL), ...Object.values(FLIGHT_VIRTUAL)]);
97
135
 
98
136
  /**
99
137
  * Prefix of the virtual module that carries one source module's StyleX rules.
@@ -152,8 +190,21 @@ export default function uniflowed(options = {}) {
152
190
  // and a driver started by hand on a config `uf` never validated gets the same
153
191
  // answer for the same reason a project would want.
154
192
  const mount = (app.rendering?.modes ?? []).includes("csr") ? "render" : "hydrate";
193
+ // Whether routes render as React Server Components, which is the default: a
194
+ // second module graph resolved under `react-server`, a document carrying the
195
+ // Flight payload it was rendered from, and a browser that hydrates that
196
+ // payload rather than importing routes. `app.rsc: false` is the application
197
+ // rendered from its modules, as every uf application was before
198
+ // ubugeeei-prod/uf#519. See `./internal/flight.js`.
199
+ const flightState = rendersFlight(app, { mount, routeTarget })
200
+ ? createFlightState({ root: options.root ?? process.cwd() })
201
+ : null;
155
202
 
156
203
  const accessibility = ufConfig.accessibility ?? {};
204
+ // `app.router.redirects`, `rewrites` and `headers`: written into the server
205
+ // bundle for every host that serves a build, and asked by `uf dev` itself
206
+ // from the same object. See `@uniflowed/server`'s `internal/routing.js`.
207
+ const routing = routingRulesOf(app.router);
157
208
 
158
209
  return [
159
210
  flowPlugin({
@@ -163,9 +214,15 @@ export default function uniflowed(options = {}) {
163
214
  strictMode,
164
215
  navigation,
165
216
  mount,
217
+ flightState,
218
+ routing,
166
219
  command: options.command,
167
220
  accessibility,
168
221
  }),
222
+ ...(flightState == null ? [] : [clientReferencePlugin(flightState), clientModuleUrlPlugin()]),
223
+ // After the references, so a client module the rsc graph has already
224
+ // replaced is not read for imports it no longer has; see the file.
225
+ barrelImportsPlugin(),
169
226
  mdxPlugin(markdown),
170
227
  assetPlugin({
171
228
  images: builtins.images ?? {},
@@ -184,6 +241,8 @@ function flowPlugin({
184
241
  strictMode,
185
242
  navigation,
186
243
  mount,
244
+ flightState,
245
+ routing,
187
246
  command,
188
247
  accessibility,
189
248
  }) {
@@ -314,10 +373,26 @@ function flowPlugin({
314
373
  return {
315
374
  name: "uf:flow",
316
375
  enforce: "pre",
376
+ // What `driver.js` runs the three builds with; `null` for an application
377
+ // rendered from its modules. See `./internal/flight.js`.
378
+ api: { flight: flightState },
317
379
 
318
380
  config(userConfig, env) {
319
381
  const projectRoot = path.resolve(userConfig.root ?? process.cwd());
382
+ // Before anything is resolved. Routes that render as React Server
383
+ // Components need `react-server-dom-parcel` and React 19.3, and the router
384
+ // installs without either (ubugeeei-prod/uf#992). Said here, once, rather
385
+ // than as an unresolved import deep in a build or a failure inside a render.
386
+ if (flightState != null) {
387
+ const problem = serverComponentsProblem(projectRoot);
388
+ if (problem != null) {
389
+ throw new Error(problem);
390
+ }
391
+ }
320
392
  isProduction = env.mode === "production" || env.command === "build";
393
+ // A reference names the client manifest only in a build, where a client
394
+ // build writes the chunks it names; a dev server names the URL it serves.
395
+ if (flightState != null) flightState.production = env.command === "build";
321
396
  // Decided here rather than in `configResolved`, because the answer has to
322
397
  // reach `optimizeDeps.include` below and that is written in this hook.
323
398
  auditsPage =
@@ -336,6 +411,9 @@ function flowPlugin({
336
411
  "react/compiler-runtime",
337
412
  "react-dom",
338
413
  "react-dom/client",
414
+ // React's Flight client, for an application whose routes render as
415
+ // Server Components; `FLIGHT_BROWSER_DEPENDENCIES` says why.
416
+ ...(flightState == null ? [] : FLIGHT_BROWSER_DEPENDENCIES),
339
417
  // The accessibility audit's engine, when this project has one.
340
418
  //
341
419
  // Named up front rather than left to be discovered. axe-core is
@@ -357,12 +435,29 @@ function flowPlugin({
357
435
  // externalised.
358
436
  noExternal: [/^@uniflowed\//],
359
437
  },
438
+ // The graph React Server Components render in, beside the two Vite
439
+ // always has — declared only for an application that renders them, so
440
+ // `app.rsc: false` is a Vite configuration with nothing added.
441
+ ...(flightState == null
442
+ ? {}
443
+ : {
444
+ environments: {
445
+ [RSC_ENVIRONMENT]: rscEnvironment({
446
+ production: env.command === "build",
447
+ exclude: uniflowedPackages(projectRoot),
448
+ }),
449
+ },
450
+ }),
360
451
  };
361
452
  },
362
453
 
363
454
  configResolved(config) {
364
455
  root = config.root;
365
456
  base = config.base;
457
+ if (flightState != null) {
458
+ flightState.root = config.root;
459
+ flightState.base = config.base;
460
+ }
366
461
  appRoot = path.resolve(root, routerRoot);
367
462
  entryPath = path.resolve(root, appEntry);
368
463
  },
@@ -372,11 +467,42 @@ function flowPlugin({
372
467
  },
373
468
 
374
469
  resolveId(id, importer, resolveOptions) {
375
- if (id === "@uniflowed/react" && !isSsr(this, resolveOptions)) {
470
+ // The client's graph, and the rsc graph too. `@uniflowed/react` is
471
+ // `export * from "react"`, and the rsc graph pre-bundles React's CommonJS
472
+ // under `react-server`: a star re-export of that namespace names nothing
473
+ // Vite's module runner can forward, so under `uf dev` every hook a server
474
+ // component imported from `@uniflowed/react` was `undefined` — `use` first.
475
+ // Importing `react` itself is the module those names are on. The ssr graph
476
+ // keeps its own resolution, because React is external there and Node's
477
+ // interop forwards the names.
478
+ if (
479
+ id === "@uniflowed/react" &&
480
+ (this.environment?.name === RSC_ENVIRONMENT || !isSsr(this, resolveOptions))
481
+ ) {
376
482
  return this.resolve("react", importer, { ...resolveOptions, skipSelf: true });
377
483
  }
378
484
  if (id === RUNTIME_PUBLIC_PATH) return RUNTIME_RESOLVED_ID;
379
485
  if (id === AUDIT_PUBLIC_PATH) return AUDIT_RESOLVED_ID;
486
+ // The rsc build cannot know a client chunk's URL, because the client
487
+ // build that writes the chunks runs after it. It leaves the manifest as an
488
+ // import, and the ssr build — which bundles the rsc output in — resolves it.
489
+ if (
490
+ id === FLIGHT_VIRTUAL.manifest &&
491
+ flightState?.production === true &&
492
+ this.environment?.name === RSC_ENVIRONMENT
493
+ ) {
494
+ return { id, external: true };
495
+ }
496
+ // The React Compiler's runtime, which reads the client's internals and so
497
+ // cannot run where `react` resolved under `react-server`; see
498
+ // `compilerRuntimeSource`.
499
+ if (
500
+ id === "react/compiler-runtime" &&
501
+ flightState != null &&
502
+ this.environment?.name === RSC_ENVIRONMENT
503
+ ) {
504
+ return resolved(FLIGHT_VIRTUAL.compilerRuntime);
505
+ }
380
506
  if (VIRTUAL_IDS.has(id)) return resolved(id);
381
507
  // A module's own stylesheet, which `transform` below asked for by
382
508
  // importing this id. Returning it unchanged marks it resolved without
@@ -390,6 +516,18 @@ function flowPlugin({
390
516
  if (id === AUDIT_RESOLVED_ID) return auditRuntimeSource(accessibility?.axe);
391
517
  if (id === resolved(VIRTUAL.routes)) {
392
518
  const table = scanRoutes(appRoot, { target: routeTarget });
519
+ // Under React Server Components the table is split by graph rather than
520
+ // filtered. The rsc graph renders routes, so it gets every route and
521
+ // boundary and no handler or middleware: those answer a request, and
522
+ // importing one here would resolve its dependencies under
523
+ // `react-server` for nothing. The ssr graph gets exactly those two,
524
+ // because every route it renders reaches it as a payload.
525
+ if (flightState != null && this.environment?.name === RSC_ENVIRONMENT) {
526
+ return routesModuleSource({ ...table, handlers: [], middleware: [] });
527
+ }
528
+ if (flightState != null && isSsr(this, loadOptions)) {
529
+ return routesModuleSource({ ...table, routes: [], notFound: [], errors: [] });
530
+ }
393
531
  // The server renders every route, so the server's table is the whole
394
532
  // one and is generated with no filter at all. Only the browser's copy
395
533
  // is split.
@@ -404,14 +542,48 @@ function flowPlugin({
404
542
  // deliberately: it is what a link does, and a dev server whose links
405
543
  // behave differently from the deployment is the wrong thing to be
406
544
  // looking at. See `clientModuleSource`.
545
+ if (id === resolved(VIRTUAL.client) && flightState != null) {
546
+ return flightClientSource(entryPath, {
547
+ strictMode: strictMode && !isProduction,
548
+ navigation,
549
+ routing,
550
+ });
551
+ }
407
552
  if (id === resolved(VIRTUAL.client)) {
408
553
  return clientModuleSource(entryPath, {
409
554
  strictMode: strictMode && !isProduction,
410
555
  navigation,
411
556
  mount,
557
+ routing,
412
558
  });
413
559
  }
414
- if (id === resolved(VIRTUAL.server)) return serverModuleSource(entryPath);
560
+ if (id === resolved(VIRTUAL.server)) {
561
+ return flightState == null
562
+ ? serverModuleSource(entryPath, routing)
563
+ : flightServerSource(entryPath, VIRTUAL.routes, VIRTUAL.actions, routing);
564
+ }
565
+ if (flightState != null) {
566
+ if (id === resolved(FLIGHT_VIRTUAL.entry)) return rscEntrySource(VIRTUAL.routes, routing);
567
+ if (id === resolved(FLIGHT_VIRTUAL.compilerRuntime)) return compilerRuntimeSource();
568
+ if (id === resolved(FLIGHT_VIRTUAL.bridge)) {
569
+ if (server != null) return devBridgeSource();
570
+ if (flightState.rscOutput == null) {
571
+ throw new Error(
572
+ "uf: the server bundle was asked to bundle the rsc graph before it was built. " +
573
+ "`uf build` builds `virtual:uf/rsc` first; a build started some other way has to as well.",
574
+ );
575
+ }
576
+ return builtBridgeSource(flightState.rscOutput);
577
+ }
578
+ if (id === resolved(FLIGHT_VIRTUAL.manifest)) {
579
+ return clientManifestSource(flightState.chunkUrls);
580
+ }
581
+ if (id === resolved(FLIGHT_VIRTUAL.references)) {
582
+ return server != null
583
+ ? devReferencesSource(root, base)
584
+ : builtReferencesSource(flightState.chunkUrls);
585
+ }
586
+ }
415
587
  // Only `virtual:uf/server` imports this, so it is only ever asked for in
416
588
  // the server environment — but the table it carries is every callable
417
589
  // endpoint of the build, so it is worth saying that a browser asking for
@@ -438,7 +610,10 @@ function flowPlugin({
438
610
  // and an import of anything else is a build error rather than a silent
439
611
  // `undefined`. `crates/uf_rsc/src/graph/build.rs` colours these modules
440
612
  // server for the same reason, so the analysis and the bundle agree.
441
- if (!isSsr(this, loadOptions)) {
613
+ //
614
+ // Nor in the rsc graph: a server component that calls an action is calling
615
+ // a function on the server, and it gets the function.
616
+ if (!isSsr(this, loadOptions) && this.environment?.name !== RSC_ENVIRONMENT) {
442
617
  const references = actionTables().modules.get(cleanId(id));
443
618
  if (references != null) return actionReferenceSource(references);
444
619
  }
@@ -446,8 +621,13 @@ function flowPlugin({
446
621
  },
447
622
 
448
623
  async transform(code, id, transformOptions) {
449
- if (!isFlowModule(id)) return null;
450
- const ssr = transformOptions?.ssr === true || this.environment?.name === "ssr";
624
+ // A view of a barrel's namespace has the barrel's path and none of its
625
+ // source: `uf:barrel-imports` generates it as JavaScript.
626
+ if (!isFlowModule(id) || namespaceViewOf(id) != null) return null;
627
+ // Both server graphs: neither gets a refresh wrapper, and the rsc graph's
628
+ // findings are reported as that graph's.
629
+ const rsc = this.environment?.name === RSC_ENVIRONMENT;
630
+ const ssr = transformOptions?.ssr === true || this.environment?.name === "ssr" || rsc;
451
631
  const refresh = !isProduction && !ssr && server != null;
452
632
  const out = await ensureService().transform(cleanId(id), code, {
453
633
  development: !isProduction,
@@ -459,7 +639,7 @@ function flowPlugin({
459
639
  id: cleanId(id),
460
640
  root,
461
641
  diagnostics: out.diagnostics,
462
- environment: ssr ? "ssr" : "client",
642
+ environment: rsc ? RSC_ENVIRONMENT : ssr ? "ssr" : "client",
463
643
  reported,
464
644
  suppressed,
465
645
  });
@@ -473,10 +653,17 @@ function flowPlugin({
473
653
  // business: Vite already injects a stylesheet in dev, extracts it in a
474
654
  // build, code-splits it per chunk, and replaces it over HMR. A module
475
655
  // whose styles are gone stops importing it, and Vite notices.
656
+ //
657
+ // The stylesheet is named by the module's path from the project root, the
658
+ // spelling `moduleId` gives the module graph report, and not by its
659
+ // absolute path. In a client chunk the stylesheet is one of the chunk's
660
+ // sources, so its name is written into the source map a site publishes,
661
+ // and an absolute name would publish where the machine that built it
662
+ // keeps its files. A module outside the root climbs out with `../`.
476
663
  const styled = out.css != null && out.css !== "";
477
664
  let output = out.code;
478
665
  if (styled) {
479
- const styleId = `${STYLE_PREFIX}${cleanId(id)}.css`;
666
+ const styleId = `${STYLE_PREFIX}${moduleId(root, cleanId(id))}.css`;
480
667
  styles.set(styleId, out.css);
481
668
  output = `import ${JSON.stringify(styleId)};\n${output}`;
482
669
  }
@@ -546,13 +733,68 @@ function flowPlugin({
546
733
  return tags;
547
734
  },
548
735
 
736
+ // An edit to a server component changes what the rsc graph renders and no
737
+ // module the browser holds, so Vite has nothing to tell the browser. It is
738
+ // reloaded, which renders the edit; an edit to a client module — which the
739
+ // rsc graph only holds references to — is left to Fast Refresh.
740
+ hotUpdate({ modules }) {
741
+ if (flightState == null || server == null) return;
742
+ if (this.environment?.name !== RSC_ENVIRONMENT) return;
743
+ const serverSide = modules.some((module) => {
744
+ const file = module.file ?? cleanId(module.id ?? "");
745
+ return file !== "" && !flightState.clientModules.has(file);
746
+ });
747
+ if (serverSide) server.environments.client.hot.send({ type: "full-reload", path: "*" });
748
+ },
749
+
549
750
  configureServer(devServer) {
550
751
  server = devServer;
752
+ // The ssr graph's way into the rsc graph; see `devBridgeSource`.
753
+ if (flightState != null) {
754
+ globalThis[Symbol.for(DEV_RSC_HOOK)] = () =>
755
+ devServer.environments[RSC_ENVIRONMENT].runner.import(FLIGHT_VIRTUAL.entry);
756
+ }
551
757
  devServer.httpServer?.once("close", () => {
552
758
  service?.close();
553
759
  service = null;
554
760
  });
555
761
 
762
+ // `app.router.headers` and `redirects`, in front of Vite's own middleware
763
+ // — the hook's body runs before those are installed — so a redirect
764
+ // answers before `public/` is looked in and a header reaches a file Vite
765
+ // serves, as both do in front of the static half of every other door.
766
+ // Vite's module server is left alone: `/@vite/client` and `/@fs/…` are
767
+ // development plumbing no deployment has, and `/__uf/` is the browser's
768
+ // channel back. Not mounted at all for a project with neither list.
769
+ if (answersInFrontOfFiles(routing)) {
770
+ devServer.middlewares.use((request, response, next) => {
771
+ const url = request.url ?? "/";
772
+ // Vite serves its own paths under the base path too, so they are
773
+ // recognised once it is taken off.
774
+ const under =
775
+ routing.basePath !== "" && url.startsWith(`${routing.basePath}/`)
776
+ ? url.slice(routing.basePath.length)
777
+ : url;
778
+ if (
779
+ under.startsWith("/@") ||
780
+ under.startsWith("/node_modules/") ||
781
+ under.startsWith("/__uf/")
782
+ ) {
783
+ next();
784
+ return;
785
+ }
786
+ answerRouting(routing, toAddressRequest(request), response)
787
+ .then((answered) => {
788
+ if (answered) return;
789
+ // The bare base path is the root, which Vite only knows as
790
+ // `/docs/`; see `forViteBase`.
791
+ forViteBase(routing, request);
792
+ next();
793
+ })
794
+ .catch(next);
795
+ });
796
+ }
797
+
556
798
  // A reserved file appearing or disappearing changes the route table,
557
799
  // which lives in a virtual module the watcher knows nothing about.
558
800
  //
@@ -664,14 +906,26 @@ function flowPlugin({
664
906
  // `request.url` and not `originalUrl`, which is the URL Vite's base
665
907
  // middleware has already stripped the base from — and the route
666
908
  // table's paths have no base in them either.
667
- const url = request.url ?? "/";
909
+ //
910
+ // `let`, because a rewrite moves it: from here on it is the address
911
+ // the application answers for, which is what the render, the payload
912
+ // and the document test below all have to agree on.
913
+ let url = request.url ?? "/";
668
914
  // Declared out here so the catch below can still settle: a request
669
915
  // that failed is a request that happened, and a middleware that
670
916
  // logged its arrival is owed its callback either way.
671
917
  let lifecycle = null;
672
918
  try {
673
919
  const entry = await importServerEntry(devServer);
674
- const asRequest = await toRequest(request, devServer.config);
920
+ // At `request.url`, which Vite's base middleware has already taken
921
+ // `app.router.basePath` off: the application path, as every other
922
+ // front door hands the application once it has admitted a request.
923
+ const arrived = await toRequest(request, devServer.config, url);
924
+ // `app.router.rewrites`, where `createFetchHandler` applies them for
925
+ // every other door: after the files Vite already served, before the
926
+ // guard — so the guard that runs is the destination's.
927
+ let asRequest = (await rewriteRouting(entry.routing, arrived)) ?? arrived;
928
+ if (asRequest !== arrived) url = addressOf(asRequest);
675
929
 
676
930
  // The request begins here and ends when the response has been
677
931
  // written, which is what `after()` promises and what `uf preview`,
@@ -679,7 +933,7 @@ function flowPlugin({
679
933
  // logs a response's status has to mean the same thing in
680
934
  // development as in production. See `internal/serve.js` and
681
935
  // ubugeeei-prod/uf#389.
682
- lifecycle = await beginRequest(entry, asRequest);
936
+ lifecycle = await beginRequest(entry, arrived);
683
937
  const answered = await lifecycle.run(async () => {
684
938
  // Before anything answers: a middleware guards a subtree, so it
685
939
  // has to run for a page, for a route handler, and for a path
@@ -687,8 +941,14 @@ function flowPlugin({
687
941
  // dispatcher and again inside the renderer would have left
688
942
  // `/dashboard/typo` unguarded and run it twice for a path that
689
943
  // is both. See ubugeeei-prod/uf#260.
944
+ //
945
+ // A `Request` back is a middleware's `rewrite()`, already past
946
+ // the destination's own middleware.
690
947
  const guarded = await entry.runMiddleware(asRequest);
691
- if (guarded != null) {
948
+ if (guarded instanceof Request) {
949
+ asRequest = guarded;
950
+ url = addressOf(guarded);
951
+ } else if (guarded != null) {
692
952
  await send(response, guarded);
693
953
  return true;
694
954
  }
@@ -713,6 +973,29 @@ function flowPlugin({
713
973
  // and a `<form action>` navigation both send `Accept:
714
974
  // text/html`, and both want the handler's answer — which is the
715
975
  // whole of ubugeeei-prod/uf#349.
976
+ // A browser that is navigating, asking for the next route's
977
+ // payload: after the guard and before the handlers, for the
978
+ // reasons `@uniflowed/server`'s `internal/flight.js` gives.
979
+ const payloadFor =
980
+ entry.flight == null ? null : flightDocumentPath(url.split("?")[0]);
981
+ if (payloadFor != null && (request.method === "GET" || request.method === "HEAD")) {
982
+ const query = url.includes("?") ? url.slice(url.indexOf("?")) : "";
983
+ const target = payloadFor + query;
984
+ const answered = await entry.flight(target, {
985
+ onError: (error) => reportRenderError(devServer, target, error),
986
+ });
987
+ if (answered.error != null) reportRenderError(devServer, target, answered.error);
988
+ if (request.method === "HEAD") await answered.stream?.cancel();
989
+ await send(
990
+ response,
991
+ new Response(request.method === "HEAD" ? null : answered.stream, {
992
+ status: answered.status,
993
+ headers: answered.headers,
994
+ }),
995
+ );
996
+ return true;
997
+ }
998
+
716
999
  const handled = await entry.dispatch(asRequest);
717
1000
  if (handled != null) {
718
1001
  await send(response, handled);
@@ -733,7 +1016,7 @@ function flowPlugin({
733
1016
  // and the project's own not-found *page* under `uf preview` and
734
1017
  // `uf start` — a difference in the body of a 404 for a path that
735
1018
  // is an asset request in the first place.
736
- const notDocument = notADocumentBecause(request);
1019
+ const notDocument = notADocumentBecause(request, url);
737
1020
  if (notDocument === "accept") {
738
1021
  // Everything about this is a navigation except the header, and
739
1022
  // the path is one uf renders. Say so, rather than letting the
@@ -778,7 +1061,18 @@ function flowPlugin({
778
1061
  onError: (error) => reportRenderError(devServer, url, error),
779
1062
  // Vite sees the head and only the head. That is what lets the
780
1063
  // development server stream like every other host — see below.
781
- transformHead: (head) => devServer.transformIndexHtml(url, head),
1064
+ //
1065
+ // Under React Server Components it is also where a server
1066
+ // component's stylesheets are linked in development: they are
1067
+ // in the rsc graph, which the browser never loads, and they
1068
+ // enter it when the route's modules are imported — after this
1069
+ // call and before the head is written, which is when this
1070
+ // runs. See `devStylesheets`.
1071
+ transformHead: (head) =>
1072
+ devServer.transformIndexHtml(
1073
+ url,
1074
+ flightState == null ? head : linkStylesheets(head, devStylesheets(devServer)),
1075
+ ),
782
1076
  // And what the streaming actually did, when it changed. The
783
1077
  // router has already decided there is something worth saying
784
1078
  // and written the words — see its `internal/inspector.js`,
@@ -862,11 +1156,7 @@ function mdxPlugin(markdown) {
862
1156
  enforce: "pre",
863
1157
  ...mdx({
864
1158
  jsxImportSource: "react",
865
- remarkPlugins: [
866
- remarkGfm,
867
- remarkFrontmatter,
868
- [remarkMdxFrontmatter, { name: "frontmatter" }],
869
- ],
1159
+ remarkPlugins: [remarkGfm, remarkFrontmatter, remarkFrontmatterExport],
870
1160
  rehypePlugins,
871
1161
  }),
872
1162
  name: "uf:mdx",
@@ -891,9 +1181,23 @@ async function importServerEntry(devServer) {
891
1181
  return devServer.ssrLoadModule(VIRTUAL.server);
892
1182
  }
893
1183
 
1184
+ /**
1185
+ * The path and query a `Request` is for, which is what the dev server's
1186
+ * renderer and payload test are keyed on.
1187
+ *
1188
+ * @param {Request} request
1189
+ */
1190
+ function addressOf(request) {
1191
+ const url = new URL(request.url);
1192
+ return url.pathname + url.search;
1193
+ }
1194
+
894
1195
  /**
895
1196
  * Why `request` is not a document request, or `null` when it is one.
896
1197
  *
1198
+ * `url` is the address the application answers for, which a rewrite may have
1199
+ * moved away from the one the request line named.
1200
+ *
897
1201
  * A reason rather than a boolean because one of the four is worth saying out
898
1202
  * loud. Three of them mean the request belongs to somebody else — Vite's module
899
1203
  * server, a static file, or a method a page cannot answer — and handing it back
@@ -905,9 +1209,8 @@ async function importServerEntry(devServer) {
905
1209
  * confused: `/favicon.svg` with `Accept: *\/*` is an asset, not a navigation
906
1210
  * with the wrong header, and still goes back to Vite's chain untouched.
907
1211
  */
908
- function notADocumentBecause(request) {
1212
+ function notADocumentBecause(request, url = request.url ?? "/") {
909
1213
  if (request.method !== "GET" && request.method !== "HEAD") return "method";
910
- const url = request.url ?? "/";
911
1214
  if (url.startsWith("/@") || url.startsWith("/node_modules/")) return "module-server";
912
1215
  const pathname = url.split("?")[0];
913
1216
  // A request for a file — `/favicon.svg`, `/assets/x.js` — that no static