diff --git a/packages/react-native-compatibility-check/AGENTS.md b/packages/react-native-compatibility-check/AGENTS.md new file mode 100644 index 00000000000..bd04043b825 --- /dev/null +++ b/packages/react-native-compatibility-check/AGENTS.md @@ -0,0 +1,178 @@ +# AGENTS.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Overview + +This package is a **type checker for React Native's JS/Native boundary**. It detects backwards-incompatible changes between JavaScript and Native code to prevent crashes, particularly useful for: +- Local development (detecting when native rebuild is needed) +- Over-the-air (OTA) updates +- Server Components with React Native + +The tool operates on JSON schema files generated by `@react-native/codegen`, making it agnostic to TypeScript/Flow. + +## Architecture: Three-Stage Pipeline + +The compatibility check flows through three distinct stages: + +``` +Schema (new) ──┐ + ├──▶ TypeDiffing ──▶ VersionDiffing ──▶ ErrorFormatting ──▶ Output +Schema (old) ──┘ +``` + +### Stage 1: TypeDiffing (`TypeDiffing.js`) +**Pure type comparison** - Compares two type annotations and returns all structural differences. +- Reports ALL differences between types (added/removed properties, union changes, etc.) +- Returns `ComparisonResult` with status: `matching`, `skipped`, `properties`, `members`, `unionMembers`, `functionChange`, `positionalTypeChange`, `nullableChange`, or `error` +- **Must remain pure** - no React Native-specific logic belongs here + +### Stage 2: VersionDiffing (`VersionDiffing.js`) +**Semantic safety analysis** - Interprets TypeDiffing results in the context of React Native's boundary. +- Determines if changes are safe based on **data flow direction**: + - `toNative`: Data flows from JS to Native (method parameters, component props) + - `fromNative`: Data flows from Native to JS (return values, getConstants) + - `both`: Bidirectional flow +- Encodes compatibility rules: + - Adding to a union sent TO native = **UNSAFE** (native won't expect it) + - Removing from a union received FROM native = **UNSAFE** (JS won't handle it) + - Adding optional properties = **SAFE** + - Making required properties optional when sending TO native = **UNSAFE** + +### Stage 3: ErrorFormatting (`ErrorFormatting.js`) +**Human-readable output** - Converts deep error objects into formatted strings. +- **Must remain pure** - no business logic + +### Supporting Files + +- **`ComparisonResult.js`**: Type definitions for all comparison result shapes +- **`DiffResults.js`**: Type definitions for schema diff results, error codes, and summary types +- **`SortTypeAnnotations.js`**: Sorting utilities for comparing type annotations in a stable order +- **`convertPropToBasicTypes.js`**: Converts Component prop types to standard type annotations for comparison +- **`index.js`**: Public API - exports `compareSchemas()` returning a `CompatCheckResult` + +## Key Type Definitions + +```javascript +// Main comparison statuses +type ComparisonResult = + | {status: 'matching'} // Types are identical + | {status: 'skipped'} // No old type to compare + | {status: 'properties', ...} // Object property changes + | {status: 'members', ...} // Enum member changes + | {status: 'unionMembers', ...} // Union member changes + | {status: 'functionChange', ...}// Function signature changes + | {status: 'error', ...} // Incompatible type change + +// Summary statuses +type DiffSummary = { + status: 'ok' | 'patchable' | 'incompatible', + incompatibilityReport: {...} +} +``` + +## Commands + +Run tests from the react-native-compatibility-check directory: +```bash +cd packages/react-native-compatibility-check + +# Run all tests +yarn test + +# Run a specific test file +yarn test src/__tests__/TypeDiffing-test.js + +# Run tests matching a pattern +yarn test --testNamePattern="compareTypes on unions" +``` + +**Meta employees**: Use `js1 test SUBPATH` instead (e.g., `js1 test react-native-compatibility-check`). + +## Testing Patterns + +### Test Fixtures +Tests use Flow files in `__tests__/__fixtures__/` parsed by `@react-native/codegen`: +- **Native Modules**: `native-module-*/NativeModule.js.flow` +- **Native Components**: `native-component-*/NativeComponent.js.flow` + +The `getTestSchema()` utility parses these fixtures into schema objects. + +### Test Structure +- **TypeDiffing-test.js**: Tests pure type comparison logic +- **VersionDiffing-test.js**: Tests safety analysis with boundary direction +- **ErrorFormatting-test.js**: Tests error message generation (uses snapshots) + +### Adding Test Cases +1. Create a new fixture directory under `__tests__/__fixtures__/` +2. Add a `.js.flow` file defining a Native Module or Component +3. Load it in tests using `getTestSchema(__dirname, '__fixtures__', 'fixture-name', 'FileName.js.flow')` + +## Design Principles + +### Separation of Concerns +- **TypeDiffing**: Pure type comparison. Should work for ANY JavaScript types. +- **VersionDiffing**: React Native boundary semantics. Only place for RN-specific logic. +- **ErrorFormatting**: Presentation only. No business logic. + +### Module-scope Type Registries +`TypeDiffing.js` uses module-scope variables (`_newerTypesReg`, `_olderTypesReg`, `_newerEnumMap`, `_olderEnumMap`) to avoid threading lookups through all recursive calls. This is acceptable because the logic is serial. + +### Structural Type Comparison +Types are compared structurally, not nominally. Two different type aliases with identical structure are considered matching. + +## Compatibility Rules Reference + +### Data Flowing TO Native (parameters, props) +| Change | Safe? | +|--------|-------| +| Add optional property | ✅ | +| Add required property | ❌ | +| Remove property | ✅ | +| Make property optional | ❌ | +| Add union member | ❌ | +| Remove union member | ✅ | +| Add enum member | ❌ | +| Remove enum member | ✅ | + +### Data Flowing FROM Native (return values, constants) +| Change | Safe? | +|--------|-------| +| Add optional property | ✅ | +| Add required property | ❌ | +| Remove property | ✅ | +| Make property required | ❌ | +| Add union member | ✅ | +| Remove union member | ❌ | +| Add enum member | ✅ | +| Remove enum member | ❌ | + +## Common Gotchas + +1. **Component Commands**: Adding/removing commands is intentionally allowed even though it could cause OTA issues, because there's no feature detection mechanism for commands. + +2. **Union ordering**: Unions are sorted before comparison, so `'a' | 'b'` equals `'b' | 'a'`. + +3. **Nullable vs Optional**: These are distinct concepts: + - Optional: Property may be absent (`prop?: T`) + - Nullable: Value may be null/undefined (`prop: ?T`) + +4. **Type Aliases**: Resolved during comparison. Different alias names with identical structure are treated as matching. + +5. **Component Props with Defaults**: `WithDefault` types are stripped during comparison - only the underlying type matters for compatibility. + +6. **Int32EnumTypeAnnotation**: Currently converted to `AnyTypeAnnotation` because the tool lacks support for number literal unions. + +## Adding New Type Support + +1. Add the type case to `compareTypeAnnotation()` in `TypeDiffing.js` +2. Add sorting logic in `SortTypeAnnotations.js` (`compareTypeAnnotationForSorting`) +3. Add formatting in `ErrorFormatting.js` (`formatTypeAnnotation`) +4. Add test fixtures and tests covering the new type +5. If it affects safety analysis, update `VersionDiffing.js` checks + +## Code Style + +- All source files use `@flow strict-local` or `@flow strict` +- All source files require `@format` pragma for Prettier +- Tests use `@noflow` or `@flow` (not strict) diff --git a/packages/react-native-compatibility-check/CONTRIBUTING.md b/packages/react-native-compatibility-check/CONTRIBUTING.md deleted file mode 100644 index c48a6b73f01..00000000000 --- a/packages/react-native-compatibility-check/CONTRIBUTING.md +++ /dev/null @@ -1,36 +0,0 @@ -This tool is essentially a type checker, and as such it can be difficult to -understand the data flow. Luckily, there are fairly extensive tests which can -aid in ramping up. - -This tool is made up of 3 primary stages: TypeDiffing, VersionDiffing, and -ErrorFormatting. - -At a high level, the schemas are passed to TypeDiffing which is the pure -typechecker. It returns all differences between the types. VersionDiffing then -interprets these results and decides if some of those changes are actually safe -in the context of React Native’s JS/Native boundary. - -For example, if you have a NativeModule method that returns a string union -`small | medium | large`. Any changes to that union would be flagged by -TypeDiffing as incompatible. However, adding a value to that union is safe -because it ensures your JS code handles more cases than native returns. Removing -a value from that union isn’t safe though because it means your JS no longer -handles something native might return which could cause an exception. - -VersionDiffing encodes the logic of what is safe and what isn’t; -property/union/enum additions and removals, changing something from optional to -required and vice versa, etc. VersionDiffing has knowledge of components and -modules. - -VersionDiffing returns a set of incompatible changes, which then gets passed to -ErrorFormatting. ErrorFormatting does as you’d expect, converting these deep -objects into nicely formatted strings. - -When contributing, some principles: - -- Keep TypeDiffing and ErrorFormatting pure. They should only know about - JavaScript types, not React Native specific concepts -- Add tests for every case you can think of. This codebase can be complex and - hard to reason about when making changes. The only way to stay sane is to be - able to rely on the tests to catch anything bad you’ve done. Do yourself and - future contributors a favor.