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

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
@@ -54,10 +54,10 @@ import {
54
54
  } from "./internal/a11y.js";
55
55
  import { assetPlugin } from "./internal/assets.js";
56
56
  import { emit, reportRenderError } from "./internal/events.js";
57
+ import remarkFrontmatterExport from "./internal/frontmatter.js";
57
58
  import { highlightPlugin } from "./internal/highlight.js";
58
59
  import remarkFrontmatter from "remark-frontmatter";
59
60
  import remarkGfm from "remark-gfm";
60
- import remarkMdxFrontmatter from "remark-mdx-frontmatter";
61
61
 
62
62
  import {
63
63
  RUNTIME_PUBLIC_PATH,
@@ -86,6 +86,27 @@ import {
86
86
  serverModuleSource,
87
87
  } from "./internal/routes.js";
88
88
  import { TransformService, isFlowModule } from "@uniflowed/host/transform";
89
+ import {
90
+ DEV_RSC_HOOK,
91
+ FLIGHT_VIRTUAL,
92
+ RSC_ENVIRONMENT,
93
+ builtBridgeSource,
94
+ builtReferencesSource,
95
+ clientManifestSource,
96
+ clientReferencePlugin,
97
+ compilerRuntimeSource,
98
+ createFlightState,
99
+ devBridgeSource,
100
+ devReferencesSource,
101
+ devStylesheets,
102
+ flightClientSource,
103
+ flightDocumentPath,
104
+ flightServerSource,
105
+ linkStylesheets,
106
+ rendersFlight,
107
+ rscEntrySource,
108
+ rscEnvironment,
109
+ } from "./internal/flight.js";
89
110
  import { createChannelMiddleware } from "./internal/diagnostics.js";
90
111
  import { devtoolsPreamble } from "./internal/devtools.js";
91
112
  import { send, toRequest } from "./internal/http.js";
@@ -93,7 +114,7 @@ import { beginRequest } from "./internal/serve.js";
93
114
 
94
115
  /** A resolved virtual id: Vite's convention is a leading NUL byte. */
95
116
  const resolved = (id) => `\0${id}`;
96
- const VIRTUAL_IDS = new Set(Object.values(VIRTUAL));
117
+ const VIRTUAL_IDS = new Set([...Object.values(VIRTUAL), ...Object.values(FLIGHT_VIRTUAL)]);
97
118
 
98
119
  /**
99
120
  * Prefix of the virtual module that carries one source module's StyleX rules.
@@ -152,6 +173,15 @@ export default function uniflowed(options = {}) {
152
173
  // and a driver started by hand on a config `uf` never validated gets the same
153
174
  // answer for the same reason a project would want.
154
175
  const mount = (app.rendering?.modes ?? []).includes("csr") ? "render" : "hydrate";
176
+ // Whether routes render as React Server Components, which is the default: a
177
+ // second module graph resolved under `react-server`, a document carrying the
178
+ // Flight payload it was rendered from, and a browser that hydrates that
179
+ // payload rather than importing routes. `app.rsc: false` is the application
180
+ // rendered from its modules, as every uf application was before
181
+ // ubugeeei-prod/uf#519. See `./internal/flight.js`.
182
+ const flightState = rendersFlight(app, { mount, routeTarget })
183
+ ? createFlightState({ root: options.root ?? process.cwd() })
184
+ : null;
155
185
 
156
186
  const accessibility = ufConfig.accessibility ?? {};
157
187
 
@@ -163,9 +193,11 @@ export default function uniflowed(options = {}) {
163
193
  strictMode,
164
194
  navigation,
165
195
  mount,
196
+ flightState,
166
197
  command: options.command,
167
198
  accessibility,
168
199
  }),
200
+ ...(flightState == null ? [] : [clientReferencePlugin(flightState)]),
169
201
  mdxPlugin(markdown),
170
202
  assetPlugin({
171
203
  images: builtins.images ?? {},
@@ -184,6 +216,7 @@ function flowPlugin({
184
216
  strictMode,
185
217
  navigation,
186
218
  mount,
219
+ flightState,
187
220
  command,
188
221
  accessibility,
189
222
  }) {
@@ -314,10 +347,16 @@ function flowPlugin({
314
347
  return {
315
348
  name: "uf:flow",
316
349
  enforce: "pre",
350
+ // What `driver.js` runs the three builds with; `null` for an application
351
+ // rendered from its modules. See `./internal/flight.js`.
352
+ api: { flight: flightState },
317
353
 
318
354
  config(userConfig, env) {
319
355
  const projectRoot = path.resolve(userConfig.root ?? process.cwd());
320
356
  isProduction = env.mode === "production" || env.command === "build";
357
+ // A reference names the client manifest only in a build, where a client
358
+ // build writes the chunks it names; a dev server names the URL it serves.
359
+ if (flightState != null) flightState.production = env.command === "build";
321
360
  // Decided here rather than in `configResolved`, because the answer has to
322
361
  // reach `optimizeDeps.include` below and that is written in this hook.
323
362
  auditsPage =
@@ -357,12 +396,29 @@ function flowPlugin({
357
396
  // externalised.
358
397
  noExternal: [/^@uniflowed\//],
359
398
  },
399
+ // The graph React Server Components render in, beside the two Vite
400
+ // always has — declared only for an application that renders them, so
401
+ // `app.rsc: false` is a Vite configuration with nothing added.
402
+ ...(flightState == null
403
+ ? {}
404
+ : {
405
+ environments: {
406
+ [RSC_ENVIRONMENT]: rscEnvironment({
407
+ production: env.command === "build",
408
+ exclude: uniflowedPackages(projectRoot),
409
+ }),
410
+ },
411
+ }),
360
412
  };
361
413
  },
362
414
 
363
415
  configResolved(config) {
364
416
  root = config.root;
365
417
  base = config.base;
418
+ if (flightState != null) {
419
+ flightState.root = config.root;
420
+ flightState.base = config.base;
421
+ }
366
422
  appRoot = path.resolve(root, routerRoot);
367
423
  entryPath = path.resolve(root, appEntry);
368
424
  },
@@ -372,11 +428,42 @@ function flowPlugin({
372
428
  },
373
429
 
374
430
  resolveId(id, importer, resolveOptions) {
375
- if (id === "@uniflowed/react" && !isSsr(this, resolveOptions)) {
431
+ // The client's graph, and the rsc graph too. `@uniflowed/react` is
432
+ // `export * from "react"`, and the rsc graph pre-bundles React's CommonJS
433
+ // under `react-server`: a star re-export of that namespace names nothing
434
+ // Vite's module runner can forward, so under `uf dev` every hook a server
435
+ // component imported from `@uniflowed/react` was `undefined` — `use` first.
436
+ // Importing `react` itself is the module those names are on. The ssr graph
437
+ // keeps its own resolution, because React is external there and Node's
438
+ // interop forwards the names.
439
+ if (
440
+ id === "@uniflowed/react" &&
441
+ (this.environment?.name === RSC_ENVIRONMENT || !isSsr(this, resolveOptions))
442
+ ) {
376
443
  return this.resolve("react", importer, { ...resolveOptions, skipSelf: true });
377
444
  }
378
445
  if (id === RUNTIME_PUBLIC_PATH) return RUNTIME_RESOLVED_ID;
379
446
  if (id === AUDIT_PUBLIC_PATH) return AUDIT_RESOLVED_ID;
447
+ // The rsc build cannot know a client chunk's URL, because the client
448
+ // build that writes the chunks runs after it. It leaves the manifest as an
449
+ // import, and the ssr build — which bundles the rsc output in — resolves it.
450
+ if (
451
+ id === FLIGHT_VIRTUAL.manifest &&
452
+ flightState?.production === true &&
453
+ this.environment?.name === RSC_ENVIRONMENT
454
+ ) {
455
+ return { id, external: true };
456
+ }
457
+ // The React Compiler's runtime, which reads the client's internals and so
458
+ // cannot run where `react` resolved under `react-server`; see
459
+ // `compilerRuntimeSource`.
460
+ if (
461
+ id === "react/compiler-runtime" &&
462
+ flightState != null &&
463
+ this.environment?.name === RSC_ENVIRONMENT
464
+ ) {
465
+ return resolved(FLIGHT_VIRTUAL.compilerRuntime);
466
+ }
380
467
  if (VIRTUAL_IDS.has(id)) return resolved(id);
381
468
  // A module's own stylesheet, which `transform` below asked for by
382
469
  // importing this id. Returning it unchanged marks it resolved without
@@ -390,6 +477,18 @@ function flowPlugin({
390
477
  if (id === AUDIT_RESOLVED_ID) return auditRuntimeSource(accessibility?.axe);
391
478
  if (id === resolved(VIRTUAL.routes)) {
392
479
  const table = scanRoutes(appRoot, { target: routeTarget });
480
+ // Under React Server Components the table is split by graph rather than
481
+ // filtered. The rsc graph renders routes, so it gets every route and
482
+ // boundary and no handler or middleware: those answer a request, and
483
+ // importing one here would resolve its dependencies under
484
+ // `react-server` for nothing. The ssr graph gets exactly those two,
485
+ // because every route it renders reaches it as a payload.
486
+ if (flightState != null && this.environment?.name === RSC_ENVIRONMENT) {
487
+ return routesModuleSource({ ...table, handlers: [], middleware: [] });
488
+ }
489
+ if (flightState != null && isSsr(this, loadOptions)) {
490
+ return routesModuleSource({ ...table, routes: [], notFound: [], errors: [] });
491
+ }
393
492
  // The server renders every route, so the server's table is the whole
394
493
  // one and is generated with no filter at all. Only the browser's copy
395
494
  // is split.
@@ -404,6 +503,12 @@ function flowPlugin({
404
503
  // deliberately: it is what a link does, and a dev server whose links
405
504
  // behave differently from the deployment is the wrong thing to be
406
505
  // looking at. See `clientModuleSource`.
506
+ if (id === resolved(VIRTUAL.client) && flightState != null) {
507
+ return flightClientSource(entryPath, {
508
+ strictMode: strictMode && !isProduction,
509
+ navigation,
510
+ });
511
+ }
407
512
  if (id === resolved(VIRTUAL.client)) {
408
513
  return clientModuleSource(entryPath, {
409
514
  strictMode: strictMode && !isProduction,
@@ -411,7 +516,33 @@ function flowPlugin({
411
516
  mount,
412
517
  });
413
518
  }
414
- if (id === resolved(VIRTUAL.server)) return serverModuleSource(entryPath);
519
+ if (id === resolved(VIRTUAL.server)) {
520
+ return flightState == null
521
+ ? serverModuleSource(entryPath)
522
+ : flightServerSource(entryPath, VIRTUAL.routes, VIRTUAL.actions);
523
+ }
524
+ if (flightState != null) {
525
+ if (id === resolved(FLIGHT_VIRTUAL.entry)) return rscEntrySource(VIRTUAL.routes);
526
+ if (id === resolved(FLIGHT_VIRTUAL.compilerRuntime)) return compilerRuntimeSource();
527
+ if (id === resolved(FLIGHT_VIRTUAL.bridge)) {
528
+ if (server != null) return devBridgeSource();
529
+ if (flightState.rscOutput == null) {
530
+ throw new Error(
531
+ "uf: the server bundle was asked to bundle the rsc graph before it was built. " +
532
+ "`uf build` builds `virtual:uf/rsc` first; a build started some other way has to as well.",
533
+ );
534
+ }
535
+ return builtBridgeSource(flightState.rscOutput);
536
+ }
537
+ if (id === resolved(FLIGHT_VIRTUAL.manifest)) {
538
+ return clientManifestSource(flightState.chunkUrls);
539
+ }
540
+ if (id === resolved(FLIGHT_VIRTUAL.references)) {
541
+ return server != null
542
+ ? devReferencesSource(root, base)
543
+ : builtReferencesSource(flightState.chunkUrls);
544
+ }
545
+ }
415
546
  // Only `virtual:uf/server` imports this, so it is only ever asked for in
416
547
  // the server environment — but the table it carries is every callable
417
548
  // endpoint of the build, so it is worth saying that a browser asking for
@@ -438,7 +569,10 @@ function flowPlugin({
438
569
  // and an import of anything else is a build error rather than a silent
439
570
  // `undefined`. `crates/uf_rsc/src/graph/build.rs` colours these modules
440
571
  // server for the same reason, so the analysis and the bundle agree.
441
- if (!isSsr(this, loadOptions)) {
572
+ //
573
+ // Nor in the rsc graph: a server component that calls an action is calling
574
+ // a function on the server, and it gets the function.
575
+ if (!isSsr(this, loadOptions) && this.environment?.name !== RSC_ENVIRONMENT) {
442
576
  const references = actionTables().modules.get(cleanId(id));
443
577
  if (references != null) return actionReferenceSource(references);
444
578
  }
@@ -447,7 +581,10 @@ function flowPlugin({
447
581
 
448
582
  async transform(code, id, transformOptions) {
449
583
  if (!isFlowModule(id)) return null;
450
- const ssr = transformOptions?.ssr === true || this.environment?.name === "ssr";
584
+ // Both server graphs: neither gets a refresh wrapper, and the rsc graph's
585
+ // findings are reported as that graph's.
586
+ const rsc = this.environment?.name === RSC_ENVIRONMENT;
587
+ const ssr = transformOptions?.ssr === true || this.environment?.name === "ssr" || rsc;
451
588
  const refresh = !isProduction && !ssr && server != null;
452
589
  const out = await ensureService().transform(cleanId(id), code, {
453
590
  development: !isProduction,
@@ -459,7 +596,7 @@ function flowPlugin({
459
596
  id: cleanId(id),
460
597
  root,
461
598
  diagnostics: out.diagnostics,
462
- environment: ssr ? "ssr" : "client",
599
+ environment: rsc ? RSC_ENVIRONMENT : ssr ? "ssr" : "client",
463
600
  reported,
464
601
  suppressed,
465
602
  });
@@ -546,8 +683,27 @@ function flowPlugin({
546
683
  return tags;
547
684
  },
548
685
 
686
+ // An edit to a server component changes what the rsc graph renders and no
687
+ // module the browser holds, so Vite has nothing to tell the browser. It is
688
+ // reloaded, which renders the edit; an edit to a client module — which the
689
+ // rsc graph only holds references to — is left to Fast Refresh.
690
+ hotUpdate({ modules }) {
691
+ if (flightState == null || server == null) return;
692
+ if (this.environment?.name !== RSC_ENVIRONMENT) return;
693
+ const serverSide = modules.some((module) => {
694
+ const file = module.file ?? cleanId(module.id ?? "");
695
+ return file !== "" && !flightState.clientModules.has(file);
696
+ });
697
+ if (serverSide) server.environments.client.hot.send({ type: "full-reload", path: "*" });
698
+ },
699
+
549
700
  configureServer(devServer) {
550
701
  server = devServer;
702
+ // The ssr graph's way into the rsc graph; see `devBridgeSource`.
703
+ if (flightState != null) {
704
+ globalThis[Symbol.for(DEV_RSC_HOOK)] = () =>
705
+ devServer.environments[RSC_ENVIRONMENT].runner.import(FLIGHT_VIRTUAL.entry);
706
+ }
551
707
  devServer.httpServer?.once("close", () => {
552
708
  service?.close();
553
709
  service = null;
@@ -713,6 +869,29 @@ function flowPlugin({
713
869
  // and a `<form action>` navigation both send `Accept:
714
870
  // text/html`, and both want the handler's answer — which is the
715
871
  // whole of ubugeeei-prod/uf#349.
872
+ // A browser that is navigating, asking for the next route's
873
+ // payload: after the guard and before the handlers, for the
874
+ // reasons `@uniflowed/server`'s `internal/flight.js` gives.
875
+ const payloadFor =
876
+ entry.flight == null ? null : flightDocumentPath(url.split("?")[0]);
877
+ if (payloadFor != null && (request.method === "GET" || request.method === "HEAD")) {
878
+ const query = url.includes("?") ? url.slice(url.indexOf("?")) : "";
879
+ const target = payloadFor + query;
880
+ const answered = await entry.flight(target, {
881
+ onError: (error) => reportRenderError(devServer, target, error),
882
+ });
883
+ if (answered.error != null) reportRenderError(devServer, target, answered.error);
884
+ if (request.method === "HEAD") await answered.stream?.cancel();
885
+ await send(
886
+ response,
887
+ new Response(request.method === "HEAD" ? null : answered.stream, {
888
+ status: answered.status,
889
+ headers: answered.headers,
890
+ }),
891
+ );
892
+ return true;
893
+ }
894
+
716
895
  const handled = await entry.dispatch(asRequest);
717
896
  if (handled != null) {
718
897
  await send(response, handled);
@@ -778,7 +957,18 @@ function flowPlugin({
778
957
  onError: (error) => reportRenderError(devServer, url, error),
779
958
  // Vite sees the head and only the head. That is what lets the
780
959
  // development server stream like every other host — see below.
781
- transformHead: (head) => devServer.transformIndexHtml(url, head),
960
+ //
961
+ // Under React Server Components it is also where a server
962
+ // component's stylesheets are linked in development: they are
963
+ // in the rsc graph, which the browser never loads, and they
964
+ // enter it when the route's modules are imported — after this
965
+ // call and before the head is written, which is when this
966
+ // runs. See `devStylesheets`.
967
+ transformHead: (head) =>
968
+ devServer.transformIndexHtml(
969
+ url,
970
+ flightState == null ? head : linkStylesheets(head, devStylesheets(devServer)),
971
+ ),
782
972
  // And what the streaming actually did, when it changed. The
783
973
  // router has already decided there is something worth saying
784
974
  // and written the words — see its `internal/inspector.js`,
@@ -862,11 +1052,7 @@ function mdxPlugin(markdown) {
862
1052
  enforce: "pre",
863
1053
  ...mdx({
864
1054
  jsxImportSource: "react",
865
- remarkPlugins: [
866
- remarkGfm,
867
- remarkFrontmatter,
868
- [remarkMdxFrontmatter, { name: "frontmatter" }],
869
- ],
1055
+ remarkPlugins: [remarkGfm, remarkFrontmatter, remarkFrontmatterExport],
870
1056
  rehypePlugins,
871
1057
  }),
872
1058
  name: "uf:mdx",