@uniflowed/vite 0.0.0-alpha.40 → 0.0.0-alpha.41

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 CHANGED
@@ -524,6 +524,9 @@ async function preview() {
524
524
  if ((request.url ?? "").split("?")[0].endsWith("/__uf.flight")) {
525
525
  response.setHeader("content-type", "text/x-component");
526
526
  }
527
+ if ((request.url ?? "").split("?")[0] === "/.well-known/apple-app-site-association") {
528
+ response.setHeader("content-type", "application/json; charset=utf-8");
529
+ }
527
530
  next();
528
531
  });
529
532
  if (handle == null) return;
@@ -1677,11 +1680,12 @@ async function deploy() {
1677
1680
  function handlerEntrySource(document, cache, capabilities, build, regeneration) {
1678
1681
  const route = cache?.route === true;
1679
1682
  const fetchCache = cache?.fetch === true;
1683
+ const dataCache = cache?.data === true;
1680
1684
  // Nothing at all when both switches are off, so a default project's
1681
1685
  // `handler.js` is the file it has always been. A cache that appears in
1682
1686
  // generated output nobody asked for is the second half of the complaint
1683
1687
  // #277 makes about the first half.
1684
- const store = route || fetchCache;
1688
+ const store = route || fetchCache || dataCache;
1685
1689
  const durable = store ? durableStoreSource(root, cache, build) : null;
1686
1690
  const options = [
1687
1691
  "app",
@@ -1716,7 +1720,7 @@ ${
1716
1720
  } See ubugeeei-prod/uf#277.
1717
1721
  const cache = { store: createCacheStore(${
1718
1722
  durable == null ? "" : `{ provider: ${durable.provider}, build: ${JSON.stringify(build)} }`
1719
- }), route: ${String(route)}, fetch: ${String(fetchCache)} };
1723
+ }), route: ${String(route)}, fetch: ${String(fetchCache)}, data: ${String(dataCache)} };
1720
1724
 
