Readiness: Implemented
Reference
uf.config.js
The only config file. It is Flow, so defineConfig type-checks it where you
write it, and every key below has a default — a project that wants uf's
opinions can leave the file nearly empty.
"use flow";
import { defineConfig } from "@uniflowed/config";
export default defineConfig({
app: {
router: { entry: "app.js", root: "app" },
},
});
uf inspect --json prints the config as uf resolved it, defaults filled in,
which is the fastest way to answer "what is it actually using".
An editor running uf lsp completes the keys on this page as you type them —
each with the documentation @uniflowed/config declares for it, and the values
of any key whose type is a fixed set — and explains a key you hover. See
uf lsp.
Top level
| Key | What it configures |
|---|---|
uf | The release of uf the project runs on: "0.3.0". See below |
runtime | The runtime commands run on when no key nearer the command names one: "node@24". See below |
packageManager | The manager uf install and the other package commands drive: "pnpm@10" |
app | The application: router, runtime, React, rendering, built-ins |
build | The production build: entries, output, budgets, source maps, and the runtime and builder it runs |
builder | Deprecated: builder.module is build.builder now |
dev | The dev server: host, port, file access, allowed hosts |
docs | The documentation build, if the project has one |
site | Where the built site is served from, and what uf build writes for a crawler |
env | The .env files and the default mode. Its toolchain is deprecated |
fmt | uf fmt |
flow | The official checker and editor server release. See below |
ignore | Paths no command walks into: uf fmt, uf lint, uf check, uf test, uf doc |
lint | uf lint and Flow's built-in lints |
test | uf test, and the runtime and runner it runs on |
permissions | What the project's own code may reach. Absent means no limit |
plugins | Vite plugins, appended to uf's own |
vite | Vite's own configuration, merged over uf's |
tasks | Named tasks for uf run |
staged | The tasks uf prepare runs over the staged files before a commit |
ui | Where uf ui add writes the components a project owns. See below |
taskRunner | Whether uf run hands a task with no command to Vite+. See below |
pm, publish, release | Which registry uf reads from and publishes to, and how a release is tagged |
vrt | Visual regression tests |
uf
The release of uf itself, the way rust-toolchain.toml pins Rust.
export default defineConfig({
uf: "0.3.0",
});
Any uf, ufr or ufx started inside the project — whichever release is on
PATH — hands the command to uf@0.3.0, installing it into the install store
first with the same download checks as uf self-update. Nothing global
changes: ~/.local/bin/uf stays where it was, and outside the project the
machine's own uf runs.
| Written | Means |
|---|---|
"0.3.0", "0.0.0-alpha.35" | Exactly that release |
"0.3", "^0.3.0", "latest" | Refused. The message names the release to write |
| absent | Whichever uf was started, as before |
uf self-update, uf self-uninstall and uf use manage the machine's uf and
never follow the pin. For one command, UF_TOOLCHAIN=0.4.0 runs another release
instead of the pinned one, and UF_TOOLCHAIN=current runs the uf that was
started.
runtime and packageManager
The tools a project runs on, each written name[@version] and declared where
it is used. This pair is the default; build.runtime, test.runtime and
test.runner name a tool for one command.
export default defineConfig({
runtime: "node@24",
packageManager: "pnpm@10",
build: { runtime: "node@26" },
test: { runtime: "bun@1.4" },
});
| Key | Names | Read by |
|---|---|---|
runtime | node, bun or deno | uf start, uf run and uf exec, and every command whose own key is absent |
build.runtime | node, bun or deno | uf dev, uf build and uf preview, before runtime |
test.runtime | node, bun or deno | uf test, before the runtime test.runner brings and then runtime |
packageManager | npm, pnpm, yarn or bun | uf install, uf add, uf remove, uf update and the other package commands, before pm.packageManager, package.json#packageManager and the lockfiles |
| Written | Means |
|---|---|
"node" | Whichever node is on PATH. A machine without one is refused rather than handed another runtime |
"node@26" | The newest release with that prefix, resolved the first time a command needs it and locked in uf.lock |
"node@26.8.2" | Exactly that release |
"node@^26", "node@>=24" | Refused: a range can resolve to a different release tomorrow. The message names the prefix to write |
A versioned tool runs from the store every project on the machine shares,
installed the first time a command needs it, with its directory first on PATH
for everything the command starts. With none of these keys, a command starts
the host app.runtime.capabilityJsHost finds and drives the manager
Dependencies describes, as it always
has. uf inspect and uf explain <command> print each tool, the key it came
from and the release it is locked at. Environments is the whole
of it.
These names are command hosts, not deploy adapters. Cloudflare Workers and
workerd are reached through runtime.deploy.adapters and
uf build --adapter edge, and the generated Worker is smoke-tested under
Wrangler; they are not valid test.runtime values.
app
| Key | Default | Meaning |
|---|---|---|
router.entry | "app.js" | The module exporting routerView() |
router.root | "app" | The directory scanned for routes |
router.enabled | true | Turning it off makes the project a library, and makes uf build build one |
router.redirects | [] | { source, destination, permanent }: a redirect answered before anything else, the first match winning — see below |
router.rewrites | [] | { source, destination }: the route at destination served at the path that was asked for |
router.headers | [] | { source, headers }: response headers by path, every match applied |
router.basePath | none | The path the application is served under, such as "/docs". Every address uf writes for a browser carries it, and a request outside it is a 404 — see below |
router.trailingSlash | "ignore" | Which spelling of a path is the page: "never" redirects /about/ to /about, "always" redirects /about to /about/, and "ignore" answers both |
runtime.default | "node" | The runtime this project is written for. node, deno and bun; naming a runtime uf has no host for is an error that says which key was meant (#246) |
runtime.compatibility | ["node", "bun", "deno"] | The other runtimes it states it runs on, from the same three. Written into .uf/install.json as the hosts that must be available |
runtime.capabilityJsHost.default | "node" | The host a command starts when no tool key names one. uf dev, uf build, uf preview, uf start and uf test resolve through this key, not through runtime.default, when runtime and the keys nearer the command are absent. uf build --compile embeds its runtime from this key whatever they say (#1036) |
runtime.capabilityJsHost.hosts | ["node", "deno", "bun"] | Hosts uf may use |
runtime.capabilityJsHost.autoDetect | true | Pick a host by what is installed |
runtime.deploy.adapter | none | The deploy target uf build writes an artefact for; --adapter overrides it |
runtime.deploy.adapters | ["node", "bun", "deno", "edge", "serverless", "static", "container", "vercel"] | The targets this project may build for. All eight are implemented; removing one here refuses that target for this project |
runtime.deploy.enabled | true | Setting it to false refuses --adapter rather than ignoring it |
react.* | — | React Compiler mode and React-specific options |
react.strictMode | true | uf dev hydrates inside <StrictMode>; a build never does (#516) |
builtins.markdown | on | Markdown and MDX |
builtins.markdown.mdx.highlight.enabled | true | Highlight fenced code during the build |
builtins.markdown.mdx.enabled | true | false turns uf's .mdx compilation off |
builtins.markdown.mdx.jsxImportSource | "react" | The package compiled MDX imports its JSX runtime from, as <package>/jsx-runtime. "@uniflowed/jsx-runtime", which has no runtime in it, is refused |
builtins.markdown.mdx.highlight.themes.light | "github-light" | The Shiki theme for a reader who prefers light. A theme left out keeps its default |
builtins.markdown.mdx.highlight.themes.dark | "github-dark-dimmed" | The Shiki theme for a reader who prefers dark |
builtins.markdown.mdx.highlight.langs | [] | Grammars beyond the ones a uf project uses |
builtins.fonts.enabled | true | Whether uf self-hosts an imported font. false leaves font imports to Vite |
builtins.fonts.display | "swap" | The font-display written into every @font-face uf generates |
builtins.fonts.fallback | "Arial" | The local face scaled into a metric-matched fallback. "Arial", "Times New Roman" and "Courier New" are the faces uf has metrics for; any other name gets no fallback, and the build says so |
builtins.images.enabled | true | Whether uf resizes and re-encodes imported images, and serves remote ones. false leaves image imports to Vite and turns the endpoint off |
builtins.images.widths | [640, 750, 828, 1080, 1200, 1440, 1920] | The widths a layout asks for: one variant per width no wider than the source, plus the source's own. They are also the only w= the image endpoint answers |
builtins.images.quality | 75 | The encode quality for imported images, and the endpoint's default q= |
builtins.images.qualities | [] | Other q= values the endpoint answers, each 1 to 100 |
builtins.images.placeholder | true | Whether each imported image gets a blurred placeholder |
builtins.images.remotePatterns | [] | The remote images /__uf/image may fetch, as { protocol?, hostname, port?, pathname? }: protocol is "https" unless it says "http", hostname may start *. for one more label or **. for any number, and pathname uses * for a segment and ** for any number. Empty means there is no endpoint at all; uf build --compile refuses a non-empty list. See remote images |
builtins.images.transformer | none | A module exporting createImageTransformer, for a target with no encoder of its own. Required with remote patterns for --adapter node, bun, deno, container and serverless |
builtins.images.dangerouslyAllowPrivateAddresses | false | Let the endpoint fetch from loopback, private and link-local addresses. For a test that serves its own images; never for a deployment |
builtins.fonts.subset | "none" | "ranges" splits a family into script buckets with exact unicode-range values. "none" is the default because subsetting is lossy and a build cannot see the text a user will type; "ranges" loses nothing (guide) |
builtins.fonts.preload | true | Whether Font preloads the primary face. Exactly one face is preloaded however many buckets a split produced |
builtins.icons.enabled | true | Whether uf:icon/… resolves |
builtins.icons.dir | "icons" | Where uf:icon/<name> looks for <name>.svg. A directory rather than a claim on .svg, which stays Vite's |
builtins.og.enabled | true | Whether *.og.json templates are drawn |
builtins.og.font | none | The typeface every card is drawn with. uf embeds none, so a project that draws cards names one |
builtins.reactCompiler | on | The official React Compiler |
builtins.reactCompiler.enabled | true | false compiles every module without the React Compiler, and switches off react/no-redundant-memo, which only makes sense with it on |
builtins.reactCompiler.mode | "syntax" | The only value |
builtins.relay | true | false skips the Relay graphql tag transform |
builtins.style | "style-x" | "style-x" compiles StyleX into class names and a stylesheet. "none" leaves plain CSS and CSS modules to Vite and does not compile stylex.create; it does not rewrite StyleX source into CSS |
rendering.modes | ["ppr", "ssr", "ssg", "isr"] | Which rendering strategies this project will deploy — see below |
rendering.navigation | "client" | What a link does: the client router takes it over, or the browser fetches a document — see below |
rendering.staleTime | 0 | Seconds the client router shows a page it already fetched without asking again; 0 keeps nothing — see the guide |
rendering.cache.route, .fetch, .data | false | Enable route, fetch and explicit function caches. There is no actions cache: the key is gone, and actions: true is still refused |
rendering.cache.store | "memory" | Where entries live: one process, "filesystem", or a module exporting createCacheProvider |
rendering.cache.storeDir | .uf/cache/route | Where "filesystem" keeps them |
targets | ["web", "react-native", "server"] | Application/runtime surfaces the build must satisfy |
rsc | true | Whether the project builds React Server Components at all. false renders routes without the Flight graph, takes the RSC step out of the pipeline, and turns off partial prerendering and server-action types |
serverActions | true | false stops uf prepare writing the types for "use server" exports |
router.manifest | "router.js" | Where the typed router module is generated, from the project root; native variants are named after it (router.ios.js) |
router.nativeLinks | none | The HTTPS links the iOS and Android apps claim: { origins, routes, iosAppIds, androidPackage, androidSha256 }. uf build checks every claimed route exists on web and on both platforms, then writes apple-app-site-association and assetlinks.json. See React Native |
router.redirects, router.rewrites and router.headers
next.config.js's redirects(), rewrites() and headers(), as three lists:
router: {
redirects: [{ source: "/blog/:slug", destination: "/posts/:slug", permanent: true }],
rewrites: [{ source: "/me", destination: "/users/me" }],
headers: [{ source: "/:path*", headers: { "X-Frame-Options": "DENY" } }],
}
| Field | In | Meaning |
|---|---|---|
source | all three | A path in the route table's grammar: literal segments, :name for one segment and a trailing :name* for the rest |
destination | redirects, rewrites | A path that may use the source's parameters. A redirect may also name an absolute http(s) URL; a rewrite may not |
permanent | redirects | Required. true answers 308, false answers 307 |
headers | headers | An object of header names to values; a later rule setting the same name wins |
Each entry is an exact object, so a misspelled field is an error when the file
is loaded. So is a spelling the matcher would read as a literal — a regular
expression, :slug+ or :slug?, a parameter inside a segment, a query in a
source — and a rewrite to another origin. Every server front door applies the
three the same way; a static deployment refuses them by name.
Redirects, rewrites and response headers
has the order they apply in and the table from next.config.js.
router.basePath and router.trailingSlash
next.config.js's basePath and trailingSlash:
router: {
basePath: "/docs",
trailingSlash: "never",
}
A base path is literal segments starting with / and not ending with one:
"/docs" or "/en/docs". "/", a trailing slash, an empty segment, . or
.., a query or a fragment, and a :name or * pattern are errors when the
file is loaded, each with a sentence that says why. Routes, a Link's to,
redirect() and the three lists above are all written without it.
"never" writes a prerendered page as about.html, and "always" and
"ignore" write about/index.html. Next.js's default is trailingSlash: false,
which is "never" here.
A base path and a trailing slash
has what carries the base and where the two apply.
targets
app.targets is an allowlist of surfaces this project claims to keep working,
not the one build to run when none was requested.
| Value | What it means |
|---|---|
"web" | React DOM routes and browser navigation |
"react-native" | Native route files and the React Native target contract |
"server" | A server-capable application surface |
"hermes" | Reserved for the native JavaScript engine surface |
An ordinary uf or React project defaults to the web application target. A
project with app.framework: "react-native" defaults to the native target.
uf build --target native, ios or android is refused unless this list
contains "react-native", and uf build --target web is refused unless it
contains "web".
export default defineConfig({
app: {
framework: "react-native",
targets: ["react-native"],
},
});
The React Native guide explains the native half: React Native target.
rendering.modes
An allowlist, not a request. It does not ask for a strategy; it says which
ones this project is willing to deploy, and uf build picks between them per
route: a route with no parameters is prerendered, a route with parameters is
prerendered when its page exports generateStaticParams, and anything left
over is rendered per request.
That matters because the two answers are deployed to different places. A prerendered document is a file a CDN serves; a route rendered per request needs a process.
| The list | What uf build does |
|---|---|
contains ssg and ssr (the default) | Prerenders what it can; the rest are rendered by uf start, uf preview or a deploy adapter |
["ssg"] | Prerenders everything, and fails, naming the route, if anything can only be answered per request |
["ssr"] | Prerenders nothing. Every route is rendered per request |
["csr"] | Prerenders one shell that is no route's document, and the browser renders every route from it |
contains isr (the default does) | Prerenders what ssg would, and regenerates a prerendered page that states a lifetime with cacheLife once the lifetime passes, when rendering.cache.route is on and a server is deployed |
contains ppr (the default does) | Prerenders what ssg would, and writes a page that reads cookies(), headers() or draftMode() inside a <Suspense> boundary as a static shell whose boundary a server renders per request, when a server is deployed and routes render as Server Components |
isr allows what ssg allows, because a regenerated page is a prerendered
document with a lifetime: ["isr"] alone prerenders every route. The
rendering guide has what regeneration does on each target.
ppr allows what ssg allows for the same reason: a page that reads nothing
from the request has no hole, and is a static document. Without ppr a
prerendered page that reads the request fails the build, naming the route and
the chain of imports that reaches the read; with it, the read decides the
page's shell, and only a read outside every <Suspense> boundary fails. The
rendering guide has what each
target does with a shell. A list that allows nothing, [], is refused while the
config is read.
csr is the one value that cannot share the list. The others answer "where
does this route's document come from", which is what makes the list a set the
build picks from per route; csr answers it for the whole application with a
document that belongs to no route, so there is no per-route decision left to
make. ["csr", "ssg"] is refused while the config is read, and so is csr
beside build.staticBuild, which prerenders every route where this prerenders
none. It is also the one value not in the default list. A csr build
refuses, by name and before the bundle, any route that needs a server — a route
handler, a middleware, a callable server action, or a module the application
reaches that imports @uniflowed/server, @uniflowed/db or server-only. The
guide has the two files it writes and the rewrite rule a
host needs.
Three things can only be answered per request, and under ["ssg"] each is an
error naming the route: a page with parameters and no generateStaticParams, a
$route.js handler, and anything under a $middleware.js.
A page can also say so for itself:
// app/now/$page.js
export const dynamic = "force-dynamic";
which is how a route with no parameters keeps out of the prerender —
generateStaticParams cannot say it, because there are no parameters to
generate. "auto" is the default. Next.js's "force-static" and "error" are
constraints uf does not check yet, and a page that names one is refused rather
than quietly rendered as "auto".
rendering.navigation
A different question from modes, which is why it is a second key rather
than a fifth value in that list: modes says where a document comes from and is
decided per route, and this says what the browser does once it has one.
| Value | What a link does |
|---|---|
"client" (the default) | The router takes a plain left click over, resolves the next route and commits it into the page that is open |
"document" | Nothing of uf's: the browser follows the link, the way it follows one on a page with no JavaScript at all |
Under "document" a Link renders the same anchor with no click handler and
no prefetching, useRouter().push and .replace become location.assign and
location.replace, refresh() is a reload, and no popstate listener is
installed. The page still hydrates, so a "use client" component is still
interactive — this is the client router minus the takeover, not the browser
minus JavaScript. It composes with every value of modes: a static site of
documents is ["ssg"] with "document", and a server-rendered application in
the older sense is ["ssr"] with it.
The mode is written into the client entry as a constant with no isProduction
beside it, so uf dev behaves the way the deployment does. uf build prints a
navigation row when it is not the default, uf explain build names it on the
bundle stage, and .uf/build/meta/uf-build-manifest.json carries it as
rendering.navigation. The guide has what it buys and what
it does not.
dev
| Key | Default | Meaning |
|---|---|---|
host | "127.0.0.1" | Interface to bind |
port | 5173 | Port to listen on |
strictPort | false | Fail rather than move to the next free port. uf dev --port is always strict, whatever this says |
allowedHosts | [] | Host headers accepted when binding a routable address |
fs.allow | [] | Extra roots the dev server may serve from |
fs.deny | [] | Globs it may never serve; deny beats allow |
--host refuses to bind a routable address while allowedHosts is empty. A dev
server reachable from the network with no host allow-list is a file server for
your source tree; the reasoning is in
docs/security.md.
build
| Key | Default | Meaning |
|---|---|---|
runtime | runtime | What uf dev, uf build and uf preview run the builder on: "node@26" |
builder | "vite" | The builder those three drive. "vite" is @uniflowed/vite; anything else is a module, resolved as below |
entries | ["app.js"] | Entry modules |
outDir | "dist" | Output directory |
sourcemap | true | Emit source maps |
staticBuild | false | Prerender everything and emit no server bundle — see below |
lib | unset | What a library build writes — see below |
budgets | unset | Size budgets that fail the build when exceeded |
budgets.total | unset | Every emitted asset together |
budgets.initialJs | unset | Per route: the JavaScript loaded before its first render |
budgets.perRoute | unset | Per route: everything it loads |
budgets.perAsset | unset | Each emitted asset on its own |
Budgets are unset on purpose: failing a build nobody asked uf to police is worse
than reporting the size and moving on. Each is { max, metric? }: max is a
byte count or a string such as "180kb", and metric is "raw", "gzip" —
the default — or "brotli". A library build is held to them too.
build.lib
uf build builds two different things, and which one it builds is
app.router.enabled. A project that has turned the file-system router
off is a library: uf build compiles its entries to the output directory
and does none of the application work — no route table, no client entry, no
server bundle, no prerender. uf explain build says which build will run and
which key decided.
That is the whole declaration. build.lib is only for a library that needs
more than the defaults, and the defaults are what uf new --lib scaffolds:
| Key | Default | Meaning |
|---|---|---|
entries | ["index.js"] | The modules to build. internal/parse.js is written to dist/internal/parse.js — the path, so two entries cannot collide |
formats | ["es"] | "es" writes <name>.js; "cjs" writes <name>.cjs, for a consumer that calls require. "umd" and "iife" are refused by name: each needs a global name per entry, and what a Flow library's global should be is not a decision uf has made |
external | [] | Extra package names to leave as imports |
declarations | true | Write a TypeScript declaration file beside each entry — see below |
Dependencies are external by default, which is the opposite of the
application build: everything in dependencies, peerDependencies and
optionalDependencies, plus the host's built-in modules, stays an import. An
application is the end of the line and a library is not — a bundled copy of
React inside a library is a second React in every application that installs it.
external is for what a manifest cannot say.
build.lib in a project whose router is on is refused while the config is
read. One project is one kind of build, and two keys disagreeing about which is
exactly the failure that refusal exists for.
uf build --compile and uf build --adapter are refused for a library, by
name and before anything is built. Both write a deployment that serves an
application; a library is imported.
What a library publishes
A Flow library ships both halves — the Flow source, and the JavaScript uf compiled from it — and names them with export conditions:
{
"exports": {
".": {
"flow": "./index.js",
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
},
"files": ["index.js", "dist"],
"sideEffects": false
}
There is no .js.flow, here or anywhere in uf: a shipped module owns its own
declarations, so the Flow source is the declaration file.
default is the compiled build because a resolver that knows no conditions
takes default, and that file has to be one every runtime can evaluate — which
is what makes a uf library installable by someone who has never installed uf.
files has to name the output directory: npm falls back to .gitignore when a
package has no .npmignore, and dist/ is in the .gitignore uf scaffolds,
so a library that left it out would publish the source and not the build.
The full argument, including why uf's own @uniflowed/* packages ship source
and nothing else and why a uf application does not select the "flow"
condition yet, is in
Architecture.
TypeScript declarations
Most people who install a Flow library write TypeScript. Without a .d.ts the
package is any to every one of them: no completion, no checking, and a rename
of the library's API that breaks nothing at their build and everything at
runtime. So uf build translates the exported Flow types of each entry into a
declaration file beside the JavaScript — index.js becomes dist/index.d.ts,
internal/parse.js becomes dist/internal/parse.d.ts — and "types" in
exports is what a TypeScript consumer reads to find them.
The "types" condition goes before "default", because TypeScript takes
the first condition it matches. A library that builds declarations and does not
name them in exports is the failure hardest to notice — everything is built,
published and installed, and consumers are still on any — so the build says
so rather than leaving it to be discovered.
Translation is honest rather than total. Flow has constructs with no
TypeScript meaning, and the rule is to translate what maps cleanly, refuse to
guess at what does not, and name every refusal in the build report. A .d.ts
in which everything quietly became any type-checks perfectly and is worthless;
an author told "these three exports could not be translated" is far better
served. What is refused is published as unknown rather than any, so a
consumer cannot use it by accident.
What maps exactly:
| Flow | TypeScript |
|---|---|
mixed, empty | unknown, never |
?T | T | null | undefined — Flow's maybe type admits both |
+x: T, -x: T | readonly x: T; a write-only property has no counterpart |
+T, -T on a type parameter | out T, in T |
$ReadOnly, $ReadOnlyArray, $NonMaybeType | Readonly, readonly T[], NonNullable |
$Keys, $Values, $ElementType | keyof T, T[keyof T], T[K] |
x is T | x is T |
What is reported instead of guessed at — exact object types, object spreads,
$Diff/$Shape/$ObjMap-style utilities, Flow enums, component syntax,
renders, React types, write-only properties, and bindings whose type Flow
infers. opaque type is the one that is neither: it is published as a branded
type, because nominality is what opaque means from outside the module and
publishing the underlying type would let a consumer build one out of thin air —
the exact call Flow exists to refuse.
The build prints each one with the export it is in:
declarations
files 3
gaps 2
where export construct
index.js:12 makeId opaque-type
index.js:31 Options exact-object
The reason for each — what the consumer gets instead, in a sentence — is in
.uf/build/meta/uf-build-manifest.json under declarations.gaps.
Set declarations: false to publish none. It is for a library that would rather
ship nothing than ship declarations with gaps in them; it is not a way to stop
hearing about the gaps, which is what a project reaching for it usually wants.
uf build --watch rebuilds a library's JavaScript, declarations and manifest
when its source changes, including added and deleted files. A failed rebuild
prints its error and keeps watching so the next edit can repair it. Output,
node_modules and tool caches do not trigger another build. Application projects
use uf dev instead.
build.staticBuild
The same declaration as rendering.modes: ["ssg"], made about the artefact
rather than about the routes: this build prerenders every route, and what it
leaves behind is documents.
The server bundle is still built — the prerender renders through it — and then
removed, so .uf/build/server/ is empty when the build finishes. Two things
follow:
uf startrefuses, by name, rather than failing later on a missing file. It is the one command that loads that bundle, and for a project deploying documents there is no deployment that runs uf at all. Deploy the output directory to any static host, or runuf preview, which serves it the way one would.- a
$route.jsor a$middleware.jsanywhere in the project is a build error, which is the refusal #342 asked for: a guard that can never run is a guard that looks enforced and is not. - a
"use server"export the browser can reach is a build error too, and it is the one that is not a route. The client bundle carries a server reference for it — an id and afetch— so the button is wired whether or not anything answers; a deployment of documents is the case where nothing does. Keep the module out of the client's reach, or dropstaticBuild.
uf build --adapter <target> and uf build --compile still work, and that is
not an oversight. Each links the application again from source and reads none
of the removed bundle, so each still produces a correct artefact — a static
site inside a Worker, or inside one executable, is a deployment somebody wants.
What staticBuild describes is what the ordinary build leaves behind. Those
two builds are also the ones the server-action refusal does not apply to: what
each writes can answer an endpoint, so there is no unanswered one to refuse.
staticBuild: true alongside a rendering.modes that does not allow ssg is
refused while the config is read. Neither declaration is wrong on its own and
there is no behaviour that honours both.
uf build also writes files beside the output directory, in
.uf/build/meta/: uf-build-manifest.json, uf-rsc-manifest.json,
uf-bundle-report.json and openapi.json. They are notes about the build
rather than part of it — between them they name every route including the ones
nothing links to, the source file behind each one, the size of every chunk and
the route-handler schema contract — and everything in the output directory is
served to whoever asks for it.
site
| Key | Default | Meaning |
|---|---|---|
url | unset | The origin the site is served from, absolute and with no trailing slash |
sitemap | true | Write sitemap.xml |
robots.enabled | true | Write robots.txt |
robots.allow | [] | Allow: lines |
robots.disallow | [] | Disallow: lines |
site: {
url: "https://docs.uniflowed.dev",
},
url is the switch. Without it neither file is written, because a <loc> has
to be an absolute URL and nothing in a bundle knows the host it will be
deployed to — and a sitemap naming the wrong host is worse for a site than no
sitemap at all. With it, uf build writes:
sitemap.xml— one<loc>per document the prerender wrote, minus the error document and minus anything under a$middleware.js. A route with parameters and nogenerateStaticParamsprerenders nothing, so it is absent by construction: nothing, this build included, knows what its URLs are. There is nolastmod,changefreqorpriorityon any entry — none of the three is a fact the build has, and alastmodthat is the deploy time for every URL is one search engines learn to ignore.robots.txt— but only when it would say something. Arobots.txtthat disallows nothing is exactly equivalent to not having one, so what makes it worth writing is theSitemap:line or a rule you asked for. With neither, no file.
A guarded route is left out of the sitemap and is not added to robots.txt:
Disallow: /admin publishes /admin to everyone who fetches the file, and a
path you did not want advertised is not made safer by being written into the
one file every crawler reads first.
A public/sitemap.xml or public/robots.txt you wrote yourself is never
overwritten. The build says which file it left alone.
The origin is not metadataBase, which is a route's own
metadata and is what the renderer resolves a
relative og:image against. The two are read by different things at different
times and a site that has both should keep them the same.
env
| Key | Default | Meaning |
|---|---|---|
active | "" | The mode when nothing else says. Empty means the command decides: development for uf dev, production for uf build, uf preview and uf start, test for uf test |
files | [] | The .env files to read, instead of the cascade. Empty means .env, .env.local, .env.<mode>, .env.<mode>.local, in that order, the later file winning |
toolchain | {} | Deprecated. The runtimes and package managers this project pins, by exact version — { node: "24.14.0" }. uf env install still installs them; declare each where it is used instead, as runtime, build.runtime, test.runtime or packageManager. A tool this key and one of those name at different releases is an error naming both keys |
--mode beats active, and so does the profile uf env use wrote. A variable
already set in the environment beats every file. Only a name starting with
VITE_ — or Vite's own envPrefix, if the project sets one — reaches browser
code; everything else is the server's. Environments is the whole
of it.
permissions
Absent, and there is no limit — the toolchain uf has always been. Present, and
it is deny by default: the project's code gets what this block lists and
nothing else. permissions: {} is a legitimate thing to write and means
"nothing beyond what uf itself needs".
| Key | Default | Meaning |
|---|---|---|
read | [] | Paths the project's code may read |
write | [] | Paths it may write |
net | [] | Hosts it may reach, optionally with a port |
env | [] | Environment variables it may read |
run | [] | Programs it may start |
// @flow
export default defineConfig({
permissions: {
read: ["./fixtures"],
write: ["./.uf"],
},
});
The set is uf's, not a runtime's, and each host is handed as much of it as it
can enforce — or refuses the run. Node.js enforces read and write; Deno
enforces all five; Bun has no permission model, so a project that declares one
and runs on Bun is stopped with a message naming the hosts that can.
uf explain test prints which host is enforcing what.
docs/hosts.md
is the whole table, including the two grants uf makes for itself on Node and why
they cannot be scoped.
Two things worth knowing before you write one:
- It does not narrow access to the project's own files. A host has to read the project to run it, so those reads are uf's and are added to yours. What the set denies is the rest of the machine.
- A misspelled key is an error. Every other block here ignores a key it does
not recognize; this one refuses, because a dropped
netwould leave a project believing it had declared a permission nobody granted. - A compiled binary carries the set too.
uf build --compileon Node bakes--permissioninto the executable, so it is in force on a machine uf will never see; on Bun there is nothing to bake it into, so the compile is refused rather than producing an unrestricted binary. That artefact is the one place uf adds no grants of its own — it loads no project — so what you declared is the whole of what it may reach.
fmt
| Key | Default | Meaning |
|---|---|---|
lineWidth | 100 | Where the printer wraps |
indentWidth | 2 | Spaces per level |
align | true | Align related match arrows, component hook bindings, object keys, multiline parameter names, type aliases and object type fields; set false to keep one space |
quotes | "double" | Quote style |
semicolons | true | Keep semicolons |
nonFlow.formatter | "biome" | Who formats JSON, CSS and TypeScript: "biome", "prettier" or "none" |
nonFlow.arguments | [] | Extra arguments, passed to that formatter verbatim |
ignore | [] | Paths uf fmt leaves alone, on top of the top-level ignore |
nonFlow.arguments is a list of strings and not a shape, on purpose. uf does
not re-declare another tool's options — every option it copied would be one
that needed a uf release before anybody could use it — and it does not have to,
because an argument list reaches every option biome or prettier has now or adds
later.
A Tailwind 4 project needs biome's CSS parser told about @theme:
export default defineConfig({
fmt: { nonFlow: { arguments: ["--css-parse-tailwind-directives=true"] } },
});
and needs no second configuration file for it.
The arguments go after uf's own, so they win: a project that wants a line
width other than fmt.lineWidth for its stylesheets can say so. The one thing
they may not do is turn a check into a write — --write, --fix and their
spellings are refused where the configuration is read, because uf fmt --check
changing files is the failure nobody sees: a green CI job and a rewritten tree.
fmt.ignore is for a path the other commands should still read but uf fmt
should not print from here. The usual case is a nested project with its own
uf.config.js and its own fmt settings: it is formatted from its own
directory, and a run from the parent would reprint it in the parent's style.
It is still linted, type-checked and tested from the parent, which the
top-level ignore would stop.
export default defineConfig({
fmt: { align: false, ignore: ["examples"] },
});
Entries read the way the top-level ignore reads them: a bare name matches a
directory at any depth, a path names one place. The list only adds to the
top-level one. An empty entry, an absolute path or one that leaves the project
with .. is refused where the configuration is read, because it could never
match anything.
ignore
Paths no command walks into. One list at the top level, because every command
that walks the project reads it: uf fmt, uf lint, uf check, uf test and
uf doc.
export default defineConfig({
ignore: ["vendor", "app/generated"],
});
A bare name — dist, node_modules — names a kind of directory and matches
wherever it appears, because a build directory two levels down is still a build
directory. A path — app/generated — names one place and matches as a prefix,
which is what somebody writing a path means.
Unset takes uf's own list, ["node_modules", "dist", "target"]. ignore: [] is
a different instruction and means it: a project that has read that list and
wants none of it. .uf and .git are not on either list and are not yours to
opt back into — they are uf's own working directory and git's.
Most projects need none of this. .gitignore is read first and covers the
common case; see what counts as your project.
Migrating from lint.ignore
This lived under lint until
#575, where it was named after
one of the five commands that obey it — so a directory excluded from
formatting had to be written under lint, and nothing about the name said so.
lint.ignore keeps working for as long as alpha lasts, and the five commands
above each say once which key to move it to. Move the list; nothing else
changes.
lint
| Key | Default | Meaning |
|---|---|---|
rules | uf's table | Rule name to "off", "warn" or "error", merged over uf's table. Flow's own lints are the flow/* rules in it |
ignore | unset | Deprecated spelling of the top-level ignore, read only when that is absent (#575) |
rules is changes to uf's table, not the whole of it. Naming one rule
changes that rule and leaves the rest at the level uf ships:
export default defineConfig({
lint: { rules: { "flow/unclear-type": "warn" } },
});
still runs every other rule, flow/syntax among them. To switch a rule off,
say so — "off" — because a rule you did not mention is not a rule you asked
to stop running, and a linter that quietly shrank to one rule reports a clean
project either way (#475).
lint.ignore was read by uf lint, uf fmt, uf check, uf test and
uf doc alike, which its name did not say. The list it always was is the
top-level ignore; the old key still works and every one of those
five commands says so once.
What counts as your project
Every command that walks the project — uf fmt, uf lint, uf check,
uf test, uf doc — reads the same three things, in this order:
-
.gitignore. A file git is told to ignore is generated output, and generated output is not yours to format, lint or check. This is the list you already keep, so the commands agree by construction rather than by four configuration keys you have to hold in step..ignorefiles are read too, and negations (!kept.js) work, because a.gitignoreis a program rather than a list and half-reading one is worse than not reading it.Your global gitignore is deliberately not read: two people on one repository have to get the same answer from
uf fmt --check. Neither is a.gitignoreabove the project root, which belongs to whatever the project is sitting inside. -
.ufand.git, always, whatever the configuration says. -
ignore, for anything left. One list at the top level, because all five commands read it;lint.ignoreis the spelling it used to have and still answers to.
A directory holding a .git of its own is another repository — a submodule,
or a checkout that happens to live inside yours — and is never walked.
Naming a path overrides .gitignore. uf fmt src/wasm reads src/wasm
whether or not git is told to ignore it: the ignore file says what your source
is, not what you may ask about, and a generated file is exactly the thing you
point at when you want to know why it will not compile. uf lint, uf check
and uf test do the same, and it is where rg, prettier and biome draw the
line too. .uf and .git are the exception: pointing at them does not make
them yours.
Flow defaults every built-in lint to off. uf does not: a linter nobody
switches on catches nothing. Every rule's level and the reason for it is in
uf's rule table, and a test asserts the table matches the descriptors.
test
| Key | Default | Meaning |
|---|---|---|
runtime | the runner's, then runtime | What uf test runs tests on: "bun@1.4". It names Node, Bun or Deno, not workerd or an edge adapter |
runner | "uf" | The runner. "uf" is the one built into uf. "bun@1.4" runs the suite with bun test and implies Bun 1.4 as the test runtime (Choosing a runner); beside a runtime naming anything else it is an error naming both keys |
module | "@uniflowed/test" | The module test files import |
splitFiles | false | Run a file long enough to hold up the run as shares on several workers, each importing it and running its own run of cases. Only for a suite whose cases do not rely on the ones above them |
coverage.enabled | false | Collect coverage on every run, without --coverage |
coverage.directory | "coverage" | Where the reports are written |
coverage.reporters | ["text", "lcov"] | text is the terminal table; lcov and cobertura are files |
coverage.include | [] | Keep only files whose path contains one of these; all of them when empty |
coverage.exclude | [".test.", ".spec."] | Drop files whose path contains one of these. A test file's own coverage is a hundred per cent by construction |
coverage.thresholds | {} | lines, functions, branches the project as a whole must reach, as whole percentages |
coverage.thresholds.lines, .functions, .branches | unset | Each one on its own; unset is not checked |
coverage.perFileThresholds | {} | The same three, required of every single file |
coverage.perFileThresholds.lines, .functions, .branches | unset | Each one on its own, per file |
target | "auto" | Which application host uf test targets: "web", "react-native", or "auto", which follows app.framework. Unset, the deprecated runner.applicationTarget is read |
The object form of runner — { applicationTarget } — is deprecated in favour
of runner: "uf", and applicationTarget is the one field of it uf reads. An object whose applicationTarget differs from what
app.framework implies is the exception: uf says to keep it until
#953 gives that setting
another spelling.
A threshold that is not reached fails the run. They are checked whenever
coverage was collected, so enabled: false plus --coverage still gates — the
number CI fails on is the number a laptop fails on. A metric nobody names is not
checked, which is not the same as requiring zero.
ui
| Key | Default | Meaning |
|---|---|---|
directory | "app/components/ui" | Where uf ui add writes the components a project owns, and where uf ui list, uf ui diff and uf ui update look for them |
The path is relative to the project root. Every component goes in the one
directory, because each imports the components it builds on as siblings:
./button.js. A path that leaves the project, such as ../shared/ui, is
refused before anything is written. A uf.config.js arrives with a cloned
repository, and it does not get to point uf ui add at files outside the
checkout.
builder
Which builder uf dev, uf build, uf preview and uf start drive. The key
is build.builder, beside the build it describes. builder.module still
works, with a deprecation naming the key to write, and a project that writes
both with different answers is refused naming both keys.
| Key | Default | Meaning |
|---|---|---|
build.builder | "vite" | The module implementing the builder contract. "vite" is @uniflowed/vite |
builder.module | "@uniflowed/vite" | Deprecated. The same module, under the old key |
export default defineConfig({
build: { builder: "./tools/my-builder" },
});
Vite is the default, not a dependency. A package name is resolved by
walking up node_modules the way a import is; a path starting with . or
/ is resolved from the project root and refused if it leaves it, which is how
a builder is tried before it is published.
A builder is a directory whose package.json declares one:
{
"uf": {
"builder": {
"driver": "./driver.js",
"preload": { "bun": "@uniflowed/host/bun-preload" }
}
}
}
driver is the module uf spawns on the build runtime — build.runtime, then
runtime, then the project's Capability JS Host — and preload names what to
--preload when that runtime is Bun, which is how a builder that transforms
Flow installs its hooks on a runtime with no module.register. Both are optional; ./driver.js is assumed when neither is
declared, so a builder published before this key existed still runs.
What uf hands that driver and what it expects back — the subcommands, the
arguments and the one-JSON-event-per-line vocabulary — is the contract, and it
is written out in
docs/architecture.md.
uf explain build names the builder that will run and its version.
Which build of a builder runs is the same key. uf pins the Vite that
@uniflowed/vite depends on, and a project that needs another one names a
module of its own — a directory that re-exports uf's driver beside the
dependency it wants — rather than forking uf. uf explain build prints the
version it resolved, so the pin is checkable without running a build.
Readiness. The seam is Implemented and @uniflowed/vite is the only
builder uf ships. A second production builder — Rolldown, rspack, esbuild — is
Planned and is separate work; crates/uf_cli/tests/fixtures/paper-builder
is a conformance fixture rather than a builder to use.
vite
Vite's own configuration, merged over the one uf generates. uf does not read it, which is the point: an option added to Vite tomorrow works in a uf project tomorrow rather than after a uf release that names it.
export default defineConfig({
vite: {
server: { warmup: { clientFiles: ["./src/big-module.js"] } },
build: { target: "es2022" },
resolve: { alias: { "~": "/src" } },
},
});
Objects merge key by key and arrays replace, so server.fs.allow means that
list rather than that list plus uf's. Three things stay uf's whatever this
says: root and configFile, because a vite.config.ts beside
uf.config.js is two files disagreeing about one project; and uf's own
plugins, which are kept and put first — dropping the Flow transform would
leave a project whose source no longer compiles, which is not something anyone
means to configure.
Everything else is yours to set, including options uf sets itself. A default is a convenience, not an architecture. The reasoning is in Architecture.
One of those is cacheDir, where Vite keeps pre-bundled dependencies. uf sets
it to node_modules/.vite inside the project, for uf dev, uf build and
every other command that runs Vite. That is where Vite puts it for a project
with its own package.json. Vite's own default is the nearest package.json's
node_modules/.vite, and a uf project needs no package.json, so without this
a project inside another repository would pre-bundle into that repository's
node_modules, shared with every other project there.
pm and publish
Two settings that used to be one. publish.registry is where uf publish
pushes a package; pm.registry is where uf reads — packuments for
uf update, provenance attestations for uf install, and the versions every
report is against.
| Key | Default | Meaning |
|---|---|---|
pm.registry | publish.registry | The registry uf resolves and reports against |
pm.scopes | {} | { "@company": "https://npm.company.example" } — which registry answers for which scope |
pm.provenance | "report" | "off" reads no attestations |
pm.lockfile | "uf.lock" | The lockfile uf install writes |
pm.storeDir | ".uf/store" | Legacy project store. The native manager uses the global shared store; set the absolute UF_PM_STORE environment variable to move it |
pm.allowLifecycleScripts | false | Every dependency's install scripts, including the ones nobody has read |
pm.packageManager | "auto" | Deprecated for every value but "uf", which has no other spelling. Overrides the manager uf infers from the project; packageManager does the same ahead of it, and can pin a release. The two naming different managers is an error naming both keys |
publish.registry | "https://registry.npmjs.org" | Where uf publish pushes |
Migrating from publish.registry
Nothing to do for a project that publishes to and installs from the same
registry — pm.registry defaults to publish.registry, so the behaviour is
unchanged and nothing is printed.
A project that has moved publish.registry off npmjs is being read from
through a key that means "publish here", and uf update says so once. Name the
two separately:
export default defineConfig({
// Where uf resolves, reports and reads attestations from.
pm: { registry: "https://mirror.company.example" },
// Where `uf publish` pushes. Unrelated, and it always was.
publish: { registry: "https://npm.company.example" },
});
publish.registry keeps working as the fallback; there is no release in which
setting only it stops resolving.
Binding a scope to a registry
export default defineConfig({
pm: {
registry: "https://registry.npmjs.org",
scopes: { "@company": "https://npm.company.example" },
},
});
A package in @company is resolved from npm.company.example and nowhere
else. There is no fallback to the public registry, and that is the point:
publishing @company/internal-thing to npmjs and waiting for a resolver to
fall back to it is the dependency-confusion attack, so a fallback would be the
vulnerability rather than a recovery from it. A name the bound registry does
not have is an error naming the scope and the registry.
uf install refuses a lockfile that resolves a bound scope from anywhere else,
before the manager runs. uf does not rewrite your .npmrc: the manager that
resolves is the one that has to be told, so bind the scope there too —
@company:registry=https://npm.company.example.
Provenance
uf install reads the npm provenance attestation of every package the install
brought in or moved. An attestation that is not about the tarball being
installed stops the install; a package that publishes none is a line in the
summary, because most of npm publishes none and a tool that refused them all
would be a tool nobody runs.
uf pm approve-builds shows the same thing in an attested column, which is
the moment it matters: that is when a package is about to be allowed to run
code on the machine.
What an attestation does and does not prove is in
Security.
pm.provenance: "off" turns the reads off for a machine with no route to a
registry.
staged
The tasks uf prepare runs before a commit, keyed by a glob over the staged
files:
export default defineConfig({
tasks: {
format: "uf fmt",
stylelint: "node_modules/.bin/stylelint --fix",
},
staged: {
"*.{js,jsx}": "format",
"styles/**/*.css": ["stylelint"],
},
});
A glob with no / matches a file's name wherever the file is; a glob with a /
matches its path from the project root. The value names tasks under tasks —
one, or several in order — and each runs once, through uf run, with every
staged file its glob matches appended to its command. A glob that matches no
staged file runs nothing.
The tasks read what is staged: a half-staged file shows its staged content for
the length of the run. A rewrite of a fully staged file is staged, so the fix is
in the commit; a rewrite of a half-staged file is not kept, and fails the run.
See uf prepare.
accessibility
The accessibility audit, in two places: toHaveNoAxeViolations() in a test,
and a report on every page uf dev serves. Both need axe-core, which uf does
not install; add it to the project and both start working.
| Key | Default | Meaning |
|---|---|---|
devAudit | true | Audit each page uf dev serves. A build is never audited |
axe.tags | [] | Run only the rules carrying one of these axe tags, such as "wcag2aa"; every rule when empty |
axe.disabledRules | [] | Rule ids to switch off |
axe.minImpact | none | Drop violations below "minor", "moderate", "serious" or "critical". A violation axe gives no impact is always kept |
vrt
Visual regression: page.screenshot(name) in a test run with
--browser, which compares a capture with the
baseline of that name and fails past the threshold. page.screenshot(name, { threshold, baselines }) overrides either for one capture; see
Testing.
| Key | Default | Meaning |
|---|---|---|
baselines | "__uf_vrt__" | The directory baselines are written to and read from. It must be inside the project and must not be reached through a symbolic link |
threshold | 0 | How many pixels may differ and still pass: a whole number, 0 or more |
release
| Key | Default | Meaning |
|---|---|---|
tagPrefix | "uf@" | uf release tags a version as this prefix and the version. The last tag with the prefix is where the changelog's range starts, and a version whose tag exists is refused as already released |
docs
| Key | Default | Meaning |
|---|---|---|
outDir | "dist/docs" | Removed by uf clean |
taskRunner
| Key | Default | Meaning |
|---|---|---|
allowPackageScripts | true | Whether a task with no command is handed to Vite+'s task runner, vp run <name>, which runs the package.json script of that name. false stops the run with an error naming the task |
A task that names a command is run by uf, and this key says nothing about it.
tasks and plugins
export default defineConfig({
tasks: {
// A string is a command and nothing else.
lint: "uf lint",
// The long form takes seven more fields, all optional.
check: {
command: "uf check",
cwd: "npm/core",
dependsOn: ["lint"],
env: { NODE_OPTIONS: "--max-old-space-size=4096" },
inputs: ["npm/core/**/*.js", "uf.config.js"],
outputs: ["npm/core/dist/**"],
},
ci: { command: "echo ok", dependsOn: ["check", "test"] },
deploy: {
command: "node scripts/deploy.js",
args: [
{ name: "target", choices: ["staging", "production"] },
{ name: "region", description: "Where it runs", default: "eu" },
],
},
},
plugins: [svgr()],
});
uf run ci runs the task, and everything dependsOn reaches before it. Tasks
with no path between them run at once; see
uf run. In a workspace, member#task in
dependsOn names a task another member defines in its own uf.config.js — by
the member's name or its path — and a name this config defines as written is
always that task, # or not.
inputs and outputs are paths or globs from the project root — * stops at
a /, ** crosses one, and a pattern beginning ! excludes what it matches.
A ** does not descend into .git, .uf, .uniflowed, dist,
node_modules or target, but a pattern that names a path inside one of them
— target/release/uf — still resolves.
command is a program and its arguments, which uf starts itself — quoting,
escaping and word splitting are a POSIX shell's, and a NAME=value in front
of the program is the child's environment. A command that uses shell syntax
(|, &&, ;, a redirection, $VAR, a glob) goes to sh -c instead, and
so needs a machine that has one; uf run
says what falls on each side.
inputs is what uf keys the cache on, so it has to be everything the task
reads. A task that declares none always runs, which is the honest default:
uf knows what a command starts and not what it opens once it is running, so a
task that has not said is a task uf will not guess about. cache: false opts
a task that does declare them back out; cache: true is that default said out
loud, and is an error on a task with no inputs rather than a request uf
quietly ignores.
args declares the arguments uf run fills, in the order they are appended to
command. Each is { name, description?, choices?, default?, required? }:
name is letters, digits, - and _, and is also --name on the command
line; a value outside choices is refused; default is used when the argument
is not given; required defaults to true unless there is a default, and an
argument that may be left out with no default must come after all the others.
uf run deploy staging and uf run deploy --target staging both run
node scripts/deploy.js staging eu. A required argument that is missing is
picked from choices — or typed — at a terminal, and is an error that names it
everywhere else; see the task guide. args
is read by uf run only.
outputs is checked rather than restored: a cached result is replayed only
while the files it produced are still on disk with the contents it produced,
so deleting a dist/ rebuilds it.
Two things are deliberately not in the key. The ambient process environment
— a key over SHLVL and TERM_SESSION_ID would never be hit twice, so a task
whose answer depends on a variable names it in env, which is in the key. And
anything about the machine that is not a file: the Node on PATH, the C
compiler, the shell. A task whose answer depends on one of those and cannot
name it as a file should not declare inputs.
plugins are Vite plugins, appended to the ones uf installs; entries are
resolved by uf_plugin, which rejects any that reach outside the project root.
Removed keys
uf 0.10 removed the keys below. Each was declared by @uniflowed/config,
accepted by the loader and completed by uf lsp, and changed nothing: it was
read by nothing at all, reached a report and stopped there, or was typed with
the one value it could hold (#1387).
A uf.config.js that still writes one fails uf check, because the key is no
longer in the type; the loader ignores it.
uf codemod --to 0.10.0 takes them out, and a section left empty with them.
| Removed | |
|---|---|
app | componentDefault, orm, react.version, react.asyncReact, react.suspense, react.useHook, router.convention, rendering.cache.actions |
app.builtins | cell, data, effect, fetch, frameworkLints, graphql, loader, motion, nativeTestRunner, pwa, reactTestingLibrary, temporal, tui, web, markdown.module, markdown.engine, markdown.cache, markdown.mdx.extensions, markdown.mdx.pipelinePlugin, reactCompiler.implementation |
build, dev, docs | build.hooks, dev.allowedOrigins, docs.enabled, docs.app, docs.source, docs.staticBuild, docs.deploy |
fmt, lint | fmt.maxBlankLines, fmt.flow, lint.engine, lint.files, lint.flow |
pm, publish, release | pm.module, pm.resolver, publish.dryRun, publish.firstPublish, publish.trustedPublish, release.command, release.publish |
taskRunner, test | taskRunner.engine, test.reactTestingLibraryNative, and every field of the deprecated test.runner object but applicationTarget |
| Whole sections | package, rm, server, std, story, and vrt.enabled and vrt.module |
Two values are refused where the config is read rather than dropped, because
dropping them would change what a project gets without telling it:
rendering.cache.actions: true, which asks for a cache uf does not have, and
markdown.mdx.jsxImportSource: "@uniflowed/jsx-runtime", the key's old default,
which names a package with no JSX runtime now that the key is read. The codemod
removes the second.
Two keys changed rather than went. markdown.mdx.jsxImportSource is read, and
defaults to "react", which is what MDX was always compiled against.
taskRunner.allowPackageScripts is enforced, and defaults to true, which is
what uf run always did; a project that wrote false is told so by the
codemod, because from 0.10 that refuses a task with no command.
Official Flow
| Key | Default | Meaning |
|---|---|---|
flow.version | unset | An exact official Flow release, such as "0.333.0", for uf check and editor type services. |
export default defineConfig({
flow: { version: "0.333.0" },
});
uf acquires the official flow-bin package, checks its published integrity
and executable version, then reuses the binary. The selected release must
publish a binary for your platform. Leave the key absent to use the bundled
checker. Changing the version requires restarting the editor language server.
The generated .uf-flowconfig preserves sections from .flowconfig, enables
checking without pragmas and marks "use js"; files untyped. Add
.uf-flowconfig to .gitignore; uf new includes it. uf formatting, lint
markers and configuration completion remain available in the editor.
Edit this pagedocs/app/reference/config/$page.mdx