Readiness: Implemented
Why uf
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.
What you will be able to do: tell a refusal from a gap, know what uf will never do and why, and find the issue for anything it does not do yet.
What you need first: nothing installed. uf compared puts these lists beside other tools.
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 — npm/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 an install-time lifecycle
script, a hook a package manager runs by itself while it installs, before it
fetches anything:
error: package manifest /…/my-site/package.json declares install-time
lifecycle scripts (postinstall), which run code during an install;
move that automation to uf tasks in uf.config.js
The hooks are npm's preinstall, install, postinstall, prepublish,
preprepare, prepare, postprepare, prepack, postpack and
dependencies, plus pnpm's pnpm:devPreinstall. 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.
A script no install runs is not refused. create-expo-app and the React Native
template both write start, android and ios, and those run only when a
person types npm run ios. uf does not run them either, and uf install says
so in one line:
› package.json declares start, android, ios, web; uf does not run package.json
scripts, so project tasks belong in uf.config.js
The refusal used to cover any scripts at all. That refused every Expo and
React Native project while closing no path an install takes, and
#992 narrowed it to the hooks.
A dependency's own install scripts do not run either: by default every manager
uf drives is handed --ignore-scripts. The dependencies you have read can be
named with uf pm approve-builds, which records them in the field the manager
itself reads — pnpm's, Bun's or Yarn's — so the next install builds exactly
those. npm and Yarn 1 have no such list, and uf says so rather than offering an
approval that would quietly mean every package at once.
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 will not add native asset codecs or guess per-use font metadata
uf assets decodes and encodes with pure Rust libraries. That keeps the
pipeline shippable as one toolchain with no C compiler, no native codec
install, and no release-time system dependency. The price is deliberate:
- no AVIF at build time. The AV1 encoder is linked — pure Rust, for the request-time endpoint, where a lossy modern format is the point — and making it a build-time candidate changes what every existing build writes, which is its own decision rather than a side effect of the endpoint;
- no lossy WebP, because the encoder is
libwebp, a C library; - no WOFF2 output for a subsetted face, because uf reads WOFF2 and does not ship a WOFF2 encoder;
- no
textfont subsetting from an import, because the text belongs to one use of a face and an import can only carry Vite's query syntax; - no
font-weightorfont-stylemetadata inferred from a font file name.
The lower-level uf assets protocol can carry some of those choices for a tool
that calls it directly. A JavaScript import cannot say them honestly today, and
uf will not guess. Images, fonts, icons and cards has the
numbers and the browser consequences; #997
is the issue that moved these from gap to refusal.
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 |
|---|---|---|
A deployment proven where it lands. uf build --adapter writes eight targets — node, bun, deno, container, edge, serverless, vercel and static — over one @uniflowed/server/fetch handler, and uf build --compile writes a single executable. Their tests establish that every server target answers a request identically, in process, and CI starts the generated Cloudflare Worker under Wrangler's local runtime | None of that output has been deployed to Cloudflare, AWS or a container platform by uf's own pipeline, so the first real deployment of each target is still somebody finding out. The Deploy parity workflow that would do it is written and skips every platform until the repository has its credentials. serverless buffers a streamed document, and a route that declares a schedule is refused on it because there is no file to write the schedule into | #956 |
The rest of React Server Components. Routes render through React's own Flight payload: a Server Component never reaches the browser, which gets the payload and the "use client" modules it names, and a navigation fetches the next route's payload from <route>/__uf.flight | An inline "use server" closure in a server module is callable and its captures are sealed; one written in a "use client" module is not an endpoint. A form bound to an action passed down as a prop posts only after hydration. The client-only check matches API names, so a hook that wraps one is reported as unclassified rather than resolved | |
The cache around incremental static regeneration. The route cache and the fetch cache are opt-in per route and per request, revalidate on a timer or when a tag or path is invalidated, and survive a restart and serve every process of a deployment when rendering.cache.store names a store; a prerendered page that states a lifetime is served from the build and regenerated in the background once it passes | Explicit cacheFunction calls share the route/fetch store, accept arguments and tags, and reject request-state reads. rendering.cache.actions is refused; a --compile binary carries no cache and renders a regenerating page for every request. Caching has the numbers | #961 |
A worker runtime as a host. Node.js, Bun and Deno run every command — Deno from 2.8, through the same in-thread registerHooks loader a new enough Node uses | Deploying to Cloudflare Workers is a different question with its own answer, --adapter edge. What is missing is running uf's own commands on a worker runtime, which needs a source-level host there. And on Deno a test that imports a package with a native addon fails while the loader is installed, which is Deno's to fix | #246 |
Tasks across a workspace. Tasks are a graph, run up to a concurrency limit and cached on the files each one declares it reads, and uf starts a task's command itself rather than through sh — so a task written as a program and its arguments needs no shell on any platform. uf run <task> -r runs a task in every workspace member in the order their package.json dependencies give, --filter selects members by name, glob, path or what they depend on, member#task in dependsOn names another member's task, and each member keeps its own cache | A command that uses shell syntax is still sh -c, so it still needs a machine with a POSIX shell. A member's outputs are checked rather than restored, and there is no remote cache | #967 |
Test coverage on every host. 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, and --reporter junit writes the results. --shard splits a suite across machines, and --merge-shards reports the parts as one run, coverage included. Coverage is Node-only today: Bun can count the JavaScript its runner executed, not uf's Flow-source report, and Deno writes --coverage profiles without the source-map cache uf reads. Module mocking works on Node and on Deno 2.8 and newer — uft.mock and its siblings replace a module through @uniflowed/host's loader hooks — and raises UnsupportedError on Bun, which has no synchronous module hook | Bun runs a suite with neither uf coverage nor module mocking and Deno with no uf coverage | #1100 |
The uncomfortable summary used to be that uf builds a static site. It is a different one now: uf builds a server application and writes it for seven kinds of host, and none of those deployments has been proven outside uf's own tests and CI.
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
Architecture is next: the red lines behind the refusals
above, and the audit of where uf does not yet keep them. uf
compared puts both lists beside Next.js, Vite, Bun and
create-react-app, and Migrating to uf is what moving
actually involves.
Edit this pagedocs/app/guide/scope/$page.mdx