1721
1725
  `
1722
1726
  : ""
package/index.js CHANGED
@@ -44,11 +44,12 @@
44
44
  // `uniflowed(options)` returns the array; a project that wants to add a plugin
45
45
  // declares it in `uf.config.js` and the driver appends it after these.
46
46
 
47
- import { readdirSync } from "node:fs";
48
47
  import path from "node:path";
48
+ import { nativeWebPlugin } from "./internal/native-web.js";
49
49
 
50
50
  import mdx from "@mdx-js/rollup";
51
51
  import rehypeSlug from "rehype-slug";
52
+ import { relayConfig, relayDependencies, transformRelay } from "./internal/relay.js";
52
53
 
53
54
  import {
54
55
  AUDIT_PUBLIC_PATH,
@@ -59,7 +60,7 @@ import {
59
60
  } from "./internal/a11y.js";
60
61
  import { assetPlugin } from "./internal/assets.js";
61
62
  import { barrelImportsPlugin, namespaceViewOf } from "./internal/barrel-imports.js";
62
- import { emit, reportRenderError } from "./internal/events.js";
63
+ import { emit, reportRenderError, errorEvent } from "./internal/events.js";
63
64
  import remarkFrontmatterExport from "./internal/frontmatter.js";
64
65
  import { highlightPlugin } from "./internal/highlight.js";
65
66
  import { moduleId } from "./internal/module-graph.js";
@@ -93,11 +94,12 @@ import {
93
94
  scanRoutes,
94
95
  serverModuleSource,
95
96
  } from "./internal/routes.js";
96
- import { TransformService, isFlowModule } from "@uniflowed/host/transform";
97
+ import { TransformService, isFlowModule, uniflowedPackages } from "@uniflowed/host/transform";
97
98
  import {
98
99
  DEV_RSC_HOOK,
99
100
  FLIGHT_BROWSER_DEPENDENCIES,
100
101
  FLIGHT_VIRTUAL,
102
+ INTERCEPTED_FROM_HEADER,
101
103
  RSC_ENVIRONMENT,
102
104
  builtBridgeSource,
103
105
  builtReferencesSource,
@@ -118,6 +120,7 @@ import {
118
120
  rscEnvironment,
119
121
  } from "./internal/flight.js";
120
122
  import { createChannelMiddleware } from "./internal/diagnostics.js";
123
+ import { startDevState } from "./internal/dev-state.js";
121
124
  import { devtoolsPreamble } from "./internal/devtools.js";
122
125
  import { send, toAddressRequest, toRequest } from "./internal/http.js";
123
126
  import {
@@ -133,6 +136,25 @@ import { serverComponentsProblem } from "./internal/server-components.js";
133
136
  const resolved = (id) => `\0${id}`;
134
137
  const VIRTUAL_IDS = new Set([...Object.values(VIRTUAL), ...Object.values(FLIGHT_VIRTUAL)]);
135
138
 
139
+ /**
140
+ * CommonJS packages the browser can reach only through an installed
141
+ * `@uniflowed/*` package.
142
+ *
143
+ * uf excludes its own packages from Vite's dependency optimizer because they
144
+ * ship Flow, so Vite will not discover a CommonJS peer through them once they
145
+ * live under a project's `node_modules`. React Testing's `happy-dom` edge is
146
+ * ESM and browser-mapped to a local stub, and the optional `axe-core` edge is
147
+ * named separately below only when the project actually installed it.
148
+ */
149
+ const CLIENT_COMMONJS_DEPENDENCIES = Object.freeze([
150
+ "react",
151
+ "react/jsx-runtime",
152
+ "react/jsx-dev-runtime",
153
+ "react/compiler-runtime",
154
+ "react-dom",
155
+ "react-dom/client",
156
+ ]);
157
+
136
158
  /**
137
159
  * Prefix of the virtual module that carries one source module's StyleX rules.
138
160
  *
@@ -229,6 +251,7 @@ export default function uniflowed(options = {}) {
229
251
  routing,
230
252
  command: options.command,
231
253
  accessibility,
254
+ relayEnabled: builtins.relay !== false,
232
255
  }),
233
256
  ...(flightState == null ? [] : [clientReferencePlugin(flightState), clientModuleUrlPlugin()]),
234
257
  // After the references, so a client module the rsc graph has already
@@ -242,6 +265,7 @@ export default function uniflowed(options = {}) {
242
265
  og: builtins.og ?? {},
243
266
  command: options.command,
244
267
  }),
268
+ nativeWebPlugin(routeTarget),
245
269
  ];
246
270
  }
247
271
 
@@ -257,6 +281,7 @@ function flowPlugin({
257
281
  routing,
258
282
  command,
259
283
  accessibility,
284
+ relayEnabled,
260
285
  }) {
261
286
  let root = process.cwd();
262
287
  let isProduction = false;
@@ -276,6 +301,7 @@ function flowPlugin({
276
301
  let server = null;
277
302
  /** @type {TransformService | null} */
278
303
  let service = null;
