Readiness: Implemented
Migrate
Migrating to uf
Start with uf migrate --dry-run in the existing application. It shows the
configuration and scripts it can move, and names settings that need a decision.
Review that plan, then apply it with uf migrate.
uf migrate --dry-run
uf migrate
uf install
uf check
uf lint
uf test
The command reads Babel, Flow, Prettier, ESLint, Jest and Vite configuration
without executing their modules. Named package scripts become tasks; common
test, lint, format and build commands become uf commands. Other task bodies
stay verbatim. Basic Jest globals gain explicit imports from
@uniflowed/test, preserving application imports and comments. Jest mocks,
timers, custom matchers and non-default settings stay in the report for review.
.uf/migration-report.json contains the proposed edits, original contents
and every setting that could not be mapped. Only a fully replaced static
configuration file is removed. JavaScript configuration modules remain until
you review their imports and side effects. Existing uf settings win: conflicting
values are reported and retained. Install lifecycle scripts require an explicit
decision before uf install accepts them.
The three CI consumers cover Flow/Babel/Jest/ESLint, Vite React and create-react-app through install, check, lint and test. Framework-specific entry points, HTML placeholders, environment variables, deployment and Next.js routes still need the steps below.
Upgrading an existing uf project
uf codemod --to 0.0.0-alpha.41 --dry-run
uf codemod --to 0.0.0-alpha.41
The previous version comes from the installed node_modules/@uniflowed/core
package. Use --from 0.0.0-alpha.32 when that installation is unavailable.
This catalog covers alpha releases through the installed binary's version and
refuses downgrades or a newer target.
The first codemod moves env.toolchain to role-specific runtime and package
manager declarations, builder.module to build.builder,
pm.packageManager to the top-level key, and the object form of
test.runner to runner: "uf" plus its explicit target override. Multiple
runtimes, conflicting values and non-default legacy runner options require a
decision and stay unchanged. Flow-parser source ranges preserve comments and
unrelated formatting; applying the same migration twice is unchanged.
The 0.10.0 codemod removes the configuration keys nothing read — the list is
removed keys — and any section they leave
empty. jsxImportSource: "@uniflowed/jsx-runtime" goes with them, since 0.10
reads the key and refuses that value. taskRunner.allowPackageScripts: false is
kept and reported: 0.10 enforces it, so a task with no command stops the run.
What you will be able to do: decide whether your application can move today,
write the uf.config.js it needs, move a create-react-app, Vite or Next.js
application step by step — config, scripts, imports, routes, tests and deploy —
and know what still differs afterwards.
What you need first: uf installed, and the application you mean to move, with its tests passing. If it is written in TypeScript, read Flow and TypeScript first.
Where your application is covered
Read Before you start whatever you have, then the row for it:
| You have | Read |
|---|---|
create-react-app | From create-react-app |
| Vite and React | From Vite + React |
| Vite+ | uf and Vite+, then From Vite + React |
| Next.js | From Next.js |
| Expo | Compared with Expo, in the React Native guide |
| TypeScript, in any of the above | Converting the types, then your row |
Before you start
Four questions decide whether the move is a week of work or not possible yet. Answer them before you convert a file.
Is it TypeScript? uf transforms .js, .jsx, .mjs and .cjs. uf check
does not read a .ts or .tsx file, an import of one is typed any, and the
router has no $page.ts. Moving means converting the application, not adding
uf beside tsc — Converting the types is what that
takes.
What do your dependencies declare? uf check types a package that ships
Flow, and a package that ships TypeScript declarations — its own, or an @types
package's — through a Flow translation of them. What the translation cannot say
is any, and the report counts it per package; a package with neither is any
and named. What an import resolves to
is the whole table.
Where does it deploy? uf build --adapter writes eight targets: node,
bun, deno, container, edge for Cloudflare Workers, serverless for AWS
Lambda, vercel (Vercel's Build Output API), and static. None of the server
targets has been deployed to its real platform by uf's own pipeline yet
(#956), serverless buffers a
streamed response, and there is no Netlify adapter. A static host runs
no route handler, middleware or server action, and a build that has one is
refused rather than uploaded with a hole in it.
Are the packages you need on npm? @uniflowed/config, react, router,
server, test, react-testing and vite are. @uniflowed/web — Image,
Font and the Open Graph cards — is one of the packages that is not yet;
Publishing status has the list.
What every migration has in common
The shape you are moving into
uf new ../my-app-ufwrites ten files in a directory beside your application, and nothing else. Make one first: it is the target of every step below, and Your first project explains each file. What you copy into it and what you delete:
| You have | In uf |
|---|---|
babel.config.js, @babel/preset-flow, the syntax plugins | nothing — Flow reaches JavaScript inside the binary |
tsconfig.json | nothing — the type system is Flow, and uf configures it |
.prettierrc | fmt in uf.config.js: lineWidth is 100 by default, not 80 |
.eslintrc, eslint.config.js | lint.rules in uf.config.js; Coming from ESLint maps each rule |
jest.config.js, vitest.config.ts | test in uf.config.js, and nothing at all for most suites |
vite.config.ts | uf.config.js, with a vite key for everything uf has no opinion about |
scripts in package.json | tasks in uf.config.js |
index.html | the root layout, app/$layout.js: a component that renders <html> |
.nvmrc, .tool-versions | runtime in uf.config.js, below |
uf install runs no package.json script. It says so for a script such as
start or test, and refuses a manifest that declares one a package manager
would run by itself during an install — postinstall, prepare and the other
lifecycle hooks — before it fetches anything.
Scripts has the list.
Declaring the tools
uf.config.js names the runtime and package manager each command runs on,
where it is used:
uf.config.js// @flow
import { defineConfig } from "@uniflowed/config";
export default defineConfig({
runtime: "node@24",
packageManager: "pnpm@10",
build: { runtime: "node@24", builder: "vite" },
test: { runtime: "node@24", runner: "uf" },
tasks: {
dev: { command: "uf dev" },
build: { command: "uf build" },
check: { command: "uf check" },
lint: { command: "uf lint" },
fmt: { command: "uf fmt" },
test: { command: "uf test" },
},
});
| Key | What reads it |
|---|---|
runtime | uf start, uf run and uf exec, and every command whose own key is absent |
build.runtime | uf dev, uf build and uf preview, before runtime |
build.builder | the builder those commands drive: "vite", the default, or a module of your own |
test.runtime | uf test, before the runtime test.runner brings and then runtime |
test.runner | "uf", the default, or "bun@1.4", which hands the suite to bun test without module mocking or the DOM matchers — see Choosing a runner |
packageManager | uf install, uf add and the other package commands, before package.json#packageManager and the lockfiles |
"node@24" is the newest 24.x, resolved the first time a command needs it,
locked in uf.lock and installed into a store every project on the machine
shares; commit uf.lock. "node@24.14.0" is exactly that release, "node" is
whichever node is on PATH, and a range such as "node@^24" is refused. A
project that writes none of these keys runs on the node it finds, with the
manager its lockfile names — so the lockfile you already have keeps choosing
until you say otherwise. uf build --compile is the exception: it still embeds
the host app.runtime.capabilityJsHost finds
(#1036).
Environments is the whole of it.
What carries over unchanged
- React itself. React 19: the same hooks, the same rendering model, the same
reactandreact-dompackages.@uniflowed/reactre-exportsreactby name, soimport { useState } from "react"reaches the same module. uf's lint warns on a plain function component or hook and asks for acomponentorhookdeclaration, and the React Compiler is already running. - JSX in
.jsfiles. Flow's parser reads it, so nothing is renamed to.jsx. - CSS. Plain imports,
.module.css, and the preprocessors Vite supports once you install one. public/. Served from the root and copied into the build untouched..envfiles..env,.env.local,.env.<mode>and.env.<mode>.local, read in that order. What reaches the browser changes; each path below says how.- Vite plugins.
pluginsinuf.config.jsis a list of ordinary Vite plugins, appended to uf's own —vite-plugin-svgrincluded. - Your dependencies at run time. What changes is what the checker can see of them, above.
Converting the types
Rename .ts and .tsx to .js. // @flow is optional: uf check checks every
file that does not say // @noflow. uf runs Flow with its TypeScript syntax on,
so much of a TypeScript file is already Flow:
| TypeScript | Under uf check |
|---|---|
readonly x: T, ReadonlyArray<T> | unchanged |
unknown, never, undefined as types | unchanged — read as Flow's mixed, empty and void |
x as T, as const | unchanged, and the only cast syntax Flow accepts |
<T extends Bound> | unchanged; Flow's older <T: Bound> is an error |
keyof, Partial, Required, Pick, Omit, Record, NonNullable, ReturnType, Parameters | unchanged |
| conditional and mapped types | unchanged |
interface Props { … } for a component | the parameter list of a component declaration — see Flow, the modern parts |
React.FC<Props>, a function returning JSX | component |
| a custom hook | hook useThing(…), where the rules of hooks are checked |
| an object type | exact by default; write { …, ... } for one that may have more properties |
enum | a Flow enum, a value with a runtime of its own |
switch with a default: never | match, which is exhaustive by construction |
a .d.ts you wrote | a library definition under flow-typed/, which needs no configuration |
One rule has no TypeScript counterpart. A file with no import and no export
is read as a script, where await is an ordinary identifier, so a top-level
await in one is refused; export {} is enough to make it a module.
From create-react-app
The shortest distance of the three: a CRA application is a client-rendered bundle, and that is a shape uf builds.
What carries over
The components, their hooks, their CSS and public/. The shape of a Jest
suite — describe, it, expect, snapshots — and the matchers
@testing-library/jest-dom adds, which expect has built in. React Testing
Library's render, screen, within, fireEvent, userEvent and waitFor,
from @uniflowed/react-testing.
The moves
- Make the project and copy the source.
uf new ../my-app-uf, then copysrc/,public/and your.envfiles into it. - Replace
package.json's tooling. Take the scaffold'spackage.json—@uniflowed/config,@uniflowed/react,@uniflowed/routerand@uniflowed/viteat the version of the uf that wrote it,reactandreact-dom, and@uniflowed/test— and add your own dependencies to it.react-scripts,eslintConfigand thescriptsblock do not come across:start,buildandtestareuf dev,uf buildanduf test, and anything else becomes a task. Add@uniflowed/react-testingfor component tests, copy your lockfile if you want its manager to keep choosing, and runuf install. - Write
uf.config.js— the shape above. - Turn
public/index.htmlintoapp/$layout.js. The<html lang>, the<body>and anything in<head>move into the component;<title>and the description becomemetadata.<div id="root">goes, because uf renders the whole document, and so does%PUBLIC_URL%: a file inpublic/is at/. - Delete
src/index.js.app.js, from the scaffold, is the entry: it handsapp/to the router, and nothing callscreateRoot. - Turn each route into a directory under
app/.<Route path="/users/:id">isapp/users/[id]/$page.js, and the parameter arrives as a prop; the catch-allpath="*"isapp/$not-found.js.<Link to>isLinkfrom@uniflowed/routerwith the sameto,useNavigate()isuseRouter().push, anduseParams()anduseLocation()areuseRoute()'sparams,pathnameandsearchParams. There is no supported way to keep React Router:app.router.enabled: falsemakes the project a library, and nothing in uf runs<BrowserRouter>inside its own router. Even as a client component, a nested<BrowserRouter>is prerendered duringuf buildand fails ondocument is not defined, so move those routes rather than nesting routers. - Rename what reaches the browser. uf defines no
process.envfor browser code:process.env.REACT_APP_APIbecomesimport.meta.env.REACT_APP_API, withvite: { envPrefix: "REACT_APP_" }inuf.config.js— or rename the variables toVITE_API, the prefix uf uses when you set none. A direct unit test that imports a component is not running under Vite's app entry, so pass the env-derived value into a testable child component, or guard the read. - Keep SVGs as components by adding
vite-plugin-svgrtoplugins, or move icons touf:icon/…. A plainimport url from "./logo.svg"is still a URL. - Move the tests — the table below.
- Map your ESLint rules with Coming from ESLint,
and write the ones you keep under
lint.rules. - Convert the types, if the project had any. A JavaScript project is
checked too, so expect
uf checkto have something to say about code nobody annotated. - Run the loop:
uf fmt,uf lint,uf check,uf test, thenuf buildanduf preview.
A route under app/ looks like this:
// @flow
// app/users/[id]/$page.js — was <Route path="/users/:id" element={<User />} />
import * as React from "@uniflowed/react";
import { Link } from "@uniflowed/router";
export component Page(params: {| readonly id: string |}) {
return (
<main>
<h1>User {params.id}</h1>
<Link to="/users">All users</Link>
</main>
);
}
This path was checked end to end with uf 0.0.0-alpha.40 from a CRA 5.1.0 app
using React Router 6.30.2, a REACT_APP_ environment variable and React Testing
Library tests. The CRA side passed npm run build and npm test;
react-router-dom@7.18.4 did not run under CRA's Jest resolver because it
looked for react-router/dom, so the fixture stayed on v6.30.2. The uf target
passed uf install, uf fmt --check, uf check, uf test and uf build.
create-react-app printed its deprecation warning and npm audit findings, but
neither changed the uf migration steps.
Moving the tests
| Jest or Vitest | uf test |
|---|---|
globals with no import, or an import from jest, @jest/globals or vitest | import { describe, expect, it, uft } from "@uniflowed/test" — a file importing another runner is reported and fails the run |
jest.fn(), vi.fn() | uft.fn() |
jest.spyOn, vi.spyOn | uft.spyOn |
jest.useFakeTimers(), vi.useFakeTimers() | uft.useFakeTimers(), with advanceTimersByTime, runAllTimers and setSystemTime beside it |
vi.stubEnv, vi.stubGlobal | uft.stubEnv, uft.stubGlobal |
jest.mock(path), vi.mock(path) | await uft.mock(path, factory), then await import(path) — not hoisted, and on Node or Deno 2.8 or newer only |
@testing-library/react | @uniflowed/react-testing, which installs happy-dom on the first render |
toMatchSnapshot(), -u | toMatchSnapshot(), uf test -u |
import.meta.vitest | import.meta.uf.test |
| a coverage provider and its thresholds | uf test --coverage, and test.coverage in uf.config.js — Coverage |
testMatch, include | nothing: a file that declares a test is a test file |
If a route component imports Link or a router hook, render it with router
context, or move the plain UI into a child component and test that child.
Rendering the route component by itself fails because @uniflowed/router hooks
only run inside the app router.
Deploying it
A CRA build was one index.html and a bundle. app.rendering.modes: ["csr"] is
the same shape: uf build writes one shell as index.html and again as
404.html, the browser renders every route from it, and a host that serves an
error document or rewrites to index.html serves the application —
csr: a single-page application
says what that costs. Leaving the default instead prerenders every route it can,
which a static host serves as documents.
What is still different
- Mocks are not hoisted. A
jest.mockanywhere in a file becomes auft.mockabove theawait import(…)that reaches it. Module mocking runs on Node and on Deno 2.8 or newer, and coverage on Node only (#972 is a suite split across machines). - Some ESLint plugin rules have no answer yet — React
(#1062), hooks, including
exhaustive-deps(#1063), jsx-a11y (#1064) and import (#1065). - The addresses move.
uf devlistens on 5173 and the build isdist/, notbuild/.
From Vite + React
You are already on the bundler uf drives — @uniflowed/vite depends on
vite: "^8.2.2" — so the mechanics are small, and a TypeScript codebase makes
the type conversion the whole job.
What carries over
Vite itself, and every plugin. Vite's options, under the vite key.
import.meta.env, the VITE_ prefix and the .env files. import.meta.glob,
?raw, ?url, ?worker and asset imports, which uf check declares.
public/, CSS Modules and preprocessors.
The moves
- Turn
vite.config.tsintouf.config.js.server.port,server.host,server.strictPort,server.allowedHostsandserver.fsaredev.port,dev.host,dev.strictPort,dev.allowedHostsanddev.fs;build.outDirandbuild.sourcemapkeep their names underbuild;pluginsstaysplugins. Everything else —resolve.alias,build.target,define— goes underviteunchanged, where objects merge key by key and arrays replace. A dev-server proxy stays there too, asvite: { server: { proxy } }, rather than underdev.rootandconfigFilestay uf's, and so do uf's own plugins, which come first. Seevite. - Turn
index.htmlintoapp/$layout.js. Vite runs withappType: "custom"under uf: there is no HTML entry to fall back to, and the document is the layout. - Turn the entry into routes.
src/main.tsxgoes;app.jsis the entry and a page underapp/is what/renders. A client-side router's routes become directories, as in step 6 above. - Write the tool keys into
uf.config.js, as above. - Convert the types.
- Move the tests from Vitest with the same table.
uf test --browseris uf's browser mode. - Rewrite the HMR guard.
if (import.meta.hot) { import.meta.hot.accept() }is auf checkerror, because Flow does not refine a test onimport.meta. Writeimport.meta.hot?.accept();uf lint --fix-unsafemakes that change for you (Writing your ownimport.meta.hot). - Swap
vite previewforuf preview, on 4173 as before. It serves the route handlers and per-request renders a static preview cannot.
This path was checked end to end with uf 0.0.0-alpha.40 from a
create-vite@9.2.1 React app using Vite 8.3.0, React 19.2.8,
@vitejs/plugin-react, Vitest 5.0.1, server.proxy, and a VITE_
environment variable. The Vite side passed npm run build, npm test, and
vite preview; the uf target passed uf install, uf fmt --check,
uf check, uf test, uf build, and uf preview. The migrated target kept
the proxy under vite.server.proxy, declared runtime, packageManager,
build.runtime, build.builder, test.runtime and test.runner, and kept the
environment variable visible to the client. Two small moves were needed: a test
that imports a route directly should render a CSS-free child component instead,
and a Vite image import may need its src read when the server prerender sees
an asset object.
What is still different
- The Vite version is uf's. A project that needs another one names a builder
module of its own in
build.builder— a directory that re-exports uf's driver beside the Vite it wants — rather than avitedependency. - An extension added to
assetsIncludeis not read byuf check, so an import of that kind isany.
From Next.js
The conventions line up closely, and most of what an App Router application does has a counterpart. What does not is listed below, and so is what uf's deployment record does not have yet.
What carries over
| Next.js | uf |
|---|---|
app/page.tsx, app/layout.tsx | app/$page.js, app/$layout.js |
app/[slug]/, app/[...rest]/, app/[[...rest]]/, app/(group)/ | the same directories; parameters arrive as props |
loading.tsx | $loading.js — a <Suspense> boundary per segment, streamed by uf dev, uf start, uf preview and every adapter but serverless, which buffers |
error.tsx | $error.js, a "use client" module: one file for 401, 403 and 500, as cases of RouteError |
not-found.tsx, template.tsx, default.tsx | $not-found.js, $template.js, $default.js |
@slot/, (.)folder/ | @slot/, and @slot/(.)folder/ inside the slot it renders into |
route.ts | $route.js — Request in, Response out, served by uf dev, uf preview, uf start, the server adapters and --compile |
middleware.ts | $middleware.js in the directory it guards |
NextResponse.rewrite(url) | rewrite(url) from @uniflowed/router/middleware |
redirects(), rewrites(), headers() in next.config.js | app.router.redirects, rewrites and headers in uf.config.js |
basePath, trailingSlash in next.config.js | app.router.basePath and trailingSlash. Next.js's default, false, is "never", and true is "always"; uf's own default is "ignore", which answers both spellings |
Server Components, "use client" | the same: a Server Component never reaches the browser, which gets the Flight payload and the client modules it names |
"use server" modules | the same — exported async functions the browser calls by id |
generateStaticParams, metadata | the same names |
export const dynamic = "force-dynamic" | the same; "force-static" and "error" are refused |
redirect, permanentRedirect, notFound, unauthorized, forbidden | the same names, from @uniflowed/router |
useRouter() | useRouter() from @uniflowed/router: push, replace, prefetch, refresh, back, forward |
usePathname(), useSearchParams(), useParams() | useRoute()'s pathname, searchParams and params |
next/link | Link from @uniflowed/router, with to where Next.js has href |
cookies(), headers(), draftMode(), after() | the same names, from @uniflowed/server |
revalidatePath, revalidateTag | the same names, from @uniflowed/server/cache |
next start | uf start |
output: 'standalone' | uf build --adapter node, container, or --compile |
output: 'export' | build: { staticBuild: true }, or uf build --adapter static |
The moves
- Make the project and move
app/.uf new ../my-app-uf, then renamepage.tsxto$page.js,layout.tsxto$layout.jsand the rest by the table, converting the types as you go. Export route components by name:export component Pageandexport component Layout, matching whatuf newwrites. Put"use client"at the top of each$error.js. - Replace
next.config.js.outputis the last two rows above, andimagesisapp.builtins.images.redirects(),rewrites()andheaders()becomeapp.router.redirects,rewritesandheaders: the same sources written as data, with a rule'sheadersan object rather than a list of{ key, value }. A rewrite to another origin is a route handler that fetches it, and ahasormissingcondition is a middleware that returnsrewrite()orResponse.redirect(Redirects, rewrites and response headers has the whole table).basePathisapp.router.basePath, and a project that relied on Next.js's default redirect from/about/to/aboutsetsapp.router.trailingSlash: "never"(A base path and a trailing slash). - Change the imports by the table:
next/navigationto@uniflowed/router,next/headersandnext/serverto@uniflowed/server,next/cacheto@uniflowed/server/cache,next/dynamicto React'slazy. In a$route.js, return JSON as aResponsewith a JSON body andcontent-type; Flow's DOM library does not exposeResponse.json()yet. - Keep data fetching in Server Components. A page can be an
export async function Page(props: Props)that callsawait fetch(...)and returns JSX. Nested async functions stream behind<Suspense>; noloaderis required. A routeloaderremains an option when several route consumers need the same prepared data (Loading data). Keep async data fetching on the server: React does not support async Client Components.generateMetadatais read from a page, and a route that uses it waits for its data rather than streaming. - Split
middleware.tsby directory. There is nomatcher: a$middleware.jsguards the directory it sits in, and middleware composes down the tree the way layouts do. Exportfunction middleware(request: Request), return aResponseto answer, or return nothing to continue —NextResponse.next()is returning nothing (Middleware). - Restate caching. Turn on
app.rendering.cache.route, then callcacheLife({ revalidate: 60 })andcacheTag("posts")inside the render, from@uniflowed/server/cache;export const revalidateis not read. A prerendered page that states a lifetime is served from the build and regenerated in the background once it passes, which is the behaviour ISR names, andrendering.cache.storedecides where the regenerated pages are kept (Caching). - Rename what reaches the browser.
NEXT_PUBLIC_APIbecomesVITE_API, or keep the name withvite: { envPrefix: "NEXT_PUBLIC_" }. Server code reads every variable throughprocess.env, as before. - Swap the components.
next/imageand localnext/fontareImageandFontfrom@uniflowed/web, given an imported file, and anopengraph-imageis a*.og.jsontemplate (Images, fonts, icons and cards).sitemap.xmlandrobots.txtare written byuf buildoncesite.urlis set. - Move the tests with the same table.
- Choose a target.
uf buildlists every route that needs a server underanswered by a server.uf build --adapter containerwrites aDockerfile,edgea Cloudflare Worker andserverlessa Lambda function; Serving a build is what each needs.
This path was checked end to end with uf 0.0.0-alpha.40 on a migrated App
Router-shaped fixture: a page with a loader, a $route.js handler and a
directory $middleware.js. uf install, uf fmt --check, uf check,
uf test and uf build all completed. The build named /api/status and
/admin/* under answered by a server, and reported the /admin guard.
What is still different
- The deployment record. No server adapter's output has been deployed to its
platform by uf's own pipeline
(#956),
serverlessbuffers a streamed document, and there is no adapter for Vercel or Netlify. - Server functions. 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 file upload does not work, and a form bound to an action passed down as a prop posts only after hydration (one bound to an imported action posts before it); an action's arguments are JSON data plus at most one form (What is not here, uf and Next.js). - A slot is not a request boundary. It holds no
$not-found.js,$route.jsor$middleware.js, and an interception is always shown in a slot rather than replacingchildren. - The rest of ISR and PPR. A
--compilebinary carries no cache and renders a partially prerendered page whole, per request;serverlessrefuses a partially prerendered page; and there is noexperimental_pprexport orcacheComponentsswitch — a page is a shell with holes when it reads the request inside a<Suspense>boundary andppris allowed (Rendering). Function caching uses explicitcacheFunction, with a name, arguments and lifetime; the"use cache"directive is not supported. - Middleware and request APIs. A rewrite changes the path and never the
request's headers, neither a middleware's nor a config rule's proxies to
another origin, and
cookies()reads without writing. - Images and fonts. An imported image is resized at build time, with no
AVIF or lossy WebP there (#997).
images.remotePatternscarries over asapp.builtins.images.remotePatternswith the same keys, andnext/image's remotesrcbecomesImagethrough/__uf/image; a deployednode,containerorserverlessdirectory needsapp.builtins.images.transformerto encode there. A font is self-hosted from a file you import rather than downloaded from a provider. - Files with no counterpart:
global-error.tsx,instrumentation.ts, and theiconandmanifestconventions.
Estimating your own migration
The measured paths above are deliberately small, so use this arithmetic for the application in front of you. The job is four numbers you can count today:
- Modules to convert. Mechanical, and mostly the table above.
- Imports
uf checkstill typesany. Convert one leaf directory, runuf installanduf check, and read the list the report ends with — and, for a package typed from its TypeScript declarations, how much of it the translation could not say. - Tests that lean on a hoisted mock or on another runner's globals. Each one moves its mock above the import it affects.
- Routes that need a server, against where you deploy.
uf buildnames them.
The second number is the one that decides whether the type system pays you back, and it is also the real conversion rate for your codebase: it beats any figure this page could assert.
Where to go next
Start is next: install uf and make a project, which is the shape you are converting into — Your first project explains each file of it. What uf does not do is the list of walls, kept current with issue numbers. If the answer turns out to be "not yet", that is a supported answer — it is why the list exists.
Edit this pagedocs/app/guide/migrate/$page.mdx