Getting started
What uf does not do
A boundary you discover on a Thursday afternoon is a limitation. The same boundary, read before you start, is a promise about what the tool will never do to you. This page is both lists: what uf refuses to do on purpose, and what it does not do yet — the second list with issue numbers, because a gap without one is a wish.
The refusals
It will never grow an eject
There is no uf eject, and there will not be a uf config --write or a
"print the effective Vite config so you can copy it" flag either, because that
becomes one in practice. create-react-app put a cliff between zero config
and fully customisable, with a one-way door at the bottom of it; uf replaces
the door with a continuum:
convention → configuration → provider replacement → raw provider API
Each step is smaller than the one before, and none of them locks. A project that needs one Vite plugin adds one Vite plugin, and does not inherit a build system. The reasoning, and the audit of where uf does not yet live up to it, is Architecture.
It does not own the bundler, and will not fork it
The dev server and the production build are Vite — packages/vite/package.json
depends on vite: "^8.2.2", and uf drives it over a JSON protocol. If uf needs
behaviour Vite does not have, the answer is a plugin or an upstream change,
never a fork. That is the first red line and it is first for a reason: a
vendored, patched Vite would be uf's forever.
The consequence you can use: anything Vite can do, a uf project can do,
without waiting for a uf release. The vite key in uf.config.js is merged
over what uf generates and uf does not read it.
It does not type-check your code
Flow does. uf check runs Flow's own inference and reports what Flow says.
uf reimplementing type inference would produce a second opinion about your
program, which is the exact failure this toolchain exists to remove.
It does not invent a name for something Flow already names
A file says it is plain JavaScript with @noflow. uf will not add a
check.exclude list, because that would be a second answer to a question that
already has one, in a place a reader is not looking.
It will not run npm lifecycle scripts
uf install refuses a manifest that declares scripts before it fetches
anything:
error: package manifest /…/my-site/package.json declares scripts;
use uf tasks in uf.config.js
Install-time scripts are a remote-code-execution path on a machine that just
cloned a repository, and the threat model is written down in
docs/security.md.
Project automation lives in uf.config.js tasks instead, which is also why a
generated project has no npm scripts.
It will not bind a routable address quietly
uf dev --host refuses to start while dev.allowedHosts is empty, before any
process is spawned. A dev server on 0.0.0.0 with no allow-list is a file
server for your source tree, and uf never writes * on your behalf.
It will not format what it cannot reproduce
uf fmt formats the GraphQL inside a tagged template, and leaves a template
alone, byte for byte, when it cannot match Prettier exactly — a # comment
in the document, a string holding a control character, more than 128 levels of
nesting. Of the 1,138 graphql templates in Relay's test suite it reproduces
Prettier on 1,092 and declines 46, every one of them for a comment. Approximately
right is not a thing a formatter may be; Formatting and
linting has the rest.
It is not framework-agnostic, and does not want to be
uf is Flow and React. A project's .js, .jsx, .mjs and .cjs files are
uf's to transform; a third-party dependency under node_modules is not, except
@uniflowed/*, which ship Flow source. A TypeScript file in your application is
not a Flow module — the formatter will hand it to Biome, and nothing else in uf
will read it.
It will not share a process between two test files
uf test gives each worker one file at a time. Two files sharing a process
share globals, and a suite that passes alone and fails beside another is the
worst failure a runner can produce. It costs real time: uf spawns a worker per
core and each one loads the test API before it can do anything, which is where
the gap to bun test comes from. It is not a trade uf intends to make back by
removing the isolation. Testing has the measurements.
It will not answer "who did that?" with silence
uf inspect --json prints the resolved configuration and the route table.
uf explain <command> names the provider for every stage of dev, build,
test, fmt, lint and check — which binary runs it and what it does. A
stage that cannot be explained is a stage that should not exist.
What it does not do yet
Everything above is a decision. Everything below is unfinished, and each row links to the issue that tracks it. If you need one of these, uf is not ready for you — that is a better thing to learn here than three days in.
| What is missing | Where it actually stands | Issue |
|---|---|---|
Deploying somewhere in particular. uf preview, uf start, uf build --adapter node and uf build --compile all serve a build, and the last two produce something you can take away. node is the only one of the seven adapters that exists | Every adapter shares one application handler now, so what each of the six still needs is an entry file and an answer for where the static half lives | #391, #250 |
React Server Components. The split is route-level. A route with no "use client" boundary reachable from its page, its layouts or its fallbacks keeps its page module — and everything only that page imported — out of the client bundle, and is not hydrated. Nothing smaller than a route is split | A page above a boundary still ships whole, because the client re-renders the matched tree from the same modules the server used; dropping one needs a Flight-shaped payload. A dropped route's modules are still imported for their side effects, so their stylesheets are shipped and their code is tree-shaken rather than never resolved. No server action is callable. The client-only check matches API names, so a hook that wraps one is reported as unclassified rather than resolved | #252, #350, #388 |
Streaming a route's data. The renderer streams and _uf.loading.js is a <Suspense> boundary, but resolveMatch still awaits the loader before the render begins | A page that suspends while rendering streams its shell first; a page waiting on its loader shows its fallback for no time. uf dev collects rather than streams, because Vite's HTML transform takes a whole document | #373, #374 |
An error boundary. A component that throws takes the response with it; there is no _uf.error.js | — | #257 |
Middleware. _uf.middleware.js is a reserved name, discovered by the router, reported by uf inspect --json — and never called | Every part except the call | #260 |
| A cache that survives the process, and the other two switches. There is a route cache and a fetch cache now — opt-in per route and per request, with time-based revalidation, an optional stale-while-revalidate window, and invalidation by tag or by path — and they live in the memory of one process, so four servers hold four caches that disagree and a restart empties one | rendering.cache.route and .fetch reach a store; .data and .actions are refused by the config loader rather than reaching a manifest and doing nothing. What is left: a durable store behind the one method a store is, build-time enforcement of "a cached render may not read cookies()" — which uf_rsc could answer and does not — and the join between this and uf build's prerender, which is what ISR is. Caching has the numbers | #277 |
Deno, and every edge runtime. Node.js and Bun load Flow; HostKind::Deno loads none, and Cloudflare Workers, Vercel Edge and Deno Deploy have no host at all | One and a half of the three named hosts | #246 |
Task caching. uf run is sh -c, one task at a time: no input hashing, no parallel dependencies, no workspace filter | The roadmap's "cached, dependency-aware task execution" is not what happens today | #272 |
Test coverage on every host, and a suite split across machines. uf test --coverage measures lines, functions and branches against the Flow source, writes LCOV and Cobertura, and fails a run on a threshold in uf.config.js; --reporter junit writes the results. All of it is Node-only, because it is V8's count written through NODE_V8_COVERAGE, and there is no --shard. Module mocking works on Node — uft.mock and its siblings replace a module through @uniflowed/host's loader hooks — and raises UnsupportedError on Bun and Deno, which have no synchronous module hook | Coverage, LCOV, Cobertura and JUnit exist; Bun and Deno get none of them | #280 |
uf lint --fix. The fix catalogue exists and only an editor can reach it | uf prepare can tell you a file is misformatted and cannot format it | #290 |
AVIF, lossy WebP, and font subsetting. The pipeline itself is done — an imported image is decoded, resized to the declared widths and re-encoded by uf assets, and an imported font is self-hosted and paired with a metric-matched fallback — but the formats it can emit stop at PNG, JPEG and lossless WebP | Every encoder uf ships is pure Rust with no C linked. AVIF needs an AV1 encoder (rav1e, about eighty more crates under a fat-LTO release profile) and lossy WebP needs libwebp, which is C. So a photograph gets several JPEG widths and no modern format: uf encodes the alternative, measures it, finds it larger, and reports the measurement rather than shipping it. Subsetting is a different job again — a glyph closure and a table rebuild, not three tables read | #273 |
Every implemented package on npm. Seventeen names go out on each uf@* tag — the closure a project needs, plus the test runner. Twelve more are implemented and waiting in tools/release/pending-packages.txt, @uniflowed/state and @uniflowed/effect among them | Those twelve resolve only through this repository's workspace, which is how this site imports them | #210 |
The uncomfortable summary: uf builds a static site today. Everything that
needs a server at run time works while uf dev is running and disappears at
uf build.
How to tell a refusal from a gap
A refusal has a reason, written down in
docs/red-lines.md,
and no issue will ever close it. A gap has an issue number and an owner.
If you hit a boundary that has neither — no reason on this page and no issue to link — that is a defect in this page, and worth reporting as one.
Where to go next
uf compared puts the two lists above beside Next.js, Vite,
Bun and create-react-app. Migrating to uf is what moving
actually involves. Architecture is the audit uf keeps
against its own rules.