304
+ let relayOptions = null;
279
305
  /**
280
306
  * Each module's compiled stylesheet, keyed by the virtual id serving it.
281
307
  *
@@ -413,16 +439,12 @@ function flowPlugin({
413
439
  // uf serves HTML itself; there is no index.html to fall back to.
414
440
  appType: "custom",
415
441
  resolve: {
416
- dedupe: ["react", "react-dom"],
442
+ dedupe: ["react", "react-dom", "react-relay", "relay-runtime"],
417
443
  },
418
444
  optimizeDeps: {
419
445
  include: [
420
- "react",
421
- "react/jsx-runtime",
422
- "react/jsx-dev-runtime",
423
- "react/compiler-runtime",
424
- "react-dom",
425
- "react-dom/client",
446
+ ...CLIENT_COMMONJS_DEPENDENCIES,
447
+ ...relayDependencies(projectRoot),
426
448
  // React's Flight client, for an application whose routes render as
427
449
  // Server Components; `FLIGHT_BROWSER_DEPENDENCIES` says why.
428
450
  ...(flightState == null ? [] : FLIGHT_BROWSER_DEPENDENCIES),
@@ -455,6 +477,7 @@ function flowPlugin({
455
477
  : {
456
478
  environments: {
457
479
  [RSC_ENVIRONMENT]: rscEnvironment({
480
+ root: projectRoot,
458
481
  production: env.command === "build",
459
482
  exclude: uniflowedPackages(projectRoot),
460
483
  }),
@@ -657,7 +680,16 @@ function flowPlugin({
657
680
  reported,
658
681
  suppressed,
659
682
  });
660
- const map = out.map == null ? null : JSON.parse(out.map);
683
+ let map = out.map == null ? null : JSON.parse(out.map);
684
+ let output = out.code;
685
+ if (relayEnabled && output.includes("graphql")) {
686
+ relayOptions ??= relayConfig(root);
687
+ const transformed = await transformRelay(output, cleanId(id), map, await relayOptions);
688
+ if (transformed != null) {
689
+ output = transformed.code;
690
+ map = transformed.map;
691
+ }
692
+ }
661
693
  // StyleX. `uf transform` compiled the module's `stylex.create` calls into
662
694
  // class names and handed back the rules they declared; the rules become a
663
695
  // module of their own that this one imports.
@@ -675,7 +707,6 @@ function flowPlugin({
675
707
  // and an absolute name would publish where the machine that built it
676
708
  // keeps its files. A module outside the root climbs out with `../`.
677
709
  const styled = out.css != null && out.css !== "";
678
- let output = out.code;
679
710
  if (styled) {
680
711
  const styleId = `${STYLE_PREFIX}${moduleId(root, cleanId(id))}.css`;
681
712
  styles.set(styleId, out.css);
@@ -763,6 +794,16 @@ function flowPlugin({
763
794
 
764
795
  configureServer(devServer) {
765
796
  server = devServer;
797
+ const diagnostics = startDevState(root, () => ({
798
+ routes: scanRoutes(appRoot, { target: routeTarget }).routes.map((route) => ({
799
+ path: route.path,
800
+ page: route.page,
801
+ layouts: route.layouts,
802
+ })),
803
+ actions: actionTables().table,
804
+ }));
805
+ devServer.middlewares.use(diagnostics.middleware);
806
+ devServer.httpServer?.once("close", diagnostics.close);
766
807
  // The ssr graph's way into the rsc graph; see `devBridgeSource`.
767
808
  if (flightState != null) {
768
809
  globalThis[Symbol.for(DEV_RSC_HOOK)] = () =>
@@ -995,8 +1036,10 @@ function flowPlugin({
995
1036
  if (payloadFor != null && (request.method === "GET" || request.method === "HEAD")) {
996
1037
  const query = url.includes("?") ? url.slice(url.indexOf("?")) : "";
997
1038
  const target = payloadFor + query;
1039
+ const fromHeader = request.headers[INTERCEPTED_FROM_HEADER];
998
1040
  const answered = await entry.flight(target, {
999
1041
  onError: (error) => reportRenderError(devServer, target, error),
1042
+ interceptedFrom: Array.isArray(fromHeader) ? fromHeader[0] : fromHeader,
1000
1043
  });
1001
1044
  if (answered.error != null) reportRenderError(devServer, target, answered.error);
1002
1045
  if (request.method === "HEAD") await answered.stream?.cancel();
@@ -1148,6 +1191,12 @@ function flowPlugin({
1148
1191
  // Map the stack back onto the Flow source before it reaches the
1149
1192
  // overlay.
1150
1193
  if (error instanceof Error) devServer.ssrFixStacktrace(error);
1194
+ emit("diagnostic", {
1195
+ severity: "error",
1196
+ kind: "runtime",
1197
+ origin: url,
1198
+ ...errorEvent(error),
1199
+ });
1151
1200
  next(error);
1152
1201
  }
1153
1202
  });
@@ -1420,24 +1469,3 @@ function cleanId(id) {
1420
1469
  const at = id.indexOf("?");
1421
1470
  return at === -1 ? id : id.slice(0, at);
1422
1471
  }
1423
-
1424
- /**
1425
- * Every `@uniflowed/*` package the project can resolve, for
1426
- * `optimizeDeps.exclude`, which takes names rather than patterns.
1427
- */
1428
- function uniflowedPackages(root) {
1429
- const names = new Set();
1430
- let directory = root;
1431
- for (let depth = 0; depth < 16; depth += 1) {
1432
- const scope = path.join(directory, "node_modules", "@uniflowed");
1433
- try {
1434
- for (const entry of readdirSync(scope)) names.add(`@uniflowed/${entry}`);
1435
- } catch {
1436
- // no packages at this level
1437
- }
1438
- const parent = path.dirname(directory);
1439
- if (parent === directory) break;
1440
- directory = parent;
1441
- }
1442
- return [...names].sort();
1443
- }
@@ -0,0 +1,161 @@
1
+ // @noflow
2
+ // A local, bounded snapshot of the same diagnostic channel the terminal reads.
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import { randomUUID } from "node:crypto";
6
+ import { AsyncLocalStorage } from "node:async_hooks";
7
+ import { format } from "node:util";
8
+
9
+ const requests = new AsyncLocalStorage();
10
+ let receive = null;
11
+
12
+ export function recordDevEvent(event) {
13
+ const request = requests.getStore();
14
+ receive?.({ ...event, ...(request == null ? {} : request), time: Date.now() });
15
+ }
16
+
17
+ export function startDevState(root, metadata) {
18
+ const directory = path.join(root, ".uf");
19
+ if (fs.existsSync(directory) && fs.lstatSync(directory).isSymbolicLink())
20
+ throw new Error("uf dev: .uf must not be a symlink for diagnostics");
21
+ fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
22
+ const file = path.join(directory, "dev-state.json");
23
+ if (fs.existsSync(file) && fs.lstatSync(file).isSymbolicLink())
24
+ throw new Error("uf dev: diagnostic state must not be a symlink");
25
+ const session = randomUUID();
26
+ const temporary = path.join(directory, "dev-state-" + session + ".tmp");
27
+ const state = {
28
+ schema: 1,
29
+ session,
30
+ pid: process.pid,
31
+ startedAt: Date.now(),
32
+ updatedAt: 0,
33
+ generation: 0,
34
+ errors: [],
35
+ logs: [],
36
+ routes: [],
37
+ actions: [],
38
+ };
39
+ let closed = false;
40
+ const persist = (refresh = false) => {
41
+ if (closed) return;
42
+ if (refresh || state.updatedAt === 0) {
43
+ let current;
44
+ try {
45
+ current = metadata();
46
+ state.metadataError = null;
47
+ } catch (error) {
48
+ state.metadataError = String(error.message ?? error).slice(0, 4000);
49
+ current = { routes: state.routes, actions: state.actions };
50
+ }
51
+ state.routes = current.routes.slice(0, 1000);
52
+ state.actions = current.actions.slice(0, 1000);
53
+ state.truncated = current.routes.length > 1000 || current.actions.length > 1000;
54
+ }
55
+ state.updatedAt = Date.now();
56
+ let serialized = JSON.stringify(state);
57
+ while (Buffer.byteLength(serialized) > 3 * 1024 * 1024) {
58
+ const rows = state.logs.length
59
+ ? state.logs
60
+ : state.errors.length
61
+ ? state.errors
62
+ : state.routes.length
63
+ ? state.routes
64
+ : state.actions;
65
+ if (rows.length === 0) break;
66
+ rows.shift();
67
+ state.truncated = true;
68
+ serialized = JSON.stringify(state);
69
+ }
70
+ fs.writeFileSync(temporary, serialized, { mode: 0o600 });
71
+ fs.renameSync(temporary, file);
72
+ };
73
+ const observer = (event) => {
74
+ const safe = bound(event);
75
+ if (event.event === "source-changed") {
76
+ state.generation += 1;
77
+ state.errors = [];
78
+ }
79
+ if (
80
+ event.event === "error" ||
81
+ (event.event === "log" && event.level === "error") ||
82
+ (event.event === "diagnostic" && event.severity === "error")
83
+ ) {
84
+ safe.kind ??= /hydrat/i.test(event.message ?? "")
85
+ ? "hydration"
86
+ : event.event === "diagnostic"
87
+ ? "runtime"
88
+ : "build";
89
+ state.errors.push(safe);
90
+ state.errors = state.errors.slice(-100);
91
+ }
92
+ if (["log", "diagnostic", "error", "request"].includes(event.event)) {
93
+ state.logs.push(safe);
94
+ state.logs = state.logs.slice(-150);
95
+ }
96
+ persist(event.event === "source-changed");
97
+ };
98
+ receive = observer;
99
+ persist();
100
+ // The terminal sees application console output too; keep its original behavior.
101
+ const originals = new Map();
102
+ for (const level of ["log", "info", "warn", "error", "debug"]) {
103
+ const original = console[level];
104
+ const wrapper = (...args) => {
105
+ recordDevEvent({ event: "log", level, kind: "runtime", message: format(...args) });
106
+ original.apply(console, args);
107
+ };
108
+ originals.set(level, { original, wrapper });
109
+ console[level] = wrapper;
110
+ }
111
+ const timer = setInterval(() => {
112
+ try {
113
+ persist(true);
114
+ } catch {
115
+ close();
116
+ }
117
+ }, 5000);
118
+ timer.unref?.();
119
+ const close = () => {
120
+ if (closed) return;
121
+ closed = true;
122
+ clearInterval(timer);
123
+ if (receive === observer) receive = null;
124
+ for (const [level, { original, wrapper }] of originals) {
125
+ if (console[level] === wrapper) console[level] = original;
126
+ }
127
+ try {
128
+ if (JSON.parse(fs.readFileSync(file, "utf8")).session === session) fs.unlinkSync(file);
129
+ } catch {
130
+ /* A missing or unreadable channel is reported by the MCP reader. */
131
+ }
132
+ process.off("exit", close);
133
+ };
134
+ process.once("exit", close);
135
+ return {
136
+ close,
137
+ middleware(request, response, next) {
138
+ const requestId = randomUUID();
139
+ const url = String(request.url ?? "/")
140
+ .split("?")[0]
141
+ .slice(0, 1000);
142
+ response.setHeader("x-uf-request-id", requestId);
143
+ requests.run({ requestId, url }, () => {
144
+ observer({ event: "request", requestId, url, method: request.method, time: Date.now() });
145
+ next();
146
+ });
147
+ },
148
+ };
149
+ }
150
+
151
+ function bound(value, depth = 0) {
152
+ if (typeof value === "string") return value.slice(0, 4000);
153
+ if (value == null || typeof value !== "object") return value;
154
+ if (depth > 4) return "[truncated]";
155
+ if (Array.isArray(value)) return value.slice(0, 40).map((v) => bound(v, depth + 1));
156
+ return Object.fromEntries(
157
+ Object.entries(value)
158
+ .slice(0, 32)
159
+ .map(([key, v]) => [key, bound(v, depth + 1)]),
160
+ );
161
+ }
@@ -11,6 +11,7 @@
11
11
  // logger, which is redirected here as well so nothing bypasses the channel.
