@uniflowed/host/assets
variable
IMAGE_EXTENSIONS
export const IMAGE_EXTENSIONS = [".png", ".jpg", ".jpeg", ".webp", ".gif", ".avif"];
File extensions the pipeline claims.
.svg is deliberately not among them. uf could only ever copy one through — it is already resolution independent, so there is nothing to resize — and claiming it would take .svg away from vite-plugin-svgr and everything like it, which turn one into a component. A plugin that claims an extension to do nothing with it is the shape of red line 8 in docs/red-lines.md: if Vite can do it, a uf project can do it. So an SVG import stays Vite's, and <Image src={url} width={…} height={…} /> is how you render one.
.gif and .avif *are* claimed even though no decoder for them is compiled in, and that is the opposite decision for a reason: uf has something to say about them. The import still evaluates to a manifest, and the manifest carries a note saying the file was served unchanged and why — which is what tells an author their AVIF is not being resized, rather than leaving them to notice.
variable
FONT_EXTENSIONS
export const FONT_EXTENSIONS = [".woff2", ".woff", ".ttf", ".otf"];
Font file extensions the pipeline claims.
variable
OG_EXTENSION
export const OG_EXTENSION = ".og.json";
The extension an Open Graph template is written under.
A compound extension, so the claim is as narrow as a claim can be. Ordinary .json stays Vite's — only a file a project *named* .og.json becomes a card, and ?raw and ?url still reach the file itself, because a query is Vite's. It is checked before the image list for the same reason it is compound: the longer suffix has to win.
variable
ICON_PREFIX
export const ICON_PREFIX = "uf:icon/";
The virtual module an icon import resolves through.
variable
ICON_SPRITE
export const ICON_SPRITE = "uf:icon-sprite";
The virtual module holding every icon a build reached.
function
assetKind
export function assetKind(id) { ... }
Whether this import is one uf's asset pipeline handles, and as what.
Returns "image", "font", "og", "icon", "sprite", or null. The query string is stripped first: ./hero.png?width=400 is an image, and the query is how an import says what it wants.
class
AssetError
export class AssetError extends Error { ... }
An asset the pipeline could not process.
class
AssetService
export class AssetService { ... }
One uf assets process, with requests answered in the order they were sent.
The same arrangement as TransformService next door, including the reason there are no correlation ids: the service replies once per request and in order, so a plain queue of resolvers pairs a reply with its caller. Any exit is final and every outstanding request is rejected at once.
# Why the child is unreferenced between requests
The same reason, stated once next door and applied here: a live child process and its pipes are handles, and a host with a handle open does not exit. So this service holds its host open for exactly as long as it owes an answer — referenced when a request joins an empty queue, unreferenced when the queue drains, and unreferenced from the start — and #holdHost acts only on the edge because ref/unref *count* on Bun where they set a flag on Node. TransformService.#holdHost carries that argument in full.
It was invisible here for as long as it was, and that is worth saying rather than discovering: the only thing that constructs an AssetService is the Node-based Vite driver, and it closes the service in buildEnd. A host that drives assets and does not — which is where runtime independence goes, since ubugeeei-redundancy.md requires it be real rather than an enum — got a build that finished and then sat there, with the symptom nowhere near the cause. See ubugeeei-prod/uf#596, and #418 for the same defect in the service that is asked about every module.
@uniflowed/host/module-mocks
variable
REVISION_PARAM
export const REVISION_PARAM = "uf-modules";
The URL parameter carrying <module epoch>.<mock revision>.
variable
ACTUAL_PARAM
export const ACTUAL_PARAM = "uf-actual";
The URL parameter marking an import that must reach the real module.
function
moduleKey
export function moduleKey(url) { ... }
A module's identity for the purpose of mocking: its URL with no query.
The query is where every mechanism in this file writes — the worker's cache-busting uf-run, this file's revision, importActual's marker — so two URLs that differ only there are the same module as far as a mock is concerned.
function
interceptionSupported
export function interceptionSupported() { ... }
Whether this host can intercept a module before it is imported.
The one requirement is synchronous, in-thread module hooks. Node has them, and so does Deno from 2.8 — the same registerHooks its Flow loader (./deno-preload.js) is built on, on a runtime that sets process.versions.node — so the first branch below answers for both, and crates/uf_cli/tests/deno_host.rs replaces a module on a real Deno to hold that. Bun's node:module has neither register nor registerHooks. @uniflowed/test turns a false here into an error that names the host rather than a mock that quietly does nothing.
Bun's plugin API is a second answer only once direct dynamic import redirects work, which starts at the version pinned in bunInterceptionSupported.
function
installInterception
export function installInterception() { ... }
Install the interception hooks, once.
Called on the first uft.mock or uft.resetModules rather than at import, because a resolve hook that runs for every specifier in the process is not something a suite that never mocks anything should pay for.
function
defineModuleMock
export function defineModuleMock(url, namespace) { ... }
Register namespace as the stand-in for the module at url.
Returns nothing: what the caller needs is that the *next* resolution of that module lands somewhere else, which the resolve hook arranges from the revision recorded here.
function
removeModuleMock
export function removeModuleMock(url) { ... }
Stop standing in for the module at url.
function
isModuleMocked
export function isModuleMocked(url) { ... }
Whether the module at url is currently stood in for.
function
moduleEpoch
export function moduleEpoch() { ... }
The epoch path imports are currently loading into.
function
startModuleEpoch
export function startModuleEpoch() { ... }
Begin a new epoch, so a path import evaluates its module again.
Counted process-wide rather than per file. A file that reused the number a previous file's epoch had would be handed that file's modules, mocks and all, which is the leak the whole scheme exists to prevent.
function
resetModuleMocks
export function resetModuleMocks() { ... }
Forget every mock and leave the epoch.
Called by the worker between files. Epochs and revisions deliberately keep counting: the next file's mock of the same module must not be handed the URL this file's mock is cached under.
function
actualUrl
export function actualUrl(url, pathLike) { ... }
The URL an import that must reach the real module should use.
A mocked module is standing at the URL an ordinary import resolves to, so reaching past it takes a URL of its own — one per epoch, so that two calls in a row hand back the same module rather than compiling it twice.
A module nobody is standing in for needs no marker: the URL an ordinary import would use already holds the real thing, and taking the marked path anyway would hand back a second copy of a module the test is already holding.
pathLike is whether the caller wrote a path rather than a package name, and it is not a detail: this is the resolve hook's rule applied by hand, and the rule is that a package keeps its identity across an epoch. Getting it wrong here would hand a test a second copy of @uniflowed/test — a second registry and a second set of spies — from the one call that is supposed to reach the real thing.
function
namespaceFor
export function namespaceFor(url) { ... }
The values a generated stand-in module exports.
Keyed by the exact URL the stand-in was loaded as rather than by the module key, so that a stand-in evaluated after its mock was replaced still reads the namespace its export list was written from.
function
mockedSource
export function mockedSource(url) { ... }
The source of the stand-in module for url, or null when there is none.
An ES module's export names are fixed when it is compiled, so they are written out here from the keys the mock actually has. That is the whole reason this returns source rather than an object: a namespace object cannot be handed to import, and a module with the wrong names would fail to link with an error about the importer rather than about the mock.
function
bunMockedModulePath
export function bunMockedModulePath(url) { ... }
Write the current stand-in for url to a real file Bun can redirect to.
Node serves generated modules from loadHook, but Bun's onLoad never sees the query-carrying identity this file uses. The piece of Bun that does work is an onResolve answer naming another file, so this materializes the same generated module under a unique path.
variable
export const FLOW_EXTENSIONS = [".js", ".jsx", ".mjs", ".cjs"];
File extensions uf treats as Flow source.
function
export function isFlowModule(id) { ... }
Whether uf is responsible for transforming this module.
Mirrors uf_transform::is_flow_module, and must keep mirroring it: a uf dev session and a uf test run that disagree about which files are Flow disagree about what the code is.
A build tool synthesises modules of its own — ids beginning with a NUL byte, a bundler's shims — and a third-party dependency ships JavaScript that is already JavaScript; neither is Flow. @uniflowed/* under node_modules is the deliberate exception: those packages ship Flow source, because that is what uf tells everyone to write.
Which build tool is deliberately not named. This loader runs Flow on a Capability JS Host and has no bundler in it; naming one would tie the answer to a tool that is not in this file's dependency graph.
function
export function isCompiledOutput(filename) { ... }
Whether filename is JavaScript uf itself compiled and wrote under .uf/: the compiled uf.config.* the config loader wrote, or anything uf build wrote under .uf/build/ — the rsc graph, the server bundle, the compiled application.
isFlowModule answers "is this path uf's to transform?" from the path's shape, and a bundle under the project root has the shape of project source. It is not source. It is the transform's own output, already through the Flow chain and the React Compiler, and handing it back meant compiling it twice: uf build's server pass re-transformed the whole rsc bundle, and the prerender's import() of the server bundle re-transformed that — hundreds of kilobytes each, on every build, in series, and with the React Compiler run a second time over components it had already compiled.
Deliberately a second question rather than a clause in isFlowModule: that function's answer is mirrored by FLOW_MODULE_PATTERN and by uf_transform::is_flow_module, and "already compiled" is a fact about where uf writes, which only a caller that is about to compile needs.
function
export function uniflowedPackages(root) { ... }
Every @uniflowed/* package the project can resolve.
Vite's dependency optimizer takes package names, not predicates. A Vite app that owns its config can use this list as optimizeDeps.exclude, matching the same @uniflowed/* exception isFlowModule makes for Flow source in node_modules.
function
export function ufBinary() { ... }
The uf binary to talk to.
uf dev, uf build and uf test set UF_BINARY to themselves when they start a host, so the host reaches exactly the binary that started it. A host started by hand finds uf on PATH, which is what the installer arranges.
function
export function inSourceTests() { ... }
Whether this host is compiling for a run that collects in-source tests.
uf test sets UF_IN_SOURCE_TESTS on every worker it starts, and nothing else sets it. The flag decides what import.meta.uf.test compiles to — uf's test API here, void 0 everywhere else — which is why it has to reach the transform rather than only the runtime: a block that survives into a production bundle is worse than no in-source tests at all.
Read per call rather than captured once, because the loader hooks are installed before uf has told the process anything.
function
export function environmentVariable(name) { ... }
One environment variable, or undefined when it is unset *or* this process may not read it.
Deno denies by default, and a worker uf test starts there is granted the variables uf set on it and nothing else. Reading any other throws NotCapable rather than answering undefined — measured on Deno 2.9 — and every variable this package reads on the way to a module is one it only consults: a loader that took the process down over one would be failing a suite over nothing. On Node and Bun this is process.env[name] and no more.
function
export function ufBinaryIdentity(command = ufBinary()) { ... }
Which *build* of uf a host will transform through, or null when that cannot be established.
ufBinary() names the compiler; this identifies it. Anything kept across runs needs the second, because the first does not change when the compiler does: crates/uf_transform is edited, cargo build writes a new binary over the old one, and every answer already on disk is now wrong while the name that produced them is unchanged. A version string is the same promise one step removed — every build between two releases shares one.
So: the size and modification time of the file that will be executed. They move together on every rebuild, they are one stat away, and — this is the part that decided it — reading them does not require starting uf. A run that finds everything already compiled must not have to spawn the compiler to learn that it does not need it, which is what asking the running uf transform to introduce itself would have cost.
The same test is applied to a path as to a bare name: a regular file with the execute bit. Size and mtime do not move when a binary loses that bit, so without the test a chmod produced the same identity as before, a warm cache went on serving, and a cold one failed to start uf — the answer depending on how warm the cache was, which is the class of bug this key exists to remove.
null means the question could not be answered. It is not an invitation to hash the rest anyway: a key that leaves the compiler out is one key for every build of it, which is the whole defect.
class
export class TransformError extends Error { ... }
An error the transform reported for one module, with its position when the parser or the lowering rules gave one.
class
export class TransformService { ... }
One uf transform process, with requests answered in the order they were sent.
uf transform replies once per request, in order, so a plain queue of resolvers pairs a reply with its caller — no correlation ids and no map to leak. Any exit is final: a request made after the process has gone is rejected at once rather than queued against something that will never answer.
# Why the child is unreferenced between requests
A live child process and its pipes are handles, and a host with a handle open does not exit. Nothing closes the process-wide service — the loader hooks and the Bun preload both take it and neither has an "afterwards" to close it in — so on Bun bun --preload @uniflowed/host/bun-preload app.js ran the program, printed its output, and then sat there forever. Node hides this: its loaders keep the service on a thread of their own — the loader thread register() starts, or the transform thread ./internal/sync-hooks.js starts on a cache miss — and the process exits with the main thread whatever that thread is still holding. That accident is the only reason it was ever invisible, and it is not something the second host can be asked to reproduce.
So the service holds its host open for exactly as long as it owes an answer: referenced when a request joins an empty queue, unreferenced when the queue drains, and unreferenced from the start. Unreferencing unconditionally would be the other bug — the host would be free to exit during a transform, and uf build would end in the middle of a module with no error anywhere.
function
export function sharedService(root, options = {}) { ... }
The process-wide service, started on first use.
The loader hooks and the config loader share one process per host rather than one per module; it lives as long as the host does.
function
export function transformFlow(code, filename, options = {}) { ... }
Transform one Flow module through the shared service.
Returns { code, map, css, diagnostics }; a module that is not uf's to transform comes back as null.
function
export function transformFlowSync(code, filename, options = {}) { ... }
Transform one Flow module in a short-lived uf transform, and wait for it.
Resolves nothing and returns what transformFlow resolves to — { code, map, css, diagnostics }, or null for a module that is not uf's — and throws what it rejects with. One request written to the child's stdin, stdin closed, one reply read back: the same binary and the same protocol as the service, so the module cannot differ between the two ways in.
For Deno's in-thread hooks (./internal/sync-hooks.js), which on Node sleep on a transform thread instead. On Deno that thread is not safe to rely on: measured on Deno 2.9.6 in this repository's CI, Linux x86_64, one of two runs of the same commit panicked with Fatal error in :0: unreachable code in every test that compiled a module through the thread, and in none that read the cache. A child process per cold module costs about ten milliseconds and involves no thread and no Atomics.wait at all.
Without a doc comment