Replace CONTRIBUTING.md with AGENTS.md (#55301)

Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/55301

Replace the brief CONTRIBUTING.md with a comprehensive AGENTS.md that provides AI coding assistants with detailed guidance for working with the react-native-compatibility-check package. The new documentation covers the three-stage pipeline architecture, compatibility rules, testing patterns, and design principles that were previously only partially documented.

Changelog: [Internal]

Reviewed By: makovkastar

Differential Revision: D91275922

fbshipit-source-id: 46fa6d887dd62f4f650d3a77468eb3e864275802
This commit is contained in:
Eli White
2026-01-26 14:04:07 -08:00
committed by meta-codesync[bot]
parent 89b8628f8c
commit 9553d7eabe
2 changed files with 178 additions and 36 deletions
@@ -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)
@@ -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.