12
12
 
13
13
  import { createLogger } from "vite";
14
+ import { recordDevEvent } from "./dev-state.js";
14
15
 
15
16
  /**
16
17
  * Emit one event.
@@ -19,6 +20,7 @@ import { createLogger } from "vite";
19
20
  * `done` is on the pipe before the process exits.
20
21
  */
21
22
  export function emit(event, fields = {}) {
23
+ recordDevEvent({ event, ...fields });
22
24
  process.stdout.write(`${JSON.stringify({ event, ...fields })}\n`);
23
25
  }
24
26
 
@@ -67,6 +69,7 @@ export function reportRenderError(server, url, error) {
67
69
  if (error instanceof Error) {
68
70
  server.ssrFixStacktrace(error);
69
71
  }
72
+ emit("diagnostic", { severity: "error", kind: "runtime", origin: url, ...errorEvent(error) });
70
73
  const detail = error instanceof Error ? (error.stack ?? error.message) : String(error);
71
74
  server.config.logger.error(`${url} rendered its error boundary\n${stripAnsi(detail)}`);
72
75
  }
@@ -50,6 +50,7 @@
50
50
  // route's modules have been imported.
51
51
 
52
52
  import path from "node:path";
53
+ import { relayDependencies } from "./relay.js";
53
54
 
