Upgrades an application from UI Kitten v5 to v6, and tells you precisely what it could not do.
# see what would change — nothing is written
npx @ui-kitten/codemod
# apply it
npx @ui-kitten/codemod --writeBoth leave a ui-kitten-v6-migration-report.md behind. Read that file: the part that matters is
not the diff, it is the list of things the codemod deliberately refused to touch.
The package is currently private to this repository. Until it is published, run it from a checkout:
node src/codemod/bin/ui-kitten-codemod <path-to-your-app>.
Every rule below is derived from a comparison of the published @ui-kitten/components@5.3.1 and
@6.0.0-beta.2 packages, not from a changelog. The evidence is in
VERIFIED.md.
v5 components were classes, so a component name doubled as a type: useRef<Input>(). v6 components
are function components with separate ref interfaces, so every v5 ref pattern is a hard compile
error — TS2749: 'Input' refers to a value, but is being used as a type here. This is the
transform that pays for the tool.
| rule | before | after |
|---|---|---|
ref-type-exported |
React.useRef<Input>(null) |
React.useRef<InputRef>(null) |
ref-type-generic-keep |
React.useRef<Calendar<Moment>>(null) |
React.useRef<CalendarRef<Moment>>(null) |
ref-type-generic-drop |
React.useRef<Icon<Partial<ImageProps>>>() |
React.useRef<IconRef>(null) |
ref-type-componentref |
React.useRef<ViewPager>(null) |
React.useRef<React.ComponentRef<typeof ViewPager>>(null) |
The four groups exist because the v6 answer genuinely differs:
ref-type-exported—Autocomplete,Input,Select. v6 exports a matching non-generic ref type and the v5 name carried no type arguments.ref-type-generic-keep—Calendar,RangeCalendar,Datepicker,RangeDatepicker. The v6 ref type is generic with the sameD = Datedefault, so a type argument is carried across.ref-type-generic-drop—Icon,List. The v5 classes were generic butIconRefandListRefare not, so the argument has to go. NoteIcon<T>had no default type parameter in v5, which is whyuseRef<Icon<Partial<ImageProps>>>()is not an edge case but the only valid v5 spelling — and exactly the shape a naive regex mangles.ref-type-componentref—Button,Divider,Layout,ListItem,MenuItem,Popover,SelectItem,TabBar,Text,ViewPager. These take a ref, but its type cannot be named:Buttonand the list items forwardTouchableWeb, which lives behind the@ui-kitten/components/devsupportsubpath;Textforwards react-native'sText, which collides with the UI KittenTextthe file already imports; andTabBar/ViewPagerforwardTabBarRefandViewPagerRef, which v6 does not export.React.ComponentRef<typeof X>avoids all three and adds no import. This rule is skippable with--skip=ref-type-componentref.
The group each component belongs to is not hand-maintained — see
src/componentMap.ts, regenerated by
node src/codemod/scripts/generate-component-map.cjs from the built v6 typings and the v5.3.1
git tag.
| rule | before | after |
|---|---|---|
useref-null-arg |
React.useRef<IconRef>() |
React.useRef<IconRef>(null) |
refobject-nullable |
const r: React.RefObject<InputRef> = React.createRef() |
const r: React.RefObject<InputRef | null> = React.createRef() |
react-text |
title: React.ReactText |
title: string | number |
refobject-nullable corrects something the migration notes get wrong: createRef<T>() returns
RefObject<T | null>, so annotating the result RefObject<T> fails under strictNullChecks,
which every React Native tsconfig preset enables.
| rule | before | after |
|---|---|---|
import-dss |
import { ThemeStyleType } from '@eva-design/dss' |
import type { ThemeStyleType } from '@ui-kitten/processor' |
import-eva-processor |
import { SchemaProcessor } from '@eva-design/processor' |
… from '@ui-kitten/processor' |
eva-mapping-import |
import * as eva from '@eva-design/eva' |
import * as eva from '@ui-kitten/eva' |
metro-eva-package |
evaPackage: '@eva-design/eva' |
evaPackage: '@ui-kitten/eva' |
pkg-deps |
"@ui-kitten/components": "5.3.1" |
"@ui-kitten/components": "^6.0.0-beta.2" |
jest-transform-ignore |
node_modules/(?!(react-native)/) |
node_modules/(?!(react-native|@ui-kitten)/) |
import-dss is emitted as import type because @eva-design/dss has no runtime exports at all —
its index.js body is only the __esModule marker — and @ui-kitten/processor re-exports the same
19 names from files that diff -rq reports as identical.
pkg-deps resolves target versions from the registry when it runs, so the tool does not go stale;
pass --offline to use the built-in table instead. It never removes an @eva-design/* dependency
your code still imports.
jest-transform-ignore matters because v6 publishes ES modules only: main is
./lib/module/index.js and there is no CommonJS build. A Jest config that ignores @ui-kitten
hands raw ESM to a CommonJS runtime and the suite dies at the import. See
__e2e__/jest-esm.sh, which runs Jest either side of the change.
import-hygiene runs last: it merges the duplicate import declarations the other rules create,
drops specifiers nothing references any more, and preserves the file's quote style, semicolon
convention, multi-line import layout, and the blank line after the import block.
These are real breaks. They are not rewritten, and the reason matters more than the rule.
styled-decorator — styled is the only export v6 removed. It still exists in the source, but
nothing re-exports it and the package's exports map declares no subpath that reaches it, so
import { styled } from '@ui-kitten/components' is a hard break rather than a deprecation. It is
not automated because its replacement, useStyled(name, options), is a hook: a decorated class
has to become a function component before the replacement is even applicable. That is a
restructuring, not a substitution. withStyles, StyledComponentProps and EvaProp are
unaffected and still exported.
ref-unsupported — three situations. A ref type or ref= prop on a component that is a plain
function component in v6 and accepts none (Tooltip, OverflowMenu, Modal, Card, Menu,
Drawer and 18 others); a component name used as a type somewhere that is not a ref, where v5
meant the class's instance type and v6 has no equivalent; and ref-type-componentref targets when
that rule was skipped. Deleting a ref prop is a runtime-behaviour change, and only the author
knows whether the ref was load-bearing.
lodash-merge — v5's @ui-kitten/components depended on it, so an app importing it directly
resolved without declaring it. v6 dropped the dependency. Nothing is removed: lodash.merge was
never part of the UI Kitten API, and an app's own dependency is not the codemod's to delete. All
that changed is who has to declare it.
useref-null-arg, partially — a bare React.useRef() with no type argument is the same
React 19 error, but is not mechanically fixable: useRef(null) infers RefObject<null>, which no
ref prop accepts, and the type it should have had is only knowable from what the ref is attached
to.
jest-transform-ignore, partially — a transformIgnorePatterns entry that is not the
recognised node_modules/(?!(a|b)/) shape is reported rather than edited. These entries are a
negative filter, so inserting an alternative in the wrong place silently stops transforming some
other package, and the failure surfaces much later as an unrelated syntax error.
AutocompleteItem, DrawerGroup, IconRegistry, MenuGroup, SelectGroup are still classes
in v6. useRef<MenuGroup>() remains valid. A rule that rewrote "every UI Kitten name in a type
position" would corrupt all five, so they have their own must-not-change fixtures.
ThemeType did not move. It comes from @ui-kitten/components in both v5 and v6 — neither
@eva-design/dss nor @ui-kitten/processor ever exported it.
@eva-design/eva still works at runtime. @eva-design/eva@2.2.0 and
@ui-kitten/eva@6.0.0-beta.1 ship the same mapping.json apart from the $schema pointer, which
the processor never reads, and byte-identical themes. This is pinned by a test in this repository,
src/components/theme/application/evaDesignCompat.spec.tsx. So eva-mapping-import is a
recommendation and is skippable — but metro-eva-package is not, because
@ui-kitten/metro-config@6 accepts only @ui-kitten/eva and @ui-kitten/material and silently
stops generating build-time styles for anything else.
Button, Select and Datepicker accepting string | number is not a migration item. The
brief that prompted this tool suggested a cleanup rule for v5 workarounds that wrapped strings in
<Text> to satisfy the types. There were none: v5 already typed those props as React.ReactText,
which was string | number. The only v6 change is that TextElement joined the union. The rule
does not exist, and this paragraph is here so it is not proposed again.
Your own unused imports. import-hygiene only removes names the codemod itself orphaned.
Generated from the rules themselves, so this list and the one in every report cannot drift apart.
- Check any
React.ComponentRef<typeof X>the codemod introduced — it needs @types/react 18.3 or newer; on older typings use the equivalentReact.ElementRef. (ref-type-componentref) - Give every bare
React.useRef()an explicit type argument andnullinitial value — React 19 removed the zero-argument overload, and only you know which ref type belongs there. (useref-null-arg) - Remove
reffrom components that are plain function components in v6 (Tooltip, OverflowMenu, Modal, Card, Menu, Drawer and the rest listed in the report), or drive them through their props instead. (ref-unsupported) - Convert each
@styled(...)class to a function component using theuseStyledhook.withStyles,StyledComponentPropsandEvaPropare unchanged, so a class that only needs theme values can move towithStylesinstead. (styled-decorator) - Add
lodash.mergeto your own dependencies if you import it directly — v5 pulled it in through@ui-kitten/components, and v6 does not. (lodash-merge) - If your Jest config sets
transformIgnorePatternsin a form the codemod did not recognise, add@ui-kittento its allow-list by hand — v6 is ESM-only and Jest must transform it. (jest-transform-ignore)
Beyond the rules, v6 declares react, react-native and react-native-svg as peer dependencies
for the first time and narrows react-native-svg from * to >=13.0.0. Install a matching
version yourself — Expo users should use npx expo install react-native-svg so it tracks the SDK.
ui-kitten-codemod [path] dry run over [path] (default: the current directory)
ui-kitten-codemod [path] --write apply the changes
--write apply changes; requires a clean git tree unless --force
--force skip the git checks
--rules a,b,c run only these rules
--skip a,b,c run everything except these rules
--include <glob> extra file glob (repeatable)
--exclude <glob> extra exclusion glob (repeatable)
--tsconfig <path> tsconfig to seed file discovery from
--target <tag|version> npm dist-tag to migrate towards (default: latest, falling back to beta)
--offline skip the registry lookup and use the built-in version table
--report <path> markdown report path (default: ui-kitten-v6-migration-report.md)
--report-json <path> also write the report as JSON
--no-report do not write a report file
--fail-on-manual exit 2 when anything needs a human, for CI
--list-rules print every rule and what it does, then exit
--verbose
Exit codes: 0 clean or dry run completed, 1 refused or errored, 2 findings exist and
--fail-on-manual was passed.
Dry run is the default and --write refuses to run on a dirty tree. The check happens before a
single file is read, so a refusal always leaves your tree exactly as it was. "Just trust git" is not
good enough here: the whole premise is that you are mid-upgrade, which is precisely when a working
tree is dirty. --force overrides both the clean-tree and the is-this-a-repo check.
Excluded by default: node_modules, ios, android, .expo, lib, dist, build, *.d.ts.
Running the codemod twice is a no-op; idempotency is asserted for every fixture.
ts-morph, not jscodeshift. Ten of the rules are TypeScript type-node surgery — useRef<X>()
type arguments, React.Ref<X>, forwardRef<X, P>, variable annotations — and ts-morph's
replaceWithText is a positional splice, so surrounding trivia and formatting survive. recast, which
jscodeshift uses, reprints any subtree it considers dirty; for a review-first tool that means noise
in the diff. ts-morph also parses .js/.jsx with the same AST under allowJs, so type rules
simply find nothing there while import and JSX rules fire identically — one pass, one fixture set,
instead of two parsers.
Rewriting is driven by imports, not by a type checker. A name is only eligible when the file
imports it from @ui-kitten/components, directly or through one local re-export barrel. That is
what keeps the codemod off your own Input and off Input from another UI library, and it means
the tool works on a checkout with no node_modules installed — which matters, because the usual
order is "edit the code, then install v6". Two-hop barrel chains are not followed; those files go
unrewritten and the resulting v6 type errors point straight at them.
Rule order matters in three places only: the ref rewrites run before useref-null-arg and
refobject-nullable so those see the v6 type, and import-hygiene runs last.
yarn jest src/codemod # 447 tests
yarn build && node src/codemod/scripts/record-fixtures.cjs # regenerate fixture outputs
bash src/codemod/__e2e__/run.sh # a real v5 app must typecheck against published v6
bash src/codemod/__e2e__/jest-esm.sh # Jest must fail before the ESM patch and pass afterFixtures live in __testfixtures__/, one directory per rule, and every input
is checked four ways: it produces its recorded output; feeding that output back through the rule
changes nothing; every other rule leaves the input alone; and the whole pipeline runs over
_all/ where several rules land on the same lines. _unchanged/ is the corpus nothing may touch —
a local Input, Input from react-native-paper, the five components that are still classes,
already-migrated v6 code, and '@eva-design/eva' appearing in a comment or a plain string.
The end-to-end check is not synthetic: __e2e__/v5-app/App.tsx is the v5.3.1
@ui-kitten/template-ts App.tsx extended with the ref patterns from the v5 showcases. It is
installed against the published v6 tarballs, typechecked before the codemod to record the
failures, and typechecked again after, where it must be clean.