diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index a8f161a..b082a3b 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -7,6 +7,11 @@ "url": "https://github.com/react-native-community" }, "skills": [ + { + "name": "migrate-to-strict-api", + "source": "./migrate-to-strict-api", + "description": "Use when migrating a React Native project to the Strict TypeScript API — the default JavaScript API from React Native 0.87, available as an opt-in preview since 0.80. Handles tsconfig.json setup, dependency updates, rewriting deep imports to root imports, and resolving breaking type changes. Invoke with `/migrate-to-strict-api`." + }, { "name": "upgrade-react-native", "source": "./upgrade-react-native", diff --git a/AGENTS.md b/AGENTS.md index 1526645..6a58e6d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,9 +6,12 @@ A skill is a set of local instructions stored in a `SKILL.md` file. ### Available skills +- migrate-to-strict-api: Migrate a React Native project (>= 0.80) to the strict TypeScript API. Handles the tsconfig.json opt-in, replaces deep imports, and fixes breaking type changes. (file: `migrate-to-strict-api/SKILL.md`) - upgrade-react-native: Upgrade React Native versions in Community CLI projects using the Upgrade Helper diff. (file: `upgrade-react-native/SKILL.md`) ### How to use skills +- Trigger rule: Use `migrate-to-strict-api` when the user asks to migrate to the strict TypeScript API, enable the strict API, or fix deep imports for the strict API. +- Invocation: `/migrate-to-strict-api` - Trigger rule: Use `upgrade-react-native` when the user asks to upgrade React Native to a newer version. - Invocation: `/upgrade-react-native ` diff --git a/README.md b/README.md index eeb87f5..9235ae7 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ Agent Skills for React Native Community CLI projects. | Skill | Description | |-------|-------------| +| [migrate-to-strict-api](./migrate-to-strict-api/) | Migrate to the Strict TypeScript API (React Native 0.80+) | | [upgrade-react-native](./upgrade-react-native/) | Upgrade React Native versions using the upgrade helper diff | ## Installation diff --git a/migrate-to-strict-api/SKILL.md b/migrate-to-strict-api/SKILL.md new file mode 100644 index 0000000..1249c0f --- /dev/null +++ b/migrate-to-strict-api/SKILL.md @@ -0,0 +1,262 @@ +--- +name: migrate-to-strict-api +description: > + Use when migrating a React Native project to the Strict TypeScript API — + the default JavaScript API from React Native 0.87, available as an opt-in + preview since 0.80. Handles tsconfig.json setup, dependency updates, + rewriting deep imports to root imports, and resolving breaking type + changes. Invoke with `/migrate-to-strict-api`. +license: MIT +metadata: + author: react-native-community + version: 0.1.0 +--- + +# Migrate to Strict TypeScript API + +Migrate a React Native project to the [Strict TypeScript API](https://reactnative.dev/docs/strict-typescript-api) — auto-generated TypeScript types, scoped to `react-native`'s root exports. The Strict API is the **default from React Native 0.87**, and an opt-in preview on 0.80–0.86. + +This migration affects TypeScript analysis only. Nothing changes in the bundle or at runtime, so it is safe to apply incrementally and there is no shipping risk. + +## Invocation + +``` +/migrate-to-strict-api +``` + +## Step-by-step procedure + +Follow every step below **in order**. Do not skip steps. + +### 1. Verify prerequisites + +- Read `package.json` and confirm the `react-native` version is **>= 0.80**. If not, stop and tell the user to upgrade first. Record whether the version is **>= 0.87** — this decides steps 3 and 5. +- Confirm `tsconfig.json` exists. +- Confirm `node_modules/react-native/types_generated/index.d.ts` exists. If not, tell the user to run their package manager install first. + +### 2. Update dependencies + +Some libraries ship raw TypeScript source that is type-checked as part of the user's project — most commonly Jest mock entry points imported from a setup file. Under the Strict API these produce errors located under `node_modules`, typically `TS2307: Cannot find module 'react-native/Libraries/...'`, which the user cannot fix in their own code. + +- Confirm `skipLibCheck` is enabled (`@react-native/typescript-config` sets it). Do not disable it. +- Whenever type errors are located under `node_modules/` — at any point in this migration — update that package first, checking its releases for a Strict API compatibility fix. Known fixed versions are listed in [references/library-compatibility.md](references/library-compatibility.md). +- If no fixed release exists, apply a local fix by redirecting the imported subpath to an untyped stub, and tell the user to report the incompatibility to the library: + +```json +{ + "compilerOptions": { + "paths": { + "some-library/jest/mock": ["./untyped-module.d.ts"] + } + } +} +``` + +```ts +// untyped-module.d.ts +declare const anyExport: unknown; +export default anyExport; +``` + +An inline `@ts-ignore` does not work for this case — the errors are reported inside the library's files, not at the import site. + +### 3. Enable the Strict API in `tsconfig.json` + +**React Native >= 0.87** — the Strict API is the default: + +- If `compilerOptions.customConditions` contains `"react-native-legacy-deep-imports"`, remove that entry (it is the temporary opt-out, due for removal in a future release), keeping `"react-native"` in place. +- Otherwise, no config change is needed. + +**React Native 0.80–0.86** — opt in: + +- Add `"customConditions": ["react-native", "react-native-strict-api"]` to `compilerOptions`. **Both entries are required**: `customConditions` replaces (not merges with) the value from an extended config, so omitting `"react-native"` drops the standard condition set by `@react-native/typescript-config` and breaks module resolution for packages that key on it. The project must use `"moduleResolution": "bundler"` (or `"node16"` / `"nodenext"`) for custom conditions to take effect. + +### 4. Fix deep imports + +#### 4a. Run the ESLint autofix first + +`@react-native/eslint-plugin` ships a `no-deep-imports` rule with a code autofixer that rewrites known deep imports (default and type imports) to root imports. If the plugin is installed — it is bundled with `@react-native/eslint-config` — enable the rule in the project's ESLint config: + +```json +{"@react-native/no-deep-imports": "error"} +``` + +Then run ESLint with `--fix` across the project source. Recommend keeping the rule enabled afterwards to prevent regressions. + +#### 4b. Manual pass for the remainder + +Read `node_modules/react-native/types_generated/index.d.ts`. This file is the source of truth for every export available under the Strict API. Each line maps an internal source path to an exported name: + +```ts +export { default as Alert } from "./Libraries/Alert/Alert"; +export type { AlertButton } from "./Libraries/Alert/Alert"; +``` + +Parse this file to build two mappings: + +- **Value exports**: internal path + original name → exported name (e.g. `./Libraries/Alert/Alert` default → `Alert`) +- **Type exports**: internal path → list of exported type names (e.g. `./Libraries/Alert/Alert` → `AlertType`, `AlertButton`, ...) + +Search all source files (`.ts`, `.tsx`, `.js`, `.jsx`) for remaining imports from `react-native/Libraries/...` or `react-native/src/...` subpaths, and rewrite each using the mapping: + +| Deep import pattern | Replacement | +| --- | --- | +| `import Foo from 'react-native/Libraries/.../Foo'` | `import {Foo} from 'react-native'` | +| `import type {FooProps} from 'react-native/Libraries/.../Foo'` | `import type {FooProps} from 'react-native'` | +| `import {named} from 'react-native/Libraries/.../Foo'` | `import {named} from 'react-native'` | + +When a file already has a `react-native` root import, merge into it rather than adding a duplicate. + +**Codegen imports** move to the `CodegenTypes` namespace: + +```ts +// Before +import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent'; +import codegenNativeCommands from 'react-native/Libraries/Utilities/codegenNativeCommands'; +import type {Int32, WithDefault} from 'react-native/Libraries/Types/CodegenTypes'; + +// After +import {codegenNativeComponent, codegenNativeCommands} from 'react-native'; +import type {CodegenTypes} from 'react-native'; + +// Usage changes: Int32 → CodegenTypes.Int32, WithDefault<...> → CodegenTypes.WithDefault<...> +``` + +**`InitializeCore`** is a side-effect entry point with no root export — replace it with the dedicated subpath (usually found in Jest setup files and custom entry points): + +```diff +- import 'react-native/Libraries/Core/InitializeCore'; ++ import 'react-native/setup-env'; +``` + +If a deep import path does not appear in the mapping, it is intentionally private. Look for a root-API alternative; if none covers the use case, flag it to the user and point them to the [feedback thread](https://github.com/react-native-community/discussions-and-proposals/discussions/1015). Note that `react-native/src/private/*` imports are also removed at runtime in 0.87 — those usages must be removed, not just retyped. + +#### 4c. Leave Jest string mocks alone + +Do not rewrite `jest.mock('react-native/Libraries/...')` or `jest.requireActual(...)` path strings. Module resolution in Jest and Metro is unchanged — these keep working, and TypeScript does not check them. Only when a test file imports a deep path as a module does TypeScript error; where no root export covers the use case, keep the import and suppress the error explicitly: + +```ts +// @ts-expect-error - React Native internal, untyped under the Strict API +import NativeAppState from 'react-native/Libraries/AppState/NativeAppState'; +``` + +### 5. Run typecheck and fix remaining type errors + +Run `npx tsc --noEmit`. Fix errors iteratively using these known patterns: + +#### Component ref and instance types + +Core components are now typed as function components, so a component name no longer works as an instance type (`useRef` refers to the component function, and methods like `.focus()` appear missing). This is the most common blocker. + +- **On 0.87+**, use the dedicated `*Instance` types — every ref-supporting core component exports one (`ViewInstance`, `TextInputInstance`, `ScrollViewInstance`, `FlatListInstance`, ...). Components without ref support have no instance type: `InputAccessoryView`, `TouchableWithoutFeedback`, `experimental_LayoutConformance`. +- **On 0.80–0.86**, use `React.ComponentRef` — it resolves the same instance type and works under both the Strict API and the legacy types. + +```ts +// Before +const inputRef = useRef(null); + +// After (0.87+) — preferred +const inputRef = useRef(null); + +// After (any version) — equivalent +const inputRef = useRef>(null); +``` + +Apply this everywhere a component name is used as an instance type: `useRef` / `createRef` type arguments, `ref` props on wrapper components (`ref?: React.Ref`), `React.forwardRef` type arguments, and variables holding instances. + +Related replacements: + +- `Animated.LegacyRef` is removed — use the plain `*Instance` type; `*Instance` types work transparently with `Animated.*` component variants. +- `NativeMethods` / `NativeMethodsMixin` are removed — use `HostInstance`, or the specific `*Instance` type. + +#### `*Static` types removed + +The API name itself is now exported as both the value and its type: + +```ts +// Before +import {Linking, LinkingStatic} from 'react-native'; +function foo(linking: LinkingStatic) {} + +// After +import {Linking} from 'react-native'; +function foo(linking: Linking) {} +``` + +Where no same-name type existed previously (`InteractionManagerStatic`, `PixelRatioStatic`, `DevMenuStatic`, `KeyboardStatic`, `DevSettingsStatic`, `NativeModulesStatic`), use `typeof` the value instead (e.g. `typeof PixelRatio`). + +Full list: `AlertStatic`, `ActionSheetIOSStatic`, `ToastAndroidStatic`, `InteractionManagerStatic`, `UIManagerStatic`, `PlatformStatic`, `SectionListStatic`, `PixelRatioStatic`, `AppStateStatic`, `AccessibilityInfoStatic`, `ImageResizeModeStatic`, `BackHandlerStatic`, `DevMenuStatic`, `ClipboardStatic`, `PermissionsAndroidStatic`, `ShareStatic`, `DeviceEventEmitterStatic`, `LayoutAnimationStatic`, `KeyboardStatic`, `DevSettingsStatic`, `I18nManagerStatic`, `EasingStatic`, `PanResponderStatic`, `NativeModulesStatic`, `LogBoxStatic`, `PushNotificationIOSStatic`, `SettingsStatic`, `VibrationStatic` + +#### Animated type changes + +Animated nodes are no longer generic — they are non-generic types with a generic `interpolate` method. Remove type parameters from `Animated.Value`, `Animated.ValueXY`, etc. + +#### Deprecated `*Properties` aliases + +The legacy `*Properties` aliases are removed. Rename to the matching `*Props` type: `ViewProperties` → `ViewProps`, `TextProperties` → `TextProps`, `ImageProperties` → `ImageProps`, etc. — plus `ImagePropertiesSourceOptions`, which becomes `ImageSourcePropType`. + +#### `StyleSheet.absoluteFillObject` + +Not part of the Strict API. Replace with `StyleSheet.absoluteFill`, which is equivalent and works under both API modes: + +```tsx +// Before +style={{...StyleSheet.absoluteFillObject, borderRadius: 12}} + +// After +style={{...StyleSheet.absoluteFill, borderRadius: 12}} +``` + +#### `useColorScheme()` and `'unspecified'` + +The return type no longer includes `'unspecified'` — this value was inaccurately typed in the old manual types and never occurred at runtime. Check for `'dark'` and default to `'light'`: + +```ts +// Before — handles a case that cannot occur +const theme = scheme === 'unspecified' ? 'light' : scheme; + +// After +const theme = scheme === 'dark' ? 'dark' : 'light'; +``` + +#### Optional props are `type | undefined` + +Every optional prop is now typed as `type | undefined`. Wrapper types that re-declare React Native props may need widening to match. + +#### Internal helper types + +`RecursiveArray`, `RegisteredStyle`, `Falsy`, `WithAnimatedArray`, `WithAnimatedObject`, and other internal-only helpers are no longer accessible from `react-native`. Inline the type definition or find an alternative. + +#### Leftover component props removed + +Props that existed only in the old type definitions were removed — for example `lineBreakMode` on `Text`, `scrollWithoutAnimationTo` on `ScrollView`, and transform styles declared outside the `transform` array. Remove or replace these usages. + +### 6. Verify + +- Run `npx tsc --noEmit` again. Confirm zero type errors remain. +- If the project has a Jest suite, run it. Mocks are unaffected by this migration; investigate any new failure before proceeding. + +Present a summary of all changes: + +- Files modified (count) +- Deep imports rewritten (count) +- Type errors fixed (by category) +- Anything flagged for the user (private APIs with no root equivalent, library incompatibilities) + +## Bailing out + +If the migration cannot be completed — an unresolvable library incompatibility, a larger error count than the user wants to absorb now, or the user asks to defer — the project can temporarily revert to the legacy types: + +- **React Native >= 0.87**: set `"customConditions": ["react-native", "react-native-legacy-deep-imports"]` in `compilerOptions` (both entries required, as in step 3). This opt-out is temporary and due for removal in a future release. +- **React Native 0.80–0.86**: remove `"react-native-strict-api"` from `customConditions`, restoring its previous value. + +Do not revert completed work that is valid under both API modes — it remains forward progress for a later attempt: + +- Deep imports rewritten to root imports +- `CodegenTypes` namespace usage (also exported when the Strict API is not enabled) +- `react-native/setup-env` +- Ref types written as `React.ComponentRef` + +The exception is `*Instance` ref types, which only exist under the Strict API — when bailing out after step 5, convert these to the equivalent `React.ComponentRef` form. If a bail-out seems likely from the start, prefer that form throughout. + +When bailing out on 0.87+, encourage the user to share what blocked them in the [opt-out discussion thread](https://github.com/react-native-community/discussions-and-proposals/discussions/1015). diff --git a/migrate-to-strict-api/references/library-compatibility.md b/migrate-to-strict-api/references/library-compatibility.md new file mode 100644 index 0000000..51a648d --- /dev/null +++ b/migrate-to-strict-api/references/library-compatibility.md @@ -0,0 +1,10 @@ +# Library compatibility for the Strict TypeScript API + +Known libraries that have shipped Strict TypeScript API compatibility fixes, and the minimum version to install. Update dependencies to at least these versions before migrating a project. + +Contributions welcome — add a row (keep alphabetical order) with a link to the fixing PR or release. + +| Package | Minimum version | Fix | +| --- | --- | --- | +| `@expensify/react-native-live-markdown` | `0.1.335` | [Expensify/react-native-live-markdown#771](https://github.com/Expensify/react-native-live-markdown/pull/771) | +| `react-native-safe-area-context` | `5.8.1` | [AppAndFlow/react-native-safe-area-context#745](https://github.com/AppAndFlow/react-native-safe-area-context/pull/745) | diff --git a/upgrade-react-native/SKILL.md b/upgrade-react-native/SKILL.md index ca8607d..1363a57 100644 --- a/upgrade-react-native/SKILL.md +++ b/upgrade-react-native/SKILL.md @@ -162,7 +162,18 @@ Present all proposed dependency bumps alongside the diff-based changes in step 5 Apply these version bumps to `package.json` as part of step 6. -### 8. Post-upgrade checklist +### 8. Migrate to the Strict TypeScript API (target >= 0.87) + +React Native 0.87 makes the [Strict TypeScript API](https://reactnative.dev/docs/strict-typescript-api) the default. When the upgrade crosses this boundary (current version < 0.87, target >= 0.87) and the project uses TypeScript (a `tsconfig.json` exists), type-checking of the project is affected and this step is **required** — do not silently skip it. + +Ask the user which they prefer: + +1. **Migrate now (recommended)** — run the [`migrate-to-strict-api`](https://skills.sh/react-native-community/skills/migrate-to-strict-api) skill (`/migrate-to-strict-api`), which handles dependency compatibility, deep import rewriting, and known breaking type changes. +2. **Defer with the temporary opt-out** — add `"customConditions": ["react-native", "react-native-legacy-deep-imports"]` to `compilerOptions` in `tsconfig.json`, keeping both entries. Tell the user this opt-out is temporary and due for removal in a future release. + +For target versions below 0.87, or projects without TypeScript, skip this step and do not suggest the migration unprompted. + +### 9. Post-upgrade checklist After applying all changes, present the user with a checklist: @@ -173,6 +184,7 @@ After applying all changes, present the user with a checklist: - [ ] Run a clean build for iOS: `cd ios && xcodebuild clean` - [ ] Run the app on both platforms to verify it launches - [ ] Run the project's test suite +- [ ] (TypeScript, target >= 0.87) Run `npx tsc --noEmit` to confirm the Strict TypeScript API migration or opt-out from step 8 - [ ] Review any conflict resolutions for correctness - [ ] Check the [React Native changelog](https://github.com/facebook/react-native/blob/main/CHANGELOG.md) for additional breaking changes - [ ] Check the [Upgrade Helper web UI](https://react-native-community.github.io/upgrade-helper/?from=&to=) for any supplementary notes @@ -183,3 +195,5 @@ Consult these for version-specific migration guidance: - [references/upgrade-helper-api.md](./references/upgrade-helper-api.md) — How to fetch diffs and version lists programmatically +- [migrate-to-strict-api](https://skills.sh/react-native-community/skills/migrate-to-strict-api) — + Companion skill for the Strict TypeScript API migration (default from 0.87)