Skip to content

Latest commit

 

History

History

codemod

README.md

@ui-kitten/codemod

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 --write

Both 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>.


What it rewrites

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.

Ref types

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 same D = Date default, so a type argument is carried across.
  • ref-type-generic-drop — Icon, List. The v5 classes were generic but IconRef and ListRef are not, so the argument has to go. Note Icon<T> had no default type parameter in v5, which is why useRef<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: Button and the list items forward TouchableWeb, which lives behind the @ui-kitten/components/devsupport subpath; Text forwards react-native's Text, which collides with the UI Kitten Text the file already imports; and TabBar/ViewPager forward TabBarRef and ViewPagerRef, 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.

React 19

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.

Packages

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.

Tidying

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.


What it only reports

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.


What it deliberately leaves alone

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.


What you still have to do by hand

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 equivalent React.ElementRef. (ref-type-componentref)
  • Give every bare React.useRef() an explicit type argument and null initial value — React 19 removed the zero-argument overload, and only you know which ref type belongs there. (useref-null-arg)
  • Remove ref from 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 the useStyled hook. withStyles, StyledComponentProps and EvaProp are unchanged, so a class that only needs theme values can move to withStyles instead. (styled-decorator)
  • Add lodash.merge to 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 transformIgnorePatterns in a form the codemod did not recognise, add @ui-kitten to 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.


Options

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.


How it works

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.


Tests

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 after

Fixtures 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.