@uniflowed/vite 0.0.0-alpha.9 → 0.2.0

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.
@@ -435,7 +435,7 @@ export function markLines(lines) {
435
435
  *
436
436
  * Exported because it is where the decision is visible without starting Shiki:
437
437
  * give it the token split a grammar would produce and it says which words it
438
- * marked. `tests/library/highlight.test.js` uses exactly that, and the splits
438
+ * marked. `packages/vite/highlight.test.js` uses exactly that, and the splits
439
439
  * in it are the ones that had bugs.
440
440
  */
441
441
  export function markLine(line) {
@@ -0,0 +1,33 @@
1
+ // @noflow
2
+ //
3
+ // A document's YAML front matter, as `export const frontmatter`.
4
+ //
5
+ // Plain JavaScript, for the reason `index.js` gives: Vite imports this before
6
+ // any transform runs.
7
+ //
8
+ // This is `remark-mdx-frontmatter` for the one format uf reads, and it replaces
9
+ // that package for one reason. The package imported `toml` 3.0.0 at the top of
10
+ // its module whether or not a document held any TOML, so every project uf
11
+ // scaffolded installed a parser with two high advisories and failed its first
12
+ // `uf audit` (#1009). The TOML half was never reachable from here either:
13
+ // `remark-frontmatter` is given its default, which recognises YAML and nothing
14
+ // else, so a `+++` block was never a front-matter node to parse.
15
+ //
16
+ // For YAML the output is the package's, through the same two helpers it used:
17
+ // the first `yaml` node is parsed and defined as `frontmatter`, and a document
18
+ // with none exports `undefined`.
19
+
20
+ import { valueToEstree } from "estree-util-value-to-estree";
21
+ import { define } from "unist-util-mdx-define";
22
+ import { parse } from "yaml";
23
+
24
+ /** The remark plugin: `export const frontmatter` from a document's YAML. */
25
+ export default function remarkFrontmatterExport() {
26
+ return (tree, file) => {
27
+ const node = tree.children.find((child) => child.type === "yaml");
28
+ const data = node == null ? undefined : parse(node.value);
29
+ define(tree, file, {
30
+ frontmatter: valueToEstree(data, { preserveReferences: true }),
31
+ });
32
+ };
33
+ }
package/internal/http.js CHANGED
@@ -25,13 +25,18 @@
25
25
  * that accepts an upload should not need the whole thing buffered before it
26
26
  * starts.
27
27
  *
28
+ * `path` names the path and query to build it at, when that is not the one the
29
+ * request line carried — `uf dev` passes the one Vite's base middleware has
30
+ * already taken `app.router.basePath` off.
31
+ *
28
32
  * @param {import("node:http").IncomingMessage} incoming
29
33
  * @param {{server?: {https?: unknown}} | undefined} config the resolved Vite config
34
+ * @param {string} [path]
30
35
  */
31
- export async function toRequest(incoming, config) {
36
+ export async function toRequest(incoming, config, path) {
32
37
  const host = incoming.headers.host ?? "localhost";
33
38
  const protocol = config?.server?.https == null ? "http" : "https";
34
- const url = new URL(incoming.originalUrl ?? incoming.url ?? "/", `${protocol}://${host}`);
39
+ const url = new URL(path ?? incoming.originalUrl ?? incoming.url ?? "/", `${protocol}://${host}`);
35
40
 
36
41
  const headers = new Headers();
37
42
  for (const [name, value] of Object.entries(incoming.headers)) {
@@ -52,6 +57,20 @@ export async function toRequest(incoming, config) {
52
57
  return new Request(url, init);
53
58
  }
54
59
 
60
+ /**
61
+ * A Node request as a `Request` that carries its address and nothing else.
62
+ *
63
+ * For the questions asked about a URL alone — `app.router`'s redirects and
64
+ * headers, in front of Vite's own middleware — which must not wrap the body a
65
+ * later middleware turns into the `Request` the application reads.
66
+ *
67
+ * @param {import("node:http").IncomingMessage} incoming
68
+ */
69
+ export function toAddressRequest(incoming) {
70
+ const host = incoming.headers.host ?? "localhost";
71
+ return new Request(new URL(incoming.originalUrl ?? incoming.url ?? "/", `http://${host}`));
72
+ }
73
+
55
74
  /**
56
75
  * Write a `Response` to a Node response.
57
76
  *
@@ -0,0 +1,118 @@
1
+ // @noflow
2
+ //
3
+ // Plain JavaScript: executed by the host that serves a build, and by `uf dev`.
4
+ //
5
+ // `/__uf/image` on the three doors `uf` itself runs — `uf dev`, `uf preview`
6
+ // and `uf start` — with `uf` as the encoder.
7
+ //
8
+ // The endpoint is `@uniflowed/server/image`'s, the fetch is
9
+ // `@uniflowed/server/image/node`'s, and this is the one piece those two
10
+ // cannot have: a transformer that reaches the `uf` binary. It writes the
11
+ // bounded source to a private temporary directory, asks `uf assets` for one
12
+ // variant of it (`crates/uf_cli/src/commands/assets.rs`'s `variant` request,
13
+ // which is `uf_assets::variant`), and reads the one file that comes back.
14
+ // Both files are removed before the answer is returned: the cache is the
15
+ // endpoint's `CacheStore`, not this directory.
16
+ //
17
+ // A deployed directory has no `uf` in it, which is why this is not in
18
+ // `@uniflowed/server`. What `--adapter` links instead is argued in
19
+ // `driver.js`'s `imageEndpointSource`.
20
+
21
+ import { randomUUID } from "node:crypto";
22
+ import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
23
+ import os from "node:os";
24
+ import path from "node:path";
25
+
26
+ import { AssetService } from "@uniflowed/host/assets";
27
+
28
+ /**
29
+ * Whether the project asked for an endpoint at all.
30
+ *
31
+ * `app.builtins.images.remotePatterns` with something in it, and images not
32
+ * turned off. Nothing else constructs one: a project that listed no host has
33
+ * no `/__uf/image`, and the path is an ordinary 404.
34
+ *
35
+ * @param {{enabled?: boolean, remotePatterns?: unknown[]} | undefined} images
36
+ */
37
+ export function servesRemoteImages(images) {
38
+ return (
39
+ images?.enabled !== false &&
40
+ Array.isArray(images?.remotePatterns) &&
41
+ images.remotePatterns.length > 0
42
+ );
43
+ }
44
+
45
+ /**
46
+ * The endpoint's options from `app.builtins.images`, with uf's defaults where
47
+ * the project said nothing — the same defaults `uf_assets::ImagesConfig`
48
+ * applies, from `@uniflowed/server/image` where both sides can read them.
49
+ *
50
+ * @param {object} images `app.builtins.images`
51
+ * @param {{DEFAULT_WIDTHS: number[], DEFAULT_QUALITY: number}} defaults
52
+ */
53
+ export function endpointSettings(images, defaults) {
54
+ return {
55
+ remotePatterns: images.remotePatterns,
56
+ widths: images.widths ?? defaults.DEFAULT_WIDTHS,
57
+ quality: images.quality ?? defaults.DEFAULT_QUALITY,
58
+ qualities: images.qualities ?? [],
59
+ };
60
+ }
61
+
62
+ /**
63
+ * An encoder that is `uf` itself.
64
+ *
65
+ * One `uf assets` process for the life of the server, started on the first
66
+ * miss and not before, and one temporary directory beside it.
67
+ *
68
+ * @param {{root: string, command?: string}} options
69
+ */
70
+ export function ufImageTransform({ root, command }) {
71
+ let service = null;
72
+ let directory = null;
73
+ return async function transform({ bytes, width, quality, avif }) {
74
+ service ??= new AssetService({ command, root });
75
+ directory ??= mkdtemp(path.join(os.tmpdir(), "uf-image-"));
76
+ const base = path.join(await directory, randomUUID());
77
+ const source = `${base}.source`;
78
+ const out = `${base}.out`;
79
+ await writeFile(source, bytes);
80
+ try {
81
+ const variant = await service.variant(source, { outDir: out, width, quality, avif });
82
+ const encoded = await readFile(path.join(out, variant.file));
83
+ return {
84
+ bytes: new Uint8Array(encoded.buffer, encoded.byteOffset, encoded.byteLength),
85
+ type: variant.mime,
86
+ };
87
+ } finally {
88
+ await rm(source, { force: true });
89
+ await rm(out, { recursive: true, force: true });
90
+ }
91
+ };
92
+ }
93
+
94
+ /**
95
+ * `/__uf/image` for a door `uf` runs, or `undefined` for a project with no
96
+ * remote images.
97
+ *
98
+ * `store` is the `CacheStore` a durable `rendering.cache.store` gave the route
99
+ * cache's provider to, when there is one; without it the endpoint keeps its
100
+ * variants in memory.
101
+ *
102
+ * @param {{images?: object, root: string, command?: string, store?: object}} options
103
+ */
104
+ export async function localImageEndpoint({ images, root, command, store }) {
105
+ if (!servesRemoteImages(images)) return undefined;
106
+ const [endpoint, node] = await Promise.all([
107
+ import("@uniflowed/server/image"),
108
+ import("@uniflowed/server/image/node"),
109
+ ]);
110
+ return endpoint.createImageEndpoint({
111
+ ...endpointSettings(images, endpoint),
112
+ fetch: node.nodeImageFetch({
113
+ allowPrivateAddresses: images.dangerouslyAllowPrivateAddresses === true,
114
+ }),
115
+ transform: ufImageTransform({ root, command }),
116
+ ...(store == null ? {} : { store }),
117
+ });
118
+ }
@@ -0,0 +1,23 @@
1
+ // @noflow
2
+
3
+ import { existsSync } from "node:fs";
4
+ import path from "node:path";
5
+
6
+ export function instrumentationFile(root, client = false) {
7
+ const stem = client ? "$instrumentation.client" : "$instrumentation";
8
+ const files = [".js", ".jsx"]
9
+ .map((extension) => path.join(root, stem + extension))
10
+ .filter(existsSync);
11
+ if (files.length > 1)
12
+ throw new Error(`uf: choose one instrumentation module: ${files.join(", ")}`);
13
+ return files[0] ?? null;
14
+ }
15
+
16
+ export function clientInstrumentationSource(file) {
17
+ if (file == null) return "";
18
+ return `import * as instrumentation from ${JSON.stringify(file)};
19
+ import { installClientInstrumentation } from "@uniflowed/router/instrumentation";
20
+ const disposeInstrumentation = await installClientInstrumentation(instrumentation);
21
+ if (import.meta.hot) import.meta.hot.dispose(disposeInstrumentation);
22
+ `;
23
+ }
@@ -0,0 +1,134 @@
1
+ // @noflow
2
+ //
3
+ // Plain JavaScript: the driver imports it, and the driver runs before any Flow
4
+ // loader exists.
5
+ //
6
+ // The module graph `uf build --analyze` reads: every module each bundle was
7
+ // built from, what it imports, and which chunk its rendered code went into.
8
+ //
9
+ // What the graph means — which route a module belongs to, the chain of imports
10
+ // behind it, what it weighs — is uf's, in `crates/uf_bundle/src/analysis.rs`.
11
+ // This file writes down only what the bundler knows and uf cannot, and that
12
+ // split is the reason the graph is a file at all: a builder other than this
13
+ // one that writes the same file gets the same analysis.
14
+ //
15
+ // {
16
+ // "version": 1,
17
+ // "builds": [{
18
+ // "environment": "client",
19
+ // "entries": ["virtual:uf/client"],
20
+ // "modules": [{ "id": "app/$page.js", "imports": ["app/Counter.js"], "dynamicImports": [] }],
21
+ // "chunks": [{
22
+ // "file": "assets/index-3f2a.js",
23
+ // "facade": "virtual:uf/client",
24
+ // "modules": [{ "id": "virtual:uf/client", "code": "…" }]
25
+ // }]
26
+ // }]
27
+ // }
28
+
29
+ import { mkdirSync, writeFileSync } from "node:fs";
30
+ import path from "node:path";
31
+
32
+ /** Where the driver writes the graph, in the build's metadata directory. */
33
+ export const MODULE_GRAPH_FILE = "uf-module-graph.json";
34
+
35
+ const VERSION = 1;
36
+
37
+ /**
38
+ * A plugin that records the graph of every bundle it takes part in, and the
39
+ * means to write what it recorded.
40
+ *
41
+ * `isReference(file)` says whether an absolute file is a client reference: an
42
+ * entry the browser loads because a server component named it, rather than on
43
+ * every page. Its chunk still carries it as the facade, and it is left out of
44
+ * `entries`, which is the list of what every page of a bundle loads.
45
+ */
46
+ export function createModuleGraphCollector(root, { isReference = () => false } = {}) {
47
+ const builds = [];
48
+ const graph = () => ({ version: VERSION, builds });
49
+ return {
50
+ plugin: {
51
+ name: "uf:module-graph",
52
+ // Last, so the bundle it reads is one every other plugin has finished.
53
+ enforce: "post",
54
+ generateBundle(_options, bundle) {
55
+ builds.push(describeBundle(this, root, bundle, isReference));
56
+ },
57
+ },
58
+ graph,
59
+ write(file) {
60
+ mkdirSync(path.dirname(file), { recursive: true });
61
+ writeFileSync(file, `${JSON.stringify(graph())}\n`);
62
+ },
63
+ };
64
+ }
65
+
66
+ /** One bundle, as `generateBundle` sees it. */
67
+ function describeBundle(context, root, bundle, isReference) {
68
+ const ids = typeof context.getModuleIds === "function" ? [...context.getModuleIds()] : [];
69
+ const modules = ids
70
+ .map((id) => {
71
+ const info = context.getModuleInfo(id);
72
+ return {
73
+ id: moduleId(root, id),
74
+ imports: (info?.importedIds ?? []).map((imported) => moduleId(root, imported)),
75
+ dynamicImports: (info?.dynamicallyImportedIds ?? []).map((imported) =>
76
+ moduleId(root, imported),
77
+ ),
78
+ };
79
+ })
80
+ .sort(byKey("id"));
81
+ const entries = new Set();
82
+ const chunks = [];
83
+ for (const output of Object.values(bundle)) {
84
+ if (output.type !== "chunk") {
85
+ continue;
86
+ }
87
+ const facade = output.isEntry ? (output.facadeModuleId ?? null) : null;
88
+ if (facade != null && !isReference(facade.split("?")[0])) {
89
+ entries.add(moduleId(root, facade));
90
+ }
91
+ chunks.push({
92
+ file: output.fileName,
93
+ facade: facade == null ? null : moduleId(root, facade),
94
+ // A module the bundler left no code for was still walked through to
95
+ // reach what it imports, so it stays in `modules` above; it has nothing
96
+ // to weigh, so it is not in the chunk.
97
+ modules: Object.entries(output.modules ?? {})
98
+ .map(([id, rendered]) => ({ id: moduleId(root, id), code: rendered?.code ?? "" }))
99
+ .filter((module) => module.code !== ""),
100
+ });
101
+ }
102
+ return {
103
+ environment: context.environment?.name ?? "client",
104
+ entries: [...entries].sort(),
105
+ modules,
106
+ chunks: chunks.sort(byKey("file")),
107
+ };
108
+ }
109
+
110
+ /**
111
+ * A module id as the analysis spells it: relative to the project root with
112
+ * `/`, without the `\0` a resolved virtual module carries, and with its query.
113
+ *
114
+ * Relative even when it climbs out of the root, as a workspace package does,
115
+ * because an absolute path would make two machines' reports of the same build
116
+ * disagree.
117
+ */
118
+ export function moduleId(root, id) {
119
+ const bare = id.startsWith("\0") ? id.slice(1) : id;
120
+ const at = bare.indexOf("?");
121
+ const file = at === -1 ? bare : bare.slice(0, at);
122
+ if (!path.isAbsolute(file)) {
123
+ return bare;
124
+ }
125
+ const relative = path.relative(root, file);
126
+ if (path.isAbsolute(relative)) {
127
+ return bare;
128
+ }
129
+ return `${relative.split(path.sep).join("/")}${at === -1 ? "" : bare.slice(at)}`;
130
+ }
131
+
132
+ function byKey(key) {
133
+ return (a, b) => (a[key] < b[key] ? -1 : a[key] > b[key] ? 1 : 0);
134
+ }
@@ -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,218 @@
1
+ // @noflow
2
+ //
3
+ // Route-handler OpenAPI output.
4
+ //
5
+ // The build driver imports this module before it registers the Node Flow
6
+ // hooks, so it must stay plain JavaScript and must not statically import
7
+ // Flow-authored packages. The schema and method vocabulary are loaded inside
8
+ // `createOpenApiDocument`, after the driver has installed the hooks.
9
+
10
+ const JSON = "application/json";
11
+ const QUERY_EXTENSION = "x-uf-query";
12
+ const STANDARD_METHODS = new Set(["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"]);
13
+
14
+ export async function createOpenApiDocument(handlers, options = {}) {
15
+ const [{ HANDLER_METHODS }, { toJsonSchema }] = await Promise.all([
16
+ import("@uniflowed/router/handler"),
17
+ import("@uniflowed/validator/json-schema"),
18
+ ]);
19
+ const paths = {};
20
+ for (const record of handlers ?? []) {
21
+ const routePath = openApiPath(record.path);
22
+ const pathItem = paths[routePath] ?? {};
23
+ paths[routePath] = pathItem;
24
+ const module = await loadHandlerModule(record, pathItem);
25
+ if (module == null) continue;
26
+ for (const method of implementedMethods(module, HANDLER_METHODS)) {
27
+ const operation = createOperation(record, method, schemaFor(module, method), toJsonSchema);
28
+ if (STANDARD_METHODS.has(method)) {
29
+ pathItem[method.toLowerCase()] = operation;
30
+ } else if (method === "QUERY") {
31
+ pathItem[QUERY_EXTENSION] = operation;
32
+ }
33
+ }
34
+ }
35
+ return {
36
+ openapi: "3.1.0",
37
+ info: {
38
+ title: options.title ?? "uf application",
39
+ version: options.version ?? "0.0.0",
40
+ },
41
+ paths,
42
+ };
43
+ }
44
+
45
+ async function loadHandlerModule(record, pathItem) {
46
+ try {
47
+ return await record.load();
48
+ } catch (error) {
49
+ if (record.file != null) pathItem["x-uf-source"] = record.file;
50
+ pathItem["x-uf-schema-unavailable"] = errorMessage(error);
51
+ return null;
52
+ }
53
+ }
54
+
55
+ function implementedMethods(module, methods) {
56
+ return methods.filter(
57
+ (method) =>
58
+ typeof module[method] === "function" ||
59
+ (method === "HEAD" && typeof module.GET === "function"),
60
+ );
61
+ }
62
+
63
+ function schemaFor(module, method) {
64
+ const table = module.schemas;
65
+ if (!isRecord(table)) return null;
66
+ const schema = table[method];
67
+ if (schema == null) return null;
68
+ if (!isRecord(schema)) {
69
+ throw new Error(`route handler schemas.${method} must be an object`);
70
+ }
71
+ return schema;
72
+ }
73
+
74
+ function createOperation(record, method, schema, toJsonSchema) {
75
+ const parameters = pathParameters(record);
76
+ const operation = {
77
+ operationId: operationId(record, method),
78
+ responses: untypedResponses(method),
79
+ };
80
+ if (record.file != null) operation["x-uf-source"] = record.file;
81
+ if (parameters.length > 0) operation.parameters = parameters;
82
+ if (schema == null) {
83
+ operation["x-uf-untyped"] = true;
84
+ return operation;
85
+ }
86
+
87
+ const unrepresentable = [];
88
+ if (schema.query != null) {
89
+ addQuerySchema(operation, exportSchema(toJsonSchema, schema.query, "query", unrepresentable));
90
+ }
91
+ if (schema.body != null) {
92
+ operation.requestBody = {
93
+ required: true,
94
+ content: {
95
+ [JSON]: { schema: exportSchema(toJsonSchema, schema.body, "body", unrepresentable) },
96
+ },
97
+ };
98
+ }
99
+ if (schema.response != null) {
100
+ operation.responses = {
101
+ "200": {
102
+ description: method === "HEAD" ? "Typed response headers" : "Typed response",
103
+ content:
104
+ method === "HEAD"
105
+ ? undefined
106
+ : {
107
+ [JSON]: {
108
+ schema: exportSchema(toJsonSchema, schema.response, "response", unrepresentable),
109
+ },
110
+ },
111
+ },
112
+ };
113
+ if (method === "HEAD") delete operation.responses["200"].content;
114
+ }
115
+ if (unrepresentable.length > 0) {
116
+ operation["x-uf-unrepresentable"] = unrepresentable;
117
+ }
118
+ return operation;
119
+ }
120
+
121
+ function untypedResponses(method) {
122
+ return {
123
+ "200": {
124
+ description: method === "HEAD" ? "Untyped response headers" : "Untyped response",
125
+ },
126
+ };
127
+ }
128
+
129
+ function exportSchema(toJsonSchema, schema, part, unrepresentable) {
130
+ const exported = toJsonSchema(schema);
131
+ for (const item of exported.unrepresentable) {
132
+ unrepresentable.push({ part, path: item.path, kind: item.kind });
133
+ }
134
+ return stripDialect(exported.schema);
135
+ }
136
+
137
+ function addQuerySchema(operation, schema) {
138
+ if (schema.type !== "object" || !isRecord(schema.properties) || hasOwn(schema, "$defs")) {
139
+ operation["x-uf-query-schema"] = schema;
140
+ return;
141
+ }
142
+ const required = new Set(Array.isArray(schema.required) ? schema.required : []);
143
+ const parameters = Object.keys(schema.properties).map((name) => ({
144
+ name,
145
+ in: "query",
146
+ required: required.has(name),
147
+ schema: schema.properties[name],
148
+ }));
149
+ operation.parameters = [...(operation.parameters ?? []), ...parameters];
150
+ }
151
+
152
+ function stripDialect(value) {
153
+ if (Array.isArray(value)) return value.map(stripDialect);
154
+ if (!isRecord(value)) return value;
155
+ const out = {};
156
+ for (const key of Object.keys(value)) {
157
+ if (key === "$schema") continue;
158
+ out[key] = stripDialect(value[key]);
159
+ }
160
+ return out;
161
+ }
162
+
163
+ function openApiPath(routePath) {
164
+ return routePath
165
+ .split("/")
166
+ .map((segment) => {
167
+ if (!segment.startsWith(":")) return segment;
168
+ return `{${segment.endsWith("*") ? segment.slice(1, -1) : segment.slice(1)}}`;
169
+ })
170
+ .join("/");
171
+ }
172
+
173
+ function pathParameters(record) {
174
+ const params = record.params ?? paramsFromPath(record.path);
175
+ return params.map((param) => {
176
+ const parameter = {
177
+ name: param.name,
178
+ in: "path",
179
+ required: true,
180
+ schema: { type: "string" },
181
+ };
182
+ if (param.catchAll) {
183
+ parameter.description = "Catch-all route segment, slash-separated in the URL.";
184
+ }
185
+ return parameter;
186
+ });
187
+ }
188
+
189
+ function paramsFromPath(routePath) {
190
+ return routePath
191
+ .split("/")
192
+ .filter((segment) => segment.startsWith(":"))
193
+ .map((segment) => ({
194
+ name: segment.endsWith("*") ? segment.slice(1, -1) : segment.slice(1),
195
+ catchAll: segment.endsWith("*"),
196
+ }));
197
+ }
198
+
199
+ function operationId(record, method) {
200
+ const name = `${method.toLowerCase()} ${record.path}`;
201
+ return name
202
+ .replaceAll(/[^a-zA-Z0-9]+/g, " ")
203
+ .trim()
204
+ .replaceAll(/ ([a-zA-Z0-9])/g, (_, char) => char.toUpperCase());
205
+ }
206
+
207
+ function isRecord(value) {
208
+ return value != null && typeof value === "object" && !Array.isArray(value);
209
+ }
210
+
211
+ function hasOwn(value, key) {
212
+ return Object.prototype.hasOwnProperty.call(value, key);
213
+ }
214
+
215
+ function errorMessage(error) {
216
+ if (error instanceof Error) return error.message;
217
+ return String(error);
218
+ }
@@ -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
+ }