@uniflowed/vite 0.0.0-alpha.35 → 0.0.0-alpha.39

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}`;
@@ -166,6 +183,16 @@ export default function uniflowed(options = {}) {
166
183
  // at. Anything but `"document"` is the client router, which is what every
167
184
  // project that has not heard of the key has.
168
185
  const navigation = app.rendering?.navigation === "document" ? "document" : "client";
186
+ // How long, in seconds, the client router shows a route it already fetched
187
+ // without asking the server again, written into the client entry beside
188
+ // `navigation` and honoured by every command for the same reason. Anything
189
+ // but a positive number is `0`, which keeps nothing.
190
+ const staleTime =
191
+ typeof app.rendering?.staleTime === "number" &&
192
+ Number.isFinite(app.rendering.staleTime) &&
193
+ app.rendering.staleTime > 0
194
+ ? app.rendering.staleTime
195
+ : 0;
169
196
  // Whether this application starts by attaching to markup or by rendering
170
197
  // into an empty root. `["csr"]` is the only list that means the second, and
171
198
  // `uf` refuses that value beside any other while the config is read — so the
@@ -184,6 +211,10 @@ export default function uniflowed(options = {}) {
184
211
  : null;
185
212
 
186
213
  const accessibility = ufConfig.accessibility ?? {};
214
+ // `app.router.redirects`, `rewrites` and `headers`: written into the server
215
+ // bundle for every host that serves a build, and asked by `uf dev` itself
216
+ // from the same object. See `@uniflowed/server`'s `internal/routing.js`.
217
+ const routing = routingRulesOf(app.router);
187
218
 
188
219
  return [
189
220
  flowPlugin({
@@ -192,12 +223,17 @@ export default function uniflowed(options = {}) {
192
223
  routeTarget,
193
224
  strictMode,
194
225
  navigation,
226
+ staleTime,
195
227
  mount,
196
228
  flightState,
229
+ routing,
197
230
  command: options.command,
198
231
  accessibility,
199
232
  }),
200
- ...(flightState == null ? [] : [clientReferencePlugin(flightState)]),
233
+ ...(flightState == null ? [] : [clientReferencePlugin(flightState), clientModuleUrlPlugin()]),
234
+ // After the references, so a client module the rsc graph has already
235
+ // replaced is not read for imports it no longer has; see the file.
236
+ barrelImportsPlugin(),
201
237
  mdxPlugin(markdown),
202
238
  assetPlugin({
203
239
  images: builtins.images ?? {},
@@ -215,8 +251,10 @@ function flowPlugin({
215
251
  routeTarget,
216
252
  strictMode,
217
253
  navigation,
254
+ staleTime,
218
255
  mount,
219
256
  flightState,
257
+ routing,
220
258
  command,
221
259
  accessibility,
222
260
  }) {
@@ -353,6 +391,16 @@ function flowPlugin({
353
391
 
354
392
  config(userConfig, env) {
355
393
  const projectRoot = path.resolve(userConfig.root ?? process.cwd());
394
+ // Before anything is resolved. Routes that render as React Server
395
+ // Components need `react-server-dom-parcel` and React 19.3, and the router
396
+ // installs without either (ubugeeei-prod/uf#992). Said here, once, rather
397
+ // than as an unresolved import deep in a build or a failure inside a render.
398
+ if (flightState != null) {
399
+ const problem = serverComponentsProblem(projectRoot);
400
+ if (problem != null) {
401
+ throw new Error(problem);
402
+ }
403
+ }
356
404
  isProduction = env.mode === "production" || env.command === "build";
357
405
  // A reference names the client manifest only in a build, where a client
358
406
  // build writes the chunks it names; a dev server names the URL it serves.
@@ -375,6 +423,9 @@ function flowPlugin({
375
423
  "react/compiler-runtime",
376
424
  "react-dom",
377
425
  "react-dom/client",
426
+ // React's Flight client, for an application whose routes render as
427
+ // Server Components; `FLIGHT_BROWSER_DEPENDENCIES` says why.
428
+ ...(flightState == null ? [] : FLIGHT_BROWSER_DEPENDENCIES),
378
429
  // The accessibility audit's engine, when this project has one.
379
430
  //
380
431
  // Named up front rather than left to be discovered. axe-core is
@@ -507,22 +558,26 @@ function flowPlugin({
507
558
  return flightClientSource(entryPath, {
508
559
  strictMode: strictMode && !isProduction,
509
560
  navigation,
561
+ staleTime,
562
+ routing,
510
563
  });
511
564
  }
512
565
  if (id === resolved(VIRTUAL.client)) {
513
566
  return clientModuleSource(entryPath, {
514
567
  strictMode: strictMode && !isProduction,
515
568
  navigation,
569
+ staleTime,
516
570
  mount,
571
+ routing,
517
572
  });
518
573
  }
519
574
  if (id === resolved(VIRTUAL.server)) {
520
575
  return flightState == null
521
- ? serverModuleSource(entryPath)
522
- : flightServerSource(entryPath, VIRTUAL.routes, VIRTUAL.actions);
576
+ ? serverModuleSource(entryPath, routing)
577
+ : flightServerSource(entryPath, VIRTUAL.routes, VIRTUAL.actions, routing);
523
578
  }
524
579
  if (flightState != null) {
525
- if (id === resolved(FLIGHT_VIRTUAL.entry)) return rscEntrySource(VIRTUAL.routes);
580
+ if (id === resolved(FLIGHT_VIRTUAL.entry)) return rscEntrySource(VIRTUAL.routes, routing);
526
581
  if (id === resolved(FLIGHT_VIRTUAL.compilerRuntime)) return compilerRuntimeSource();
527
582
  if (id === resolved(FLIGHT_VIRTUAL.bridge)) {
528
583
  if (server != null) return devBridgeSource();
@@ -580,7 +635,9 @@ function flowPlugin({
580
635
  },
581
636
 
582
637
  async transform(code, id, transformOptions) {
583
- if (!isFlowModule(id)) return null;
638
+ // A view of a barrel's namespace has the barrel's path and none of its
639
+ // source: `uf:barrel-imports` generates it as JavaScript.
640
+ if (!isFlowModule(id) || namespaceViewOf(id) != null) return null;
584
641
  // Both server graphs: neither gets a refresh wrapper, and the rsc graph's
585
642
  // findings are reported as that graph's.
586
643
  const rsc = this.environment?.name === RSC_ENVIRONMENT;
@@ -610,10 +667,17 @@ function flowPlugin({
610
667
  // business: Vite already injects a stylesheet in dev, extracts it in a
611
668
  // build, code-splits it per chunk, and replaces it over HMR. A module
612
669
  // whose styles are gone stops importing it, and Vite notices.
670
+ //
671
+ // The stylesheet is named by the module's path from the project root, the
672
+ // spelling `moduleId` gives the module graph report, and not by its
673
+ // absolute path. In a client chunk the stylesheet is one of the chunk's
674
+ // sources, so its name is written into the source map a site publishes,
675
+ // and an absolute name would publish where the machine that built it
676
+ // keeps its files. A module outside the root climbs out with `../`.
613
677
  const styled = out.css != null && out.css !== "";
614
678
  let output = out.code;
615
679
  if (styled) {
616
- const styleId = `${STYLE_PREFIX}${cleanId(id)}.css`;
680
+ const styleId = `${STYLE_PREFIX}${moduleId(root, cleanId(id))}.css`;
617
681
  styles.set(styleId, out.css);
618
682
  output = `import ${JSON.stringify(styleId)};\n${output}`;
619
683
  }
@@ -709,6 +773,42 @@ function flowPlugin({
709
773
  service = null;
710
774
  });
711
775
 
776
+ // `app.router.headers` and `redirects`, in front of Vite's own middleware
777
+ // — the hook's body runs before those are installed — so a redirect
778
+ // answers before `public/` is looked in and a header reaches a file Vite
779
+ // serves, as both do in front of the static half of every other door.
780
+ // Vite's module server is left alone: `/@vite/client` and `/@fs/…` are
781
+ // development plumbing no deployment has, and `/__uf/` is the browser's
782
+ // channel back. Not mounted at all for a project with neither list.
783
+ if (answersInFrontOfFiles(routing)) {
784
+ devServer.middlewares.use((request, response, next) => {
785
+ const url = request.url ?? "/";
786
+ // Vite serves its own paths under the base path too, so they are
787
+ // recognised once it is taken off.
788
+ const under =
789
+ routing.basePath !== "" && url.startsWith(`${routing.basePath}/`)
790
+ ? url.slice(routing.basePath.length)
791
+ : url;
792
+ if (
793
+ under.startsWith("/@") ||
794
+ under.startsWith("/node_modules/") ||
795
+ under.startsWith("/__uf/")
796
+ ) {
797
+ next();
798
+ return;
799
+ }
800
+ answerRouting(routing, toAddressRequest(request), response)
801
+ .then((answered) => {
802
+ if (answered) return;
803
+ // The bare base path is the root, which Vite only knows as
804
+ // `/docs/`; see `forViteBase`.
805
+ forViteBase(routing, request);
806
+ next();
807
+ })
808
+ .catch(next);
809
+ });
810
+ }
811
+
712
812
  // A reserved file appearing or disappearing changes the route table,
713
813
  // which lives in a virtual module the watcher knows nothing about.
714
814
  //
@@ -820,14 +920,26 @@ function flowPlugin({
820
920
  // `request.url` and not `originalUrl`, which is the URL Vite's base
821
921
  // middleware has already stripped the base from — and the route
822
922
  // table's paths have no base in them either.
823
- const url = request.url ?? "/";
923
+ //
924
+ // `let`, because a rewrite moves it: from here on it is the address
925
+ // the application answers for, which is what the render, the payload
926
+ // and the document test below all have to agree on.
927
+ let url = request.url ?? "/";
824
928
  // Declared out here so the catch below can still settle: a request
825
929
  // that failed is a request that happened, and a middleware that
826
930
  // logged its arrival is owed its callback either way.
827
931
  let lifecycle = null;
828
932
  try {
829
933
  const entry = await importServerEntry(devServer);
830
- const asRequest = await toRequest(request, devServer.config);
934
+ // At `request.url`, which Vite's base middleware has already taken
935
+ // `app.router.basePath` off: the application path, as every other
936
+ // front door hands the application once it has admitted a request.
937
+ const arrived = await toRequest(request, devServer.config, url);
938
+ // `app.router.rewrites`, where `createFetchHandler` applies them for
939
+ // every other door: after the files Vite already served, before the
940
+ // guard — so the guard that runs is the destination's.
941
+ let asRequest = (await rewriteRouting(entry.routing, arrived)) ?? arrived;
942
+ if (asRequest !== arrived) url = addressOf(asRequest);
831
943
 
832
944
  // The request begins here and ends when the response has been
833
945
  // written, which is what `after()` promises and what `uf preview`,
@@ -835,7 +947,7 @@ function flowPlugin({
835
947
  // logs a response's status has to mean the same thing in
836
948
  // development as in production. See `internal/serve.js` and
837
949
  // ubugeeei-prod/uf#389.
838
- lifecycle = await beginRequest(entry, asRequest);
950
+ lifecycle = await beginRequest(entry, arrived);
839
951
  const answered = await lifecycle.run(async () => {
840
952
  // Before anything answers: a middleware guards a subtree, so it
841
953
  // has to run for a page, for a route handler, and for a path
@@ -843,8 +955,14 @@ function flowPlugin({
843
955
  // dispatcher and again inside the renderer would have left
844
956
  // `/dashboard/typo` unguarded and run it twice for a path that
845
957
  // is both. See ubugeeei-prod/uf#260.
958
+ //
959
+ // A `Request` back is a middleware's `rewrite()`, already past
960
+ // the destination's own middleware.
846
961
  const guarded = await entry.runMiddleware(asRequest);
847
- if (guarded != null) {
962
+ if (guarded instanceof Request) {
963
+ asRequest = guarded;
964
+ url = addressOf(guarded);
965
+ } else if (guarded != null) {
848
966
  await send(response, guarded);
849
967
  return true;
850
968
  }
@@ -912,7 +1030,7 @@ function flowPlugin({
912
1030
  // and the project's own not-found *page* under `uf preview` and
913
1031
  // `uf start` — a difference in the body of a 404 for a path that
914
1032
  // is an asset request in the first place.
915
- const notDocument = notADocumentBecause(request);
1033
+ const notDocument = notADocumentBecause(request, url);
916
1034
  if (notDocument === "accept") {
917
1035
  // Everything about this is a navigation except the header, and
918
1036
  // the path is one uf renders. Say so, rather than letting the
@@ -1077,9 +1195,23 @@ async function importServerEntry(devServer) {
1077
1195
  return devServer.ssrLoadModule(VIRTUAL.server);
1078
1196
  }
1079
1197
 
1198
+ /**
1199
+ * The path and query a `Request` is for, which is what the dev server's
1200
+ * renderer and payload test are keyed on.
1201
+ *
1202
+ * @param {Request} request
1203
+ */
1204
+ function addressOf(request) {
1205
+ const url = new URL(request.url);
1206
+ return url.pathname + url.search;
1207
+ }
1208
+
1080
1209
  /**
1081
1210
  * Why `request` is not a document request, or `null` when it is one.
1082
1211
  *
1212
+ * `url` is the address the application answers for, which a rewrite may have
1213
+ * moved away from the one the request line named.
1214
+ *
1083
1215
  * A reason rather than a boolean because one of the four is worth saying out
1084
1216
  * loud. Three of them mean the request belongs to somebody else — Vite's module
1085
1217
  * server, a static file, or a method a page cannot answer — and handing it back
@@ -1091,9 +1223,8 @@ async function importServerEntry(devServer) {
1091
1223
  * confused: `/favicon.svg` with `Accept: *\/*` is an asset, not a navigation
1092
1224
  * with the wrong header, and still goes back to Vite's chain untouched.
1093
1225
  */
1094
- function notADocumentBecause(request) {
1226
+ function notADocumentBecause(request, url = request.url ?? "/") {
1095
1227
  if (request.method !== "GET" && request.method !== "HEAD") return "method";
1096
- const url = request.url ?? "/";
1097
1228
  if (url.startsWith("/@") || url.startsWith("/node_modules/")) return "module-server";
1098
1229
  const pathname = url.split("?")[0];
1099
1230
  // A request for a file — `/favicon.svg`, `/assets/x.js` — that no static