|
| 1 | +# AGENTS.md |
| 2 | + |
| 3 | +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. |
| 4 | + |
| 5 | +## Overview |
| 6 | + |
| 7 | +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: |
| 8 | +- Local development (detecting when native rebuild is needed) |
| 9 | +- Over-the-air (OTA) updates |
| 10 | +- Server Components with React Native |
| 11 | + |
| 12 | +The tool operates on JSON schema files generated by `@react-native/codegen`, making it agnostic to TypeScript/Flow. |
| 13 | + |
| 14 | +## Architecture: Three-Stage Pipeline |
| 15 | + |
| 16 | +The compatibility check flows through three distinct stages: |
| 17 | + |
| 18 | +``` |
| 19 | +Schema (new) ──┐ |
| 20 | + ├──▶ TypeDiffing ──▶ VersionDiffing ──▶ ErrorFormatting ──▶ Output |
| 21 | +Schema (old) ──┘ |
| 22 | +``` |
| 23 | + |
| 24 | +### Stage 1: TypeDiffing (`TypeDiffing.js`) |
| 25 | +**Pure type comparison** - Compares two type annotations and returns all structural differences. |
| 26 | +- Reports ALL differences between types (added/removed properties, union changes, etc.) |
| 27 | +- Returns `ComparisonResult` with status: `matching`, `skipped`, `properties`, `members`, `unionMembers`, `functionChange`, `positionalTypeChange`, `nullableChange`, or `error` |
| 28 | +- **Must remain pure** - no React Native-specific logic belongs here |
| 29 | + |
| 30 | +### Stage 2: VersionDiffing (`VersionDiffing.js`) |
| 31 | +**Semantic safety analysis** - Interprets TypeDiffing results in the context of React Native's boundary. |
| 32 | +- Determines if changes are safe based on **data flow direction**: |
| 33 | + - `toNative`: Data flows from JS to Native (method parameters, component props) |
| 34 | + - `fromNative`: Data flows from Native to JS (return values, getConstants) |
| 35 | + - `both`: Bidirectional flow |
| 36 | +- Encodes compatibility rules: |
| 37 | + - Adding to a union sent TO native = **UNSAFE** (native won't expect it) |
| 38 | + - Removing from a union received FROM native = **UNSAFE** (JS won't handle it) |
| 39 | + - Adding optional properties = **SAFE** |
| 40 | + - Making required properties optional when sending TO native = **UNSAFE** |
| 41 | + |
| 42 | +### Stage 3: ErrorFormatting (`ErrorFormatting.js`) |
| 43 | +**Human-readable output** - Converts deep error objects into formatted strings. |
| 44 | +- **Must remain pure** - no business logic |
| 45 | + |
| 46 | +### Supporting Files |
| 47 | + |
| 48 | +- **`ComparisonResult.js`**: Type definitions for all comparison result shapes |
| 49 | +- **`DiffResults.js`**: Type definitions for schema diff results, error codes, and summary types |
| 50 | +- **`SortTypeAnnotations.js`**: Sorting utilities for comparing type annotations in a stable order |
| 51 | +- **`convertPropToBasicTypes.js`**: Converts Component prop types to standard type annotations for comparison |
| 52 | +- **`index.js`**: Public API - exports `compareSchemas()` returning a `CompatCheckResult` |
| 53 | + |
| 54 | +## Key Type Definitions |
| 55 | + |
| 56 | +```javascript |
| 57 | +// Main comparison statuses |
| 58 | +type ComparisonResult = |
| 59 | + | {status: 'matching'} // Types are identical |
| 60 | + | {status: 'skipped'} // No old type to compare |
| 61 | + | {status: 'properties', ...} // Object property changes |
| 62 | + | {status: 'members', ...} // Enum member changes |
| 63 | + | {status: 'unionMembers', ...} // Union member changes |
| 64 | + | {status: 'functionChange', ...}// Function signature changes |
| 65 | + | {status: 'error', ...} // Incompatible type change |
| 66 | + |
| 67 | +// Summary statuses |
| 68 | +type DiffSummary = { |
| 69 | + status: 'ok' | 'patchable' | 'incompatible', |
| 70 | + incompatibilityReport: {...} |
| 71 | +} |
| 72 | +``` |
| 73 | + |
| 74 | +## Commands |
| 75 | + |
| 76 | +Run tests from the react-native-compatibility-check directory: |
| 77 | +```bash |
| 78 | +cd packages/react-native-compatibility-check |
| 79 | + |
| 80 | +# Run all tests |
| 81 | +yarn test |
| 82 | + |
| 83 | +# Run a specific test file |
| 84 | +yarn test src/__tests__/TypeDiffing-test.js |
| 85 | + |
| 86 | +# Run tests matching a pattern |
| 87 | +yarn test --testNamePattern="compareTypes on unions" |
| 88 | +``` |
| 89 | + |
| 90 | +**Meta employees**: Use `js1 test SUBPATH` instead (e.g., `js1 test react-native-compatibility-check`). |
| 91 | + |
| 92 | +## Testing Patterns |
| 93 | + |
| 94 | +### Test Fixtures |
| 95 | +Tests use Flow files in `__tests__/__fixtures__/` parsed by `@react-native/codegen`: |
| 96 | +- **Native Modules**: `native-module-*/NativeModule.js.flow` |
| 97 | +- **Native Components**: `native-component-*/NativeComponent.js.flow` |
| 98 | + |
| 99 | +The `getTestSchema()` utility parses these fixtures into schema objects. |
| 100 | + |
| 101 | +### Test Structure |
| 102 | +- **TypeDiffing-test.js**: Tests pure type comparison logic |
| 103 | +- **VersionDiffing-test.js**: Tests safety analysis with boundary direction |
| 104 | +- **ErrorFormatting-test.js**: Tests error message generation (uses snapshots) |
| 105 | + |
| 106 | +### Adding Test Cases |
| 107 | +1. Create a new fixture directory under `__tests__/__fixtures__/` |
| 108 | +2. Add a `.js.flow` file defining a Native Module or Component |
| 109 | +3. Load it in tests using `getTestSchema(__dirname, '__fixtures__', 'fixture-name', 'FileName.js.flow')` |
| 110 | + |
| 111 | +## Design Principles |
| 112 | + |
| 113 | +### Separation of Concerns |
| 114 | +- **TypeDiffing**: Pure type comparison. Should work for ANY JavaScript types. |
| 115 | +- **VersionDiffing**: React Native boundary semantics. Only place for RN-specific logic. |
| 116 | +- **ErrorFormatting**: Presentation only. No business logic. |
| 117 | + |
| 118 | +### Module-scope Type Registries |
| 119 | +`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. |
| 120 | + |
| 121 | +### Structural Type Comparison |
| 122 | +Types are compared structurally, not nominally. Two different type aliases with identical structure are considered matching. |
| 123 | + |
| 124 | +## Compatibility Rules Reference |
| 125 | + |
| 126 | +### Data Flowing TO Native (parameters, props) |
| 127 | +| Change | Safe? | |
| 128 | +|--------|-------| |
| 129 | +| Add optional property | ✅ | |
| 130 | +| Add required property | ❌ | |
| 131 | +| Remove property | ✅ | |
| 132 | +| Make property optional | ❌ | |
| 133 | +| Add union member | ❌ | |
| 134 | +| Remove union member | ✅ | |
| 135 | +| Add enum member | ❌ | |
| 136 | +| Remove enum member | ✅ | |
| 137 | + |
| 138 | +### Data Flowing FROM Native (return values, constants) |
| 139 | +| Change | Safe? | |
| 140 | +|--------|-------| |
| 141 | +| Add optional property | ✅ | |
| 142 | +| Add required property | ❌ | |
| 143 | +| Remove property | ✅ | |
| 144 | +| Make property required | ❌ | |
| 145 | +| Add union member | ✅ | |
| 146 | +| Remove union member | ❌ | |
| 147 | +| Add enum member | ✅ | |
| 148 | +| Remove enum member | ❌ | |
| 149 | + |
| 150 | +## Common Gotchas |
| 151 | + |
| 152 | +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. |
| 153 | + |
| 154 | +2. **Union ordering**: Unions are sorted before comparison, so `'a' | 'b'` equals `'b' | 'a'`. |
| 155 | + |
| 156 | +3. **Nullable vs Optional**: These are distinct concepts: |
| 157 | + - Optional: Property may be absent (`prop?: T`) |
| 158 | + - Nullable: Value may be null/undefined (`prop: ?T`) |
| 159 | + |
| 160 | +4. **Type Aliases**: Resolved during comparison. Different alias names with identical structure are treated as matching. |
| 161 | + |
| 162 | +5. **Component Props with Defaults**: `WithDefault` types are stripped during comparison - only the underlying type matters for compatibility. |
| 163 | + |
| 164 | +6. **Int32EnumTypeAnnotation**: Currently converted to `AnyTypeAnnotation` because the tool lacks support for number literal unions. |
| 165 | + |
| 166 | +## Adding New Type Support |
| 167 | + |
| 168 | +1. Add the type case to `compareTypeAnnotation()` in `TypeDiffing.js` |
| 169 | +2. Add sorting logic in `SortTypeAnnotations.js` (`compareTypeAnnotationForSorting`) |
| 170 | +3. Add formatting in `ErrorFormatting.js` (`formatTypeAnnotation`) |
| 171 | +4. Add test fixtures and tests covering the new type |
| 172 | +5. If it affects safety analysis, update `VersionDiffing.js` checks |
| 173 | + |
| 174 | +## Code Style |
| 175 | + |
| 176 | +- All source files use `@flow strict-local` or `@flow strict` |
| 177 | +- All source files require `@format` pragma for Prettier |
| 178 | +- Tests use `@noflow` or `@flow` (not strict) |
0 commit comments