@uniflowed/vite 0.0.0-alpha.1

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.
@@ -0,0 +1,110 @@
1
+ // Plain JavaScript: executed by the host that runs Vite, before any transform.
2
+ //
3
+ // React Fast Refresh wiring for `uf dev`. The runtime itself is
4
+ // `./refresh-runtime.js`, Meta's `react-refresh` runtime as simplified by
5
+ // `@vitejs/plugin-react` (MIT); this file is the glue that plugin-react adds
6
+ // around it, kept structurally identical so the two stay comparable:
7
+ //
8
+ // * the runtime is served at `/@react-refresh` as a virtual module;
9
+ // * a preamble installs it on `window` before any component module loads;
10
+ // * every module that registered a component gets a header that points
11
+ // `$RefreshReg$`/`$RefreshSig$` at this module and a footer that validates
12
+ // the boundary and enqueues the refresh on `import.meta.hot.accept`.
13
+
14
+ import { readFileSync } from "node:fs";
15
+ import { fileURLToPath } from "node:url";
16
+
17
+ /** Public URL the refresh runtime is served from. */
18
+ export const RUNTIME_PUBLIC_PATH = "/@react-refresh";
19
+
20
+ /** The resolved id Vite hands back for the runtime. */
21
+ export const RUNTIME_RESOLVED_ID = "\0uf:react-refresh";
22
+
23
+ const RUNTIME_SOURCE_PATH = fileURLToPath(new URL("./refresh-runtime.js", import.meta.url));
24
+
25
+ /** The runtime's source, read once. */
26
+ export function refreshRuntimeSource() {
27
+ return readFileSync(RUNTIME_SOURCE_PATH, "utf8");
28
+ }
29
+
30
+ /**
31
+ * The script every HTML document loads first in development.
32
+ *
33
+ * `base` is Vite's `config.base`, so the runtime resolves under a sub-path
34
+ * deployment as well as at the root.
35
+ */
36
+ export function preambleCode(base = "/") {
37
+ return `import { injectIntoGlobalHook } from "${base}${RUNTIME_PUBLIC_PATH.slice(1)}";
38
+ injectIntoGlobalHook(window);
39
+ window.$RefreshReg$ = () => {};
40
+ window.$RefreshSig$ = () => (type) => type;`;
41
+ }
42
+
43
+ const REFRESH_CONTENT = /\$RefreshReg\$\(/;
44
+ const REACT_CLASS_COMPONENT = /extends\s+(?:React\.)?(?:Pure)?Component/;
45
+
46
+ /**
47
+ * Wrap a transformed module with the Fast Refresh header and footer.
48
+ *
49
+ * A module that registered nothing and defines no class component comes back
50
+ * untouched, so the wrapper never costs a module that has no component in it.
51
+ * The source map is shifted by the number of lines prepended, which is what
52
+ * keeps a stack trace pointing at the author's line.
53
+ */
54
+ export function addRefreshWrapper(code, map, id) {
55
+ const hasRefresh = REFRESH_CONTENT.test(code);
56
+ const onlyReactComponent = !hasRefresh && REACT_CLASS_COMPONENT.test(code);
57
+ if (!hasRefresh && !onlyReactComponent) {
58
+ return { code, map };
59
+ }
60
+
61
+ const nextMap = typeof map === "string" ? JSON.parse(map) : map;
62
+ let nextCode = code;
63
+
64
+ if (hasRefresh) {
65
+ nextCode = `let prevRefreshReg;
66
+ let prevRefreshSig;
67
+
68
+ if (import.meta.hot && !inWebWorker) {
69
+ if (!window.$RefreshReg$) {
70
+ throw new Error(
71
+ "@uniflowed/vite can't detect the Fast Refresh preamble. Something is wrong."
72
+ );
73
+ }
74
+
75
+ prevRefreshReg = window.$RefreshReg$;
76
+ prevRefreshSig = window.$RefreshSig$;
77
+ window.$RefreshReg$ = RefreshRuntime.getRefreshReg(${JSON.stringify(id)});
78
+ window.$RefreshSig$ = RefreshRuntime.createSignatureFunctionForTransform;
79
+ }
80
+
81
+ ${nextCode}
82
+
83
+ if (import.meta.hot && !inWebWorker) {
84
+ window.$RefreshReg$ = prevRefreshReg;
85
+ window.$RefreshSig$ = prevRefreshSig;
86
+ }
87
+ `;
88
+ if (nextMap) nextMap.mappings = ";".repeat(16) + nextMap.mappings;
89
+ }
90
+
91
+ nextCode = `import * as RefreshRuntime from "${RUNTIME_PUBLIC_PATH}";
92
+ const inWebWorker = typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope;
93
+
94
+ ${nextCode}
95
+
96
+ if (import.meta.hot && !inWebWorker) {
97
+ RefreshRuntime.__hmr_import(import.meta.url).then((currentExports) => {
98
+ RefreshRuntime.registerExportsForReactRefresh(${JSON.stringify(id)}, currentExports);
99
+ import.meta.hot.accept((nextExports) => {
100
+ if (!nextExports) return;
101
+ const invalidateMessage = RefreshRuntime.validateRefreshBoundaryAndEnqueueUpdate(${JSON.stringify(id)}, currentExports, nextExports);
102
+ if (invalidateMessage) import.meta.hot.invalidate(invalidateMessage);
103
+ });
104
+ });
105
+ }
106
+ `;
107
+ if (nextMap) nextMap.mappings = ";;;" + nextMap.mappings;
108
+
109
+ return { code: nextCode, map: nextMap };
110
+ }
@@ -0,0 +1,243 @@
1
+ // Plain JavaScript: executed by the host that runs Vite, before any transform.
2
+ //
3
+ // The file-system router, as the build sees it.
4
+ //
5
+ // This mirrors `uf_router` in Rust — the same reserved-name grammar
6
+ // (`_uf.<role>[.<variant>].js`, plus `.mdx` for pages), the same route path
7
+ // syntax (`[param]`, `[...rest]`, `(group)`) and the same sort order — and it
8
+ // must keep mirroring it: `uf lint` and `router.js`'s generated types describe
9
+ // the routes this module serves, so the two cannot be allowed to disagree.
10
+ //
11
+ // Everything produced here is a string of JavaScript for a virtual module. The
12
+ // route table imports every page and layout lazily, so a route is a chunk of
13
+ // its own and the client only downloads what it navigates to.
14
+
15
+ import { readdirSync, statSync } from "node:fs";
16
+ import path from "node:path";
17
+
18
+ /** The file names the router reserves inside the router root. */
19
+ export const RESERVED = Object.freeze({
20
+ layout: "_uf.layout",
21
+ page: "_uf.page",
22
+ middleware: "_uf.middleware",
23
+ notFound: "_uf.not-found",
24
+ });
25
+
26
+ /** Extensions a page or layout may use; `.mdx` is a page written as content. */
27
+ const PAGE_EXTENSIONS = [".js", ".jsx", ".mdx"];
28
+ const MODULE_EXTENSIONS = [".js", ".jsx"];
29
+
30
+ /** Deepest directory nesting the scan will follow. */
31
+ const MAX_DEPTH = 32;
32
+
33
+ /**
34
+ * One route in the table.
35
+ *
36
+ * @typedef {object} Route
37
+ * @property {string} path route path such as `/docs/:slug`
38
+ * @property {string} pattern the same path with `*` for catch-alls, for humans
39
+ * @property {ReadonlyArray<{name: string, catchAll: boolean}>} params
40
+ * @property {string} page absolute path of the page module
41
+ * @property {ReadonlyArray<string>} layouts absolute paths, root first
42
+ * @property {ReadonlyArray<string>} middleware absolute paths, root first
43
+ * @property {boolean} mdx whether the page is MDX content
44
+ */
45
+
46
+ /**
47
+ * Scan `appRoot` for routes.
48
+ *
49
+ * Returns routes sorted by path, which is the order `uf_router` uses too.
50
+ * Directories that do not exist yield an empty table rather than an error: a
51
+ * library project has no router root, and that is not a mistake.
52
+ *
53
+ * @param {string} appRoot absolute path of the router root (`app/`)
54
+ * @returns {Route[]}
55
+ */
56
+ export function scanRoutes(appRoot) {
57
+ const routes = [];
58
+ let notFound = null;
59
+ if (!isDirectory(appRoot)) return { routes, notFound };
60
+
61
+ const walk = (directory, segments, layouts, middleware, depth) => {
62
+ if (depth > MAX_DEPTH) return;
63
+ const entries = readdirSync(directory, { withFileTypes: true }).sort((a, b) =>
64
+ a.name < b.name ? -1 : a.name > b.name ? 1 : 0,
65
+ );
66
+
67
+ const ownLayout = findModule(directory, RESERVED.layout, MODULE_EXTENSIONS);
68
+ const ownMiddleware = findModule(directory, RESERVED.middleware, MODULE_EXTENSIONS);
69
+ const nextLayouts = ownLayout ? [...layouts, ownLayout] : layouts;
70
+ const nextMiddleware = ownMiddleware ? [...middleware, ownMiddleware] : middleware;
71
+
72
+ const page = findModule(directory, RESERVED.page, PAGE_EXTENSIONS);
73
+ if (page) {
74
+ const { path: routePath, pattern, params } = routeFromSegments(segments);
75
+ routes.push({
76
+ path: routePath,
77
+ pattern,
78
+ params,
79
+ page,
80
+ layouts: nextLayouts,
81
+ middleware: nextMiddleware,
82
+ mdx: page.endsWith(".mdx"),
83
+ });
84
+ }
85
+ if (depth === 0) {
86
+ const own = findModule(directory, RESERVED.notFound, PAGE_EXTENSIONS);
87
+ if (own) notFound = { page: own, layouts: nextLayouts, mdx: own.endsWith(".mdx") };
88
+ }
89
+
90
+ for (const entry of entries) {
91
+ if (!entry.isDirectory()) continue;
92
+ // A leading dot or underscore is private to the author: `_components/`
93
+ // beside a page is a place to put things, not a route.
94
+ if (entry.name.startsWith(".") || entry.name.startsWith("_")) continue;
95
+ walk(
96
+ path.join(directory, entry.name),
97
+ [...segments, entry.name],
98
+ nextLayouts,
99
+ nextMiddleware,
100
+ depth + 1,
101
+ );
102
+ }
103
+ };
104
+
105
+ walk(appRoot, [], [], [], 0);
106
+ routes.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
107
+ return { routes, notFound };
108
+ }
109
+
110
+ function isDirectory(candidate) {
111
+ try {
112
+ return statSync(candidate).isDirectory();
113
+ } catch {
114
+ return false;
115
+ }
116
+ }
117
+
118
+ function findModule(directory, stem, extensions) {
119
+ for (const extension of extensions) {
120
+ const candidate = path.join(directory, stem + extension);
121
+ try {
122
+ if (statSync(candidate).isFile()) return candidate;
123
+ } catch {
124
+ // keep looking
125
+ }
126
+ }
127
+ return null;
128
+ }
129
+
130
+ /**
131
+ * Turn directory segments into a route path and its parameters.
132
+ *
133
+ * `(group)` segments organise files without appearing in the URL, `[name]`
134
+ * captures one segment, and `[...name]` captures the rest of the path.
135
+ */
136
+ export function routeFromSegments(segments) {
137
+ const params = [];
138
+ const out = [];
139
+ for (const segment of segments) {
140
+ if (segment.startsWith("(") && segment.endsWith(")")) continue;
141
+ if (segment.startsWith("[...") && segment.endsWith("]")) {
142
+ const name = segment.slice(4, -1);
143
+ params.push({ name, catchAll: true });
144
+ out.push(`:${name}*`);
145
+ continue;
146
+ }
147
+ if (segment.startsWith("[") && segment.endsWith("]")) {
148
+ const name = segment.slice(1, -1);
149
+ params.push({ name, catchAll: false });
150
+ out.push(`:${name}`);
151
+ continue;
152
+ }
153
+ out.push(segment);
154
+ }
155
+ const routePath = out.length === 0 ? "/" : `/${out.join("/")}`;
156
+ return { path: routePath, pattern: routePath.replace(/:(\w+)\*/g, "*$1"), params };
157
+ }
158
+
159
+ /** Virtual module ids the router plugin serves. */
160
+ export const VIRTUAL = Object.freeze({
161
+ routes: "virtual:uf/routes",
162
+ client: "virtual:uf/client",
163
+ server: "virtual:uf/server",
164
+ });
165
+
166
+ /**
167
+ * The source of `virtual:uf/routes`.
168
+ *
169
+ * Each page and layout is a lazy `import()`, so a route is a chunk of its own.
170
+ * Layouts are deduplicated into one table so a layout shared by fifty routes
171
+ * is one dynamic import, not fifty.
172
+ *
173
+ * @param {{routes: Route[], notFound: object | null}} table
174
+ */
175
+ export function routesModuleSource(table) {
176
+ const layoutIds = new Map();
177
+ const layoutImports = [];
178
+ const layoutId = (file) => {
179
+ let id = layoutIds.get(file);
180
+ if (id === undefined) {
181
+ id = `layout${layoutIds.size}`;
182
+ layoutIds.set(file, id);
183
+ layoutImports.push(`const ${id} = () => import(${JSON.stringify(file)});`);
184
+ }
185
+ return id;
186
+ };
187
+
188
+ const entries = table.routes.map((route) => {
189
+ const layouts = route.layouts.map(layoutId);
190
+ return ` {
191
+ path: ${JSON.stringify(route.path)},
192
+ params: ${JSON.stringify(route.params)},
193
+ mdx: ${route.mdx},
194
+ file: ${JSON.stringify(route.page)},
195
+ page: () => import(${JSON.stringify(route.page)}),
196
+ layouts: [${layouts.join(", ")}],
197
+ }`;
198
+ });
199
+
200
+ const notFound = table.notFound
201
+ ? `{
202
+ mdx: ${table.notFound.mdx},
203
+ file: ${JSON.stringify(table.notFound.page)},
204
+ page: () => import(${JSON.stringify(table.notFound.page)}),
205
+ layouts: [${table.notFound.layouts.map(layoutId).join(", ")}],
206
+ }`
207
+ : "null";
208
+
209
+ return `${layoutImports.join("\n")}
210
+ export const routes = [
211
+ ${entries.join(",\n")}
212
+ ];
213
+ export const notFound = ${notFound};
214
+ export default routes;
215
+ `;
216
+ }
217
+
218
+ /**
219
+ * The source of `virtual:uf/client`: hydrate the document with the app.
220
+ *
221
+ * The current route's modules are loaded *before* hydration so the first
222
+ * render is synchronous and matches the server's HTML; a lazy import during
223
+ * hydration would suspend and React would fall back to a client render.
224
+ */
225
+ export function clientModuleSource(appEntry) {
226
+ return `import { hydrate } from "@uniflowed/router/client";
227
+ import { routes, notFound } from ${JSON.stringify(VIRTUAL.routes)};
228
+ import App from ${JSON.stringify(appEntry)};
229
+ hydrate({ App, routes, notFound });
230
+ `;
231
+ }
232
+
233
+ /**
234
+ * The source of `virtual:uf/server`: render one URL to HTML.
235
+ */
236
+ export function serverModuleSource(appEntry) {
237
+ return `import { createRenderer } from "@uniflowed/router/server";
238
+ import { routes, notFound } from ${JSON.stringify(VIRTUAL.routes)};
239
+ import App from ${JSON.stringify(appEntry)};
240
+ export { routes, notFound };
241
+ export const render = createRenderer({ App, routes, notFound });
242
+ `;
243
+ }
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@uniflowed/vite",
3
+ "version": "0.0.0-alpha.1",
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
+ "type": "module",
6
+ "license": "MIT",
7
+ "sideEffects": false,
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/ubugeeei-prod/uf.git",
11
+ "directory": "packages/vite"
12
+ },
13
+ "exports": {
14
+ ".": "./index.js",
15
+ "./driver": "./driver.js",
16
+ "./register": "./register.js",
17
+ "./bun-preload": "./bun-preload.js",
18
+ "./transform": "./transform.js",
19
+ "./package.json": "./package.json"
20
+ },
21
+ "files": [
22
+ "index.js",
23
+ "driver.js",
24
+ "register.js",
25
+ "bun-preload.js",
26
+ "transform.js",
27
+ "internal"
28
+ ],
29
+ "dependencies": {
30
+ "@mdx-js/rollup": "^3.1.1",
31
+ "rehype-slug": "^6.0.0",
32
+ "remark-frontmatter": "^5.0.0",
33
+ "remark-gfm": "^4.0.1",
34
+ "remark-mdx-frontmatter": "^5.2.0",
35
+ "vite": "^8.2.2"
36
+ },
37
+ "peerDependencies": {
38
+ "react": ">=19",
39
+ "react-dom": ">=19"
40
+ }
41
+ }
package/register.js ADDED
@@ -0,0 +1,11 @@
1
+ // Plain JavaScript: this file registers the loader, so it cannot need one.
2
+ //
3
+ // `node --import @uniflowed/vite/register app.js` runs a Flow project on
4
+ // Node.js without a build step. Importing this module installs the hooks in
5
+ // `./internal/node-hooks.js` for the rest of the process.
6
+
7
+ import { register } from "node:module";
8
+
9
+ register("./internal/node-hooks.js", import.meta.url, {
10
+ data: { root: process.env.UF_PROJECT_ROOT ?? process.cwd() },
11
+ });
package/transform.js ADDED
@@ -0,0 +1,187 @@
1
+ // Plain JavaScript: executed by the host that runs Vite, before any transform
2
+ // exists — this module is how the transform is reached, so it cannot be Flow.
3
+ //
4
+ // The Flow → JavaScript transform lives in `uf` itself (`crates/uf_transform`:
5
+ // the official Flow parser, Flow's own lowering rules, the official React
6
+ // Compiler, oxc for JSX and code generation). This module is the JavaScript
7
+ // side of the `uf transform` service: one long-lived `uf` process per host
8
+ // process, newline-delimited JSON in, replies in request order out.
9
+ //
10
+ // Every host that runs Flow — the Vite plugin, the Node loader hook, the Bun
11
+ // preload, the config loader — goes through here, which is what makes them
12
+ // all produce the same module from the same source.
13
+
14
+ import { spawn } from "node:child_process";
15
+ import { createInterface } from "node:readline";
16
+
17
+ /** File extensions uf treats as Flow source. */
18
+ export const FLOW_EXTENSIONS = [".js", ".jsx", ".mjs", ".cjs"];
19
+
20
+ /**
21
+ * Whether uf is responsible for transforming this module.
22
+ *
23
+ * Mirrors `uf_transform::is_flow_module`, and must keep mirroring it: a `uf
24
+ * dev` session and a `uf test` run that disagree about which files are Flow
25
+ * disagree about what the code is.
26
+ *
27
+ * A build driver synthesises modules of its own (`\0vite/client`, Rolldown's
28
+ * shims), and a third-party dependency ships JavaScript that is already
29
+ * JavaScript; neither is Flow. `@uniflowed/*` under `node_modules` is the
30
+ * deliberate exception: those packages ship Flow source, because that is what
31
+ * uf tells everyone to write.
32
+ */
33
+ export function isFlowModule(id) {
34
+ if (id.startsWith("\0")) return false;
35
+ const clean = stripQuery(id);
36
+ if (!FLOW_EXTENSIONS.some((extension) => clean.endsWith(extension))) return false;
37
+ const at = clean.lastIndexOf("/node_modules/");
38
+ return at === -1 || clean.slice(at).startsWith("/node_modules/@uniflowed/");
39
+ }
40
+
41
+ function stripQuery(id) {
42
+ const at = id.indexOf("?");
43
+ return at === -1 ? id : id.slice(0, at);
44
+ }
45
+
46
+ /**
47
+ * The `uf` binary to talk to.
48
+ *
49
+ * `uf dev`, `uf build` and `uf test` set `UF_BINARY` to themselves when they
50
+ * start a host, so the host reaches exactly the binary that started it. A host
51
+ * started by hand finds `uf` on PATH, which is what the installer arranges.
52
+ */
53
+ export function ufBinary() {
54
+ return process.env.UF_BINARY ?? "uf";
55
+ }
56
+
57
+ /**
58
+ * An error the transform reported for one module, with its position when
59
+ * the parser or the lowering rules gave one.
60
+ */
61
+ export class TransformError extends Error {
62
+ constructor(id, message, line, column) {
63
+ super(message);
64
+ this.name = "TransformError";
65
+ this.id = id;
66
+ this.loc = line != null ? { file: id, line, column: column ?? 0 } : undefined;
67
+ }
68
+ }
69
+
70
+ /**
71
+ * One `uf transform` process, with requests answered in the order they were
72
+ * sent.
73
+ *
74
+ * `uf transform` replies once per request, in order, so a plain queue of
75
+ * resolvers pairs a reply with its caller — no correlation ids and no map to
76
+ * leak. Any exit is final: a request made after the process has gone is
77
+ * rejected at once rather than queued against something that will never
78
+ * answer.
79
+ */
80
+ export class TransformService {
81
+ #child;
82
+ #pending = [];
83
+ #failure = null;
84
+
85
+ /**
86
+ * @param {object} [options]
87
+ * @param {string} [options.command] the `uf` binary; `ufBinary()` by default
88
+ * @param {string} [options.root] project root, so `uf.config.js` is found
89
+ */
90
+ constructor(options = {}) {
91
+ const command = options.command ?? ufBinary();
92
+ const root = options.root ?? process.cwd();
93
+ this.#child = spawn(command, ["--cwd", root, "transform"], {
94
+ stdio: ["pipe", "pipe", "inherit"],
95
+ });
96
+
97
+ createInterface({ input: this.#child.stdout }).on("line", (line) => {
98
+ const waiting = this.#pending.shift();
99
+ if (!waiting) return;
100
+ let reply;
101
+ try {
102
+ reply = JSON.parse(line);
103
+ } catch {
104
+ waiting.reject(new Error(`uf transform sent a malformed reply: ${line}`));
105
+ return;
106
+ }
107
+ if (reply.error != null) {
108
+ waiting.reject(new TransformError(waiting.id, reply.error, reply.line, reply.column));
109
+ return;
110
+ }
111
+ waiting.resolve(reply);
112
+ });
113
+
114
+ this.#child.on("error", (error) => {
115
+ this.#settleAll(new Error(`could not run \`${command} transform\`: ${error.message}`));
116
+ });
117
+ this.#child.on("close", (code) => {
118
+ this.#settleAll(new Error(`uf transform exited (${code})`));
119
+ });
120
+ }
121
+
122
+ #settleAll(error) {
123
+ this.#failure = error;
124
+ while (this.#pending.length > 0) this.#pending.shift().reject(error);
125
+ }
126
+
127
+ /**
128
+ * Transform one module.
129
+ *
130
+ * Resolves to `{ code, map, diagnostics }`, or to `null` when the module is
131
+ * not uf's to transform (see `isFlowModule`). Rejects with a
132
+ * `TransformError` carrying the position when the source is not valid Flow.
133
+ *
134
+ * @param {string} id absolute path, used for the map and for errors
135
+ * @param {string} code the Flow source
136
+ * @param {object} [options]
137
+ * @param {boolean} [options.development] readable output, `jsxDEV`
138
+ * @param {boolean} [options.refresh] Fast Refresh registrations (development only)
139
+ * @param {boolean} [options.sourceMap] produce a source map; on by default
140
+ */
141
+ transform(id, code, options = {}) {
142
+ if (this.#failure) return Promise.reject(this.#failure);
143
+ return new Promise((resolve, reject) => {
144
+ this.#pending.push({
145
+ id,
146
+ reject,
147
+ resolve: (reply) => {
148
+ if (reply.code == null) {
149
+ resolve(null);
150
+ return;
151
+ }
152
+ resolve({ code: reply.code, map: reply.map ?? null, diagnostics: reply.diagnostics ?? [] });
153
+ },
154
+ });
155
+ this.#child.stdin.write(`${JSON.stringify({ id, code, options })}\n`);
156
+ });
157
+ }
158
+
159
+ /** Stop the process. Outstanding requests are rejected. */
160
+ close() {
161
+ this.#child.stdin.end();
162
+ this.#child.kill();
163
+ }
164
+ }
165
+
166
+ let shared = null;
167
+
168
+ /**
169
+ * The process-wide service, started on first use.
170
+ *
171
+ * The loader hooks and the config loader share one process per host rather
172
+ * than one per module; it lives as long as the host does.
173
+ */
174
+ export function sharedService(root) {
175
+ shared ??= new TransformService({ root: root ?? process.env.UF_PROJECT_ROOT ?? process.cwd() });
176
+ return shared;
177
+ }
178
+
179
+ /**
180
+ * Transform one Flow module through the shared service.
181
+ *
182
+ * Returns `{ code, map, diagnostics }`; a module that is not uf's to transform
183
+ * comes back as `null`.
184
+ */
185
+ export function transformFlow(code, filename, options = {}) {
186
+ return sharedService(options.root).transform(filename, code, options);
187
+ }