mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
60e3921f9c
Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/49340 This tool enables checking the boundary between JavaScript and Native for backwards incompatible changes to protect against crashes. This is useful for: - Local Development - Over the Air updates on platforms that support it - Theoretically: Server Components with React Native Check out the Readme for more information Changelog: [General][Added] Open Sourcing React Native's Compatibility Check Reviewed By: panagosg7 Differential Revision: D69476742 fbshipit-source-id: 8af6039839c5475c1258fa82d9750a9320cf0751
246 lines
6.9 KiB
Markdown
246 lines
6.9 KiB
Markdown
# **React Native compatibility-check**
|
||
|
||
Status: Experimental (stage 1)
|
||
|
||
Work In Progress. Documentation is lacking, and intended to be used by power
|
||
users at this point.
|
||
|
||
This tool enables checking the boundary between JavaScript and Native for
|
||
backwards incompatible changes to protect against crashes.
|
||
|
||
This is useful for:
|
||
|
||
- Local Development
|
||
- Over the Air updates on platforms that support it
|
||
- Theoretically: Server Components with React Native
|
||
|
||
## **Motivating Problems**
|
||
|
||
Let’s look at some motivating examples for this project.
|
||
|
||
> [!NOTE]
|
||
> The examples below are written with Flow, but the compatibility-check
|
||
> tool is agnostic to the types you write. The compatibility-check runs on JSON
|
||
> schema files, most commonly generated by the
|
||
> [@react-native/codegen](https://www.npmjs.com/package/@react-native/codegen)
|
||
> tool which supports both TypeScript and Flow.
|
||
|
||
### **Adding new methods**
|
||
|
||
You might have an Analytics Native Module in your app, and you last built the
|
||
native client a couple of days ago:
|
||
|
||
```javascript
|
||
export interface Spec extends TurboModule {
|
||
log: (eventName: string, content: string) => void;
|
||
}
|
||
```
|
||
|
||
And you are working on a change to add a new method to this Native Module:
|
||
|
||
```javascript
|
||
export interface Spec extends TurboModule {
|
||
log: (eventName: string, content: string) => void;
|
||
logError: (message: string) => void;
|
||
}
|
||
```
|
||
|
||
```
|
||
NativeAnalytics.logError('Oh No! We hit a crash')
|
||
```
|
||
|
||
Since you are working on this, you’ve built a new native client and tested the
|
||
change on your computer and everything works.
|
||
|
||
However, when your colleague pulls your latest changes and tries to run it,
|
||
they’ll get a crash `logError is not a function`. They need to rebuild their
|
||
native client\!
|
||
|
||
Using this tool, you can detect this incompatibility at build time, getting an
|
||
error that looks like:
|
||
|
||
```
|
||
NativeAnalytics: Object added required properties, which native will not provide
|
||
-- logError
|
||
```
|
||
|
||
Errors like this can occur for much more nuanced reasons than adding a method.
|
||
For example:
|
||
|
||
### **Sending native new union values**
|
||
|
||
```javascript
|
||
export interface Spec extends TurboModule {
|
||
// You add 'system' to this union
|
||
+setColorScheme: (color: 'light' | 'dark') => void;
|
||
}
|
||
```
|
||
|
||
If you add a new option of `system` and add native support for that option, when
|
||
you call this method with `system` on your commit it would work but on an older
|
||
build not expecting `system` it will crash. This tool will give you the error
|
||
message:
|
||
|
||
```
|
||
ColorManager.setColorScheme parameter 0: Union added items, but native will not expect/support them
|
||
-- position 3 system
|
||
```
|
||
|
||
### **Changing an enum value sent from native**
|
||
|
||
As another example, say you are getting the color scheme from the system as an
|
||
integer value, used in JavaScript as an enum:
|
||
|
||
```javascript
|
||
enum TestEnum {
|
||
LIGHT = 1,
|
||
DARK = 2,
|
||
SYSTEM = 3,
|
||
}
|
||
|
||
export interface Spec extends TurboModule {
|
||
getColorScheme: () => TestEnum;
|
||
}
|
||
```
|
||
|
||
And you realize you actually need native to send `-1` for System instead of 3\.
|
||
|
||
```javascript
|
||
enum TestEnum {
|
||
LIGHT = 1,
|
||
DARK = 2,
|
||
SYSTEM = -1,
|
||
}
|
||
```
|
||
|
||
If you make this change and run the JavaScript on an old build, it might still
|
||
send JavaScript the value 3, which your JavaScript isn’t handling anymore\!
|
||
|
||
This tool gives an error:
|
||
|
||
```javascript
|
||
ColorManager: Object contained a property with a type mismatch
|
||
-- getColorScheme: has conflicting type changes
|
||
--new: ()=>Enum<number>
|
||
--old: ()=>Enum<number>
|
||
Function return types do not match
|
||
--new: ()=>Enum<number>
|
||
--old: ()=>Enum<number>
|
||
Enum types do not match
|
||
--new: Enum<number> {LIGHT = 1, DARK = 2, SYSTEM = -1}
|
||
--old: Enum<number> {LIGHT = 1, DARK = 2, SYSTEM = 3}
|
||
Enum contained a member with a type mismatch
|
||
-- Member SYSTEM: has conflicting changes
|
||
--new: -1
|
||
--old: 3
|
||
Numeric literals are not equal
|
||
--new: -1
|
||
--old: 3
|
||
|
||
```
|
||
|
||
## **Avoiding Breaking Changes**
|
||
|
||
You can use this tool to either detect changes locally to warn that you need to
|
||
install a new native build, or when doing OTA you might need to guarantee that
|
||
the changes in your PR are compatible with the native client they’ll be running
|
||
in.
|
||
|
||
### **Example 1**
|
||
|
||
In example 1, when adding logError, it needs to be optional to be safe:
|
||
|
||
```javascript
|
||
export interface Spec extends TurboModule {
|
||
log: (eventName: string, content: string) => void;
|
||
logError?: (message: string) => void;
|
||
}
|
||
```
|
||
|
||
That will enforce if you are using TypeScript or Flow that you check if the
|
||
native client supports logError before calling it:
|
||
|
||
```javascript
|
||
if (NativeAnalytics.logError) {
|
||
NativeAnalytics.logError('Oh No! We hit a crash');
|
||
}
|
||
```
|
||
|
||
### **Example 2**
|
||
|
||
When you want to add '`system'` as a value to the union, modifying the existing
|
||
union is not safe. You would need to add a new optional method that has that
|
||
change. You can clean up the old method when you know that all of the builds you
|
||
ever want to run this JavaScript on have native support.
|
||
|
||
```javascript
|
||
export interface Spec extends TurboModule {
|
||
+setColorScheme: (color: 'light' | 'dark') => void
|
||
+setColorSchemeWithSystem?: (color: 'light' | 'dark' | 'system') => void
|
||
}
|
||
```
|
||
|
||
### **Example 3**
|
||
|
||
Changing a union case is similar to Example 2, you would either need a new
|
||
method, or support the existing value and the new `-1`.
|
||
|
||
```
|
||
enum TestEnum {
|
||
LIGHT = 1,
|
||
DARK = 2,
|
||
SYSTEM = 3,
|
||
SYSTEM_ALSO = -1,
|
||
}
|
||
```
|
||
|
||
## **Installation**
|
||
|
||
```
|
||
yarn add @react-native/compatibility-check
|
||
```
|
||
|
||
## **Usage**
|
||
|
||
To use this package, you’ll need a script that works something like this:
|
||
|
||
This script checks the compatibility of a React Native app's schema between two
|
||
versions. It takes into account the changes made to the schema and determines
|
||
whether they are compatible or not.
|
||
|
||
```javascript
|
||
import {compareSchemas} from '@react-native/compatibility-check';
|
||
const util = require('util');
|
||
|
||
async function run(argv: Argv, STDERR: string) {
|
||
const debug = (log: mixed) => {
|
||
argv.debug &&
|
||
console.info(util.inspect(log, {showHidden: false, depth: null}));
|
||
};
|
||
|
||
const currentSchema =
|
||
JSON.parse(/*you'll read the file generated by codegen wherever it is in your app*/);
|
||
const previousSchema =
|
||
JSON.parse(/*you'll read the schema file that you persisted from when your native app was built*/);
|
||
|
||
const safetyResult = compareSchemas(currentSchema, previousSchema);
|
||
|
||
const summary = safetyResult.getSummary();
|
||
switch (summary.status) {
|
||
case 'ok':
|
||
debug('No changes in boundary');
|
||
console.log(JSON.stringify(summary));
|
||
break;
|
||
case 'patchable':
|
||
debug('Changes in boundary, but are compatible');
|
||
debug(result.getDebugInfo());
|
||
console.log(JSON.stringify(summary));
|
||
break;
|
||
default:
|
||
debug(result.getDebugInfo());
|
||
console.error(JSON.stringify(result.getErrors()));
|
||
throw new Error(`Incompatible changes in boundary`);
|
||
}
|
||
}
|
||
```
|