54
55
  import {
55
56
  createRunnableDevEnvironment,
@@ -120,6 +121,9 @@ export const DEV_RSC_HOOK = "uf.dev.rsc";
120
121
  */
121
122
  export const FLIGHT_SEGMENT = "__uf.flight";
122
123
 
124
+ /** The header a browser sends when a Flight payload should render an interception. */
125
+ export const INTERCEPTED_FROM_HEADER = "uf-intercepted-from";
126
+
123
127
  /** The document a payload path is for, or `null` for any other path. */
124
128
  export function flightDocumentPath(pathname) {
125
129
  const suffix = `/${FLIGHT_SEGMENT}`;
@@ -166,7 +170,7 @@ export function rendersFlight(app, { mount, routeTarget }) {
166
170
  *
167
171
  * @param {{ production: boolean, exclude: Array<string> }} options
168
172
  */
169
- export function rscEnvironment({ production, exclude }) {
173
+ export function rscEnvironment({ production, exclude, root = process.cwd() }) {
170
174
  return {
171
175
  consumer: "server",
172
176
  resolve: {
@@ -180,6 +184,7 @@ export function rscEnvironment({ production, exclude }) {
180
184
  "react/jsx-runtime",
181
185
  "react/jsx-dev-runtime",
182
186
  "react-server-dom-parcel/server",
187
+ ...relayDependencies(root, true),
183
188
  ],
184
189
  exclude,
185
190
  },
@@ -731,7 +736,7 @@ export function devStylesheets(server) {
731
736
  // package's, which Vite serves under `/@fs/` — and the same rule a client
732
737
  // reference's URL follows. A stylesheet with no file is a virtual one.
733
738
  urls.push(
734
- typeof module.file === "string" && module.file !== ""
739
+ typeof module.file === "string" && path.isAbsolute(module.file)
735
740
  ? devUrlOf(root, base, module.file)
736
741
  : `${base.replace(/\/$/, "")}/@id/${id.replace(/\0/g, "__x00__")}`,
737
742
  );
@@ -0,0 +1,26 @@
1
+ // @noflow
2
+
3
+ import { createRequire } from "node:module";
4
+ import path from "node:path";
5
+
6
+ /** React Native Web is supplied by the application, including its version. */
7
+ export function nativeWebPlugin(target) {
8
+ let root = process.cwd();
9
+ return {
10
+ name: "uf:react-native-web",
11
+ enforce: "pre",
12
+ configResolved(config) {
13
+ root = config.root;
14
+ },
15
+ resolveId(source) {
16
+ if (target !== "web" || source !== "react-native") return null;
17
+ try {
18
+ return createRequire(path.join(root, "package.json")).resolve("react-native-web");
19
+ } catch {
20
+ throw new Error(
21
+ "A shared React Native page needs the application's react-native-web dependency: run `uf add react-native-web`.",
22
+ );
23
+ }
24
+ },
25
+ };
26
+ }
@@ -0,0 +1,74 @@
1
+ // @noflow
2
+ import { createRequire } from "node:module";
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import { pathToFileURL } from "node:url";
6
+
7
+ /** CommonJS hidden behind uf's excluded Flow packages still needs prebundling. */
8
+ export function relayDependencies(root, server = false) {
9
+ const require = createRequire(path.join(root, "package.json"));
10
+ const entries = server
11
+ ? ["relay-runtime", "relay-runtime/experimental", "react-relay/rsc_EXPERIMENTAL.js"]
12
+ : ["relay-runtime", "react-relay", "react-relay/rsc-client_EXPERIMENTAL.js"];
13
+ return entries.filter((entry) => {
14
+ try {
15
+ require.resolve(entry);
16
+ return true;
17
+ } catch (error) {
18
+ if (error.code === "MODULE_NOT_FOUND" || error.code === "ERR_PACKAGE_PATH_NOT_EXPORTED")
19
+ return false;
20
+ throw error;
21
+ }
22
+ });
23
+ }
24
+
25
+ /** Resolve config from the application, independently of the process's cwd. */
26
+ export async function relayConfig(root) {
27
+ for (const name of ["relay.config.json", "relay.config.js", "relay.config.mjs"]) {
28
+ const filename = path.join(root, name);
29
+ if (!fs.existsSync(filename)) continue;
30
+ const config = name.endsWith(".json")
31
+ ? JSON.parse(fs.readFileSync(filename, "utf8"))
32
+ : (await import(pathToFileURL(filename).href)).default;
33
+ return artifactOptions(root, config);
34
+ }
35
+ const filename = path.join(root, "package.json");
36
+ const config = fs.existsSync(filename)
37
+ ? JSON.parse(fs.readFileSync(filename, "utf8")).relay
38
+ : null;
39
+ return artifactOptions(root, config);
40
+ }
41
+
42
+ function artifactOptions(root, config) {
43
+ if (config?.projects != null) {
44
+ throw new Error("uf's Relay integration currently needs a single-project Relay config.");
45
+ }
46
+ return {
47
+ artifactDirectory:
48
+ config?.artifactDirectory == null ? null : path.resolve(root, config.artifactDirectory),
49
+ eagerEsModules: true,
50
+ jsModuleFormat: "commonjs",
51
+ codegenCommand: "uf run relay",
52
+ };
53
+ }
54
+
55
+ /** Flow has already lowered components/hooks; Relay owns the GraphQL transform. */
56
+ export async function transformRelay(code, filename, map, options) {
57
+ if (
58
+ !code.includes("graphql") ||
59
+ !/["'](?:@uniflowed\/relay|react-relay|relay-runtime)["']/.test(code)
60
+ )
61
+ return null;
62
+ const [{ transformAsync }, { default: relay }] = await Promise.all([
63
+ import("@babel/core"),
64
+ import("babel-plugin-relay"),
65
+ ]);
66
+ return transformAsync(code, {
67
+ filename,
68
+ babelrc: false,
69
+ configFile: false,
70
+ sourceMaps: true,
71
+ inputSourceMap: map ?? undefined,
72
+ plugins: [[relay, options]],
73
+ });
74
+ }
package/internal/serve.js CHANGED
@@ -112,18 +112,19 @@ function deployment() {
112
112
  * a setting nobody can set would be the shape of configurability with none of
113
113
  * the substance.
114
114
  *
115
- * @param {{route?: boolean, fetch?: boolean, store?: string, storeDir?: string} | undefined} declared
115
+ * @param {{route?: boolean, fetch?: boolean, data?: boolean, store?: string, storeDir?: string} | undefined} declared
116
116
  * @param {(options?: object) => object} createCacheStore
117
117
  * @param {{root: string, build: string | null}} where
118
118
  */
119
119
  async function cacheFor(declared, createCacheStore, where) {
120
120
  const route = declared?.route === true;
121
121
  const fetchCache = declared?.fetch === true;
122
- if (!route && !fetchCache) return undefined;
122
+ const data = declared?.data === true;
123
+ if (!route && !fetchCache && !data) return undefined;
123
124
  const provider = await providerFor(declared, where);
124
125
  const store =
125
126
  provider == null ? createCacheStore() : createCacheStore({ provider, build: where.build });
126
- return { store, route, fetch: fetchCache };
127
+ return { store, route, fetch: fetchCache, data };
127
128
  }
128
129
 
129
130
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniflowed/vite",
3
- "version": "0.0.0-alpha.40",
3
+ "version": "0.0.0-alpha.41",
4
4
  "description": "Vite, driven by uf.config.js: every Flow module through `uf transform`, MDX, the file-system router and static rendering as Vite plugins.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -32,12 +32,14 @@
32
32
  "!*.test.js"
33
33
  ],
34
34
  "dependencies": {
35
+ "@babel/core": "^7.29.7",
35
36
  "@mdx-js/rollup": "^3.1.1",
36
37
  "@shikijs/rehype": "^3.23.0",
37
- "@uniflowed/host": "0.0.0-alpha.40",
38
- "@uniflowed/router": "0.0.0-alpha.40",
39
- "@uniflowed/server": "0.0.0-alpha.40",
40
- "@uniflowed/validator": "0.0.0-alpha.40",
38
+ "@uniflowed/host": "0.0.0-alpha.41",
39
+ "@uniflowed/router": "0.0.0-alpha.41",
40
+ "@uniflowed/server": "0.0.0-alpha.41",
41
+ "@uniflowed/validator": "0.0.0-alpha.41",
42
+ "babel-plugin-relay": "^21.0.1",
41
43
  "estree-util-value-to-estree": "^3.5.0",
42
44
  "rehype-slug": "^6.0.0",
43
45
  "remark-frontmatter": "^5.0.0",
@@ -48,9 +50,9 @@
48
50
  "yaml": "^2.9.1"
49
51
  },
50
52
  "peerDependencies": {
53
+ "axe-core": ">=4",
51
54
  "react": ">=19",
52
- "react-dom": ">=19",
53
- "axe-core": ">=4"
55
+ "react-dom": ">=19"
54
56
  },
55
57
  "peerDependenciesMeta": {
56
58
  "axe-core": {