@uniflowed/vite 0.0.0-alpha.35 → 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,9 +58,11 @@ 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";
57
63
  import remarkFrontmatterExport from "./internal/frontmatter.js";
58
64
  import { highlightPlugin } from "./internal/highlight.js";
65
+ import { moduleId } from "./internal/module-graph.js";
59
66
  import remarkFrontmatter from "remark-frontmatter";
60
67
  import remarkGfm from "remark-gfm";
61
68
 
@@ -82,17 +89,20 @@ 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";
89
97
  import {
90
98
  DEV_RSC_HOOK,
99
+ FLIGHT_BROWSER_DEPENDENCIES,
91
100
  FLIGHT_VIRTUAL,
92
101
  RSC_ENVIRONMENT,
93
102
  builtBridgeSource,
94
103
  builtReferencesSource,
95
104
  clientManifestSource,
105
+ clientModuleUrlPlugin,
96
106
  clientReferencePlugin,
97
107
  compilerRuntimeSource,
98
108
  createFlightState,
@@ -109,8 +119,15 @@ import {
109
119
  } from "./internal/flight.js";
110
120
  import { createChannelMiddleware } from "./internal/diagnostics.js";
111
121
  import { devtoolsPreamble } from "./internal/devtools.js";
112
- import { send, toRequest } from "./internal/http.js";
113
- 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";
114
131
 
115
132
  /** A resolved virtual id: Vite's convention is a leading NUL byte. */
116
133
  const resolved = (id) => `\0${id}`;
@@ -184,6 +201,10 @@ export default function uniflowed(options = {}) {
184
201
  : null;
185
202
 
186
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);
187
208
 
188
209
  return [
189
210
  flowPlugin({
@@ -194,10 +215,14 @@ export default function uniflowed(options = {}) {
194
215
  navigation,
195
216
  mount,
196
217
  flightState,
218
+ routing,
197
219
  command: options.command,
198
220
  accessibility,
199
221
  }),
200
- ...(flightState == null ? [] : [clientReferencePlugin(flightState)]),
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(),
201
226
  mdxPlugin(markdown),
202
227
  assetPlugin({
203
228
  images: builtins.images ?? {},
@@ -217,6 +242,7 @@ function flowPlugin({
217
242
  navigation,
218
243
  mount,
219
244
  flightState,
245
+ routing,
220
246
  command,
221
247
  accessibility,
222
248
  }) {
@@ -353,6 +379,16 @@ function flowPlugin({
353
379
 
354
380
  config(userConfig, env) {
355
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
+ }
356
392
  isProduction = env.mode === "production" || env.command === "build";
357
393
  // A reference names the client manifest only in a build, where a client
358
394
  // build writes the chunks it names; a dev server names the URL it serves.
@@ -375,6 +411,9 @@ function flowPlugin({
375
411
  "react/compiler-runtime",
376
412
  "react-dom",
377
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),
378
417
  // The accessibility audit's engine, when this project has one.
379
418
  //
380
419
  // Named up front rather than left to be discovered. axe-core is
@@ -507,6 +546,7 @@ function flowPlugin({
507
546
  return flightClientSource(entryPath, {
508
547
  strictMode: strictMode && !isProduction,
509
548
  navigation,
549
+ routing,
510
550
  });
511
551
  }
512
552
  if (id === resolved(VIRTUAL.client)) {
@@ -514,15 +554,16 @@ function flowPlugin({
514
554
  strictMode: strictMode && !isProduction,
515
555
  navigation,
516
556
  mount,
557
+ routing,
517
558
  });
518
559
  }
519
560
  if (id === resolved(VIRTUAL.server)) {
520
561
  return flightState == null
521
- ? serverModuleSource(entryPath)
522
- : flightServerSource(entryPath, VIRTUAL.routes, VIRTUAL.actions);
562
+ ? serverModuleSource(entryPath, routing)
563
+ : flightServerSource(entryPath, VIRTUAL.routes, VIRTUAL.actions, routing);
523
564
  }
524
565
  if (flightState != null) {
525
- if (id === resolved(FLIGHT_VIRTUAL.entry)) return rscEntrySource(VIRTUAL.routes);
566
+ if (id === resolved(FLIGHT_VIRTUAL.entry)) return rscEntrySource(VIRTUAL.routes, routing);
526
567
  if (id === resolved(FLIGHT_VIRTUAL.compilerRuntime)) return compilerRuntimeSource();
527
568
  if (id === resolved(FLIGHT_VIRTUAL.bridge)) {
528
569
  if (server != null) return devBridgeSource();
@@ -580,7 +621,9 @@ function flowPlugin({
580
621
  },
581
622
 
582
623
  async transform(code, id, transformOptions) {
583
- if (!isFlowModule(id)) return null;
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;
584
627
  // Both server graphs: neither gets a refresh wrapper, and the rsc graph's
585
628
  // findings are reported as that graph's.
586
629
  const rsc = this.environment?.name === RSC_ENVIRONMENT;
@@ -610,10 +653,17 @@ function flowPlugin({
610
653
  // business: Vite already injects a stylesheet in dev, extracts it in a
611
654
  // build, code-splits it per chunk, and replaces it over HMR. A module
612
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 `../`.
613
663
  const styled = out.css != null && out.css !== "";
614
664
  let output = out.code;
615
665
  if (styled) {
616
- const styleId = `${STYLE_PREFIX}${cleanId(id)}.css`;
666
+ const styleId = `${STYLE_PREFIX}${moduleId(root, cleanId(id))}.css`;
617
667
  styles.set(styleId, out.css);
618
668
  output = `import ${JSON.stringify(styleId)};\n${output}`;
619
669
  }
@@ -709,6 +759,42 @@ function flowPlugin({
709
759
  service = null;
710
760
  });
711
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
+
712
798
  // A reserved file appearing or disappearing changes the route table,
713
799
  // which lives in a virtual module the watcher knows nothing about.
714
800
  //
@@ -820,14 +906,26 @@ function flowPlugin({
820
906
  // `request.url` and not `originalUrl`, which is the URL Vite's base
821
907
  // middleware has already stripped the base from — and the route
822
908
  // table's paths have no base in them either.
823
- 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 ?? "/";
824
914
  // Declared out here so the catch below can still settle: a request
825
915
  // that failed is a request that happened, and a middleware that
826
916
  // logged its arrival is owed its callback either way.
827
917
  let lifecycle = null;
828
918
  try {
829
919
  const entry = await importServerEntry(devServer);
830
- 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);
831
929
 
832
930
  // The request begins here and ends when the response has been
833
931
  // written, which is what `after()` promises and what `uf preview`,
@@ -835,7 +933,7 @@ function flowPlugin({
835
933
  // logs a response's status has to mean the same thing in
836
934
  // development as in production. See `internal/serve.js` and
837
935
  // ubugeeei-prod/uf#389.
838
- lifecycle = await beginRequest(entry, asRequest);
936
+ lifecycle = await beginRequest(entry, arrived);
839
937
  const answered = await lifecycle.run(async () => {
840
938
  // Before anything answers: a middleware guards a subtree, so it
841
939
  // has to run for a page, for a route handler, and for a path
@@ -843,8 +941,14 @@ function flowPlugin({
843
941
  // dispatcher and again inside the renderer would have left
844
942
  // `/dashboard/typo` unguarded and run it twice for a path that
845
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.
846
947
  const guarded = await entry.runMiddleware(asRequest);
847
- if (guarded != null) {
948
+ if (guarded instanceof Request) {
949
+ asRequest = guarded;
950
+ url = addressOf(guarded);
951
+ } else if (guarded != null) {
848
952
  await send(response, guarded);
849
953
  return true;
850
954
  }
@@ -912,7 +1016,7 @@ function flowPlugin({
912
1016
  // and the project's own not-found *page* under `uf preview` and
913
1017
  // `uf start` — a difference in the body of a 404 for a path that
914
1018
  // is an asset request in the first place.
915
- const notDocument = notADocumentBecause(request);
1019
+ const notDocument = notADocumentBecause(request, url);
916
1020
  if (notDocument === "accept") {
917
1021
  // Everything about this is a navigation except the header, and
918
1022
  // the path is one uf renders. Say so, rather than letting the
@@ -1077,9 +1181,23 @@ async function importServerEntry(devServer) {
1077
1181
  return devServer.ssrLoadModule(VIRTUAL.server);
1078
1182
  }
1079
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
+
1080
1195
  /**
1081
1196
  * Why `request` is not a document request, or `null` when it is one.
1082
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
+ *
1083
1201
  * A reason rather than a boolean because one of the four is worth saying out
1084
1202
  * loud. Three of them mean the request belongs to somebody else — Vite's module
1085
1203
  * server, a static file, or a method a page cannot answer — and handing it back
@@ -1091,9 +1209,8 @@ async function importServerEntry(devServer) {
1091
1209
  * confused: `/favicon.svg` with `Accept: *\/*` is an asset, not a navigation
1092
1210
  * with the wrong header, and still goes back to Vite's chain untouched.
1093
1211
  */
1094
- function notADocumentBecause(request) {
1212
+ function notADocumentBecause(request, url = request.url ?? "/") {
1095
1213
  if (request.method !== "GET" && request.method !== "HEAD") return "method";
1096
- const url = request.url ?? "/";
1097
1214
  if (url.startsWith("/@") || url.startsWith("/node_modules/")) return "module-server";
1098
1215
  const pathname = url.split("?")[0];
1099
1216
  // A request for a file — `/favicon.svg`, `/assets/x.js` — that no static