Skip to content

Commit 60e3921

Browse files
elicwhitefacebook-github-bot
authored andcommitted
Initial Open Sourcing of React Native Compatibility Check (#49340)
Summary: Pull Request resolved: #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
1 parent d11f622 commit 60e3921

File tree

80 files changed

+10327
-0
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

80 files changed

+10327
-0
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
lib
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
This tool is essentially a type checker, and as such it can be difficult to
2+
understand the data flow. Luckily, there are fairly extensive tests which can
3+
aid in ramping up.
4+
5+
This tool is made up of 3 primary stages: TypeDiffing, VersionDiffing, and
6+
ErrorFormatting.
7+
8+
At a high level, the schemas are passed to TypeDiffing which is the pure
9+
typechecker. It returns all differences between the types. VersionDiffing then
10+
interprets these results and decides if some of those changes are actually safe
11+
in the context of React Native’s JS/Native boundary.
12+
13+
For example, if you have a NativeModule method that returns a string union
14+
`small | medium | large`. Any changes to that union would be flagged by
15+
TypeDiffing as incompatible. However, adding a value to that union is safe
16+
because it ensures your JS code handles more cases than native returns. Removing
17+
a value from that union isn’t safe though because it means your JS no longer
18+
handles something native might return which could cause an exception.
19+
20+
VersionDiffing encodes the logic of what is safe and what isn’t;
21+
property/union/enum additions and removals, changing something from optional to
22+
required and vice versa, etc. VersionDiffing has knowledge of components and
23+
modules.
24+
25+
VersionDiffing returns a set of incompatible changes, which then gets passed to
26+
ErrorFormatting. ErrorFormatting does as you’d expect, converting these deep
27+
objects into nicely formatted strings.
28+
29+
When contributing, some principles:
30+
31+
- Keep TypeDiffing and ErrorFormatting pure. They should only know about
32+
JavaScript types, not React Native specific concepts
33+
- Add tests for every case you can think of. This codebase can be complex and
34+
hard to reason about when making changes. The only way to stay sane is to be
35+
able to rely on the tests to catch anything bad you’ve done. Do yourself and
36+
future contributors a favor.
Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
# **React Native compatibility-check**
2+
3+
Status: Experimental (stage 1)
4+
5+
Work In Progress. Documentation is lacking, and intended to be used by power
6+
users at this point.
7+
8+
This tool enables checking the boundary between JavaScript and Native for
9+
backwards incompatible changes to protect against crashes.
10+
11+
This is useful for:
12+
13+
- Local Development
14+
- Over the Air updates on platforms that support it
15+
- Theoretically: Server Components with React Native
16+
17+
## **Motivating Problems**
18+
19+
Let’s look at some motivating examples for this project.
20+
21+
> [!NOTE]
22+
> The examples below are written with Flow, but the compatibility-check
23+
> tool is agnostic to the types you write. The compatibility-check runs on JSON
24+
> schema files, most commonly generated by the
25+
> [@react-native/codegen](https://www.npmjs.com/package/@react-native/codegen)
26+
> tool which supports both TypeScript and Flow.
27+
28+
### **Adding new methods**
29+
30+
You might have an Analytics Native Module in your app, and you last built the
31+
native client a couple of days ago:
32+
33+
```javascript
34+
export interface Spec extends TurboModule {
35+
log: (eventName: string, content: string) => void;
36+
}
37+
```
38+
39+
And you are working on a change to add a new method to this Native Module:
40+
41+
```javascript
42+
export interface Spec extends TurboModule {
43+
log: (eventName: string, content: string) => void;
44+
logError: (message: string) => void;
45+
}
46+
```
47+
48+
```
49+
NativeAnalytics.logError('Oh No! We hit a crash')
50+
```
51+
52+
Since you are working on this, you’ve built a new native client and tested the
53+
change on your computer and everything works.
54+
55+
However, when your colleague pulls your latest changes and tries to run it,
56+
they’ll get a crash `logError is not a function`. They need to rebuild their
57+
native client\!
58+
59+
Using this tool, you can detect this incompatibility at build time, getting an
60+
error that looks like:
61+
62+
```
63+
NativeAnalytics: Object added required properties, which native will not provide
64+
-- logError
65+
```
66+
67+
Errors like this can occur for much more nuanced reasons than adding a method.
68+
For example:
69+
70+
### **Sending native new union values**
71+
72+
```javascript
73+
export interface Spec extends TurboModule {
74+
// You add 'system' to this union
75+
+setColorScheme: (color: 'light' | 'dark') => void;
76+
}
77+
```
78+
79+
If you add a new option of `system` and add native support for that option, when
80+
you call this method with `system` on your commit it would work but on an older
81+
build not expecting `system` it will crash. This tool will give you the error
82+
message:
83+
84+
```
85+
ColorManager.setColorScheme parameter 0: Union added items, but native will not expect/support them
86+
-- position 3 system
87+
```
88+
89+
### **Changing an enum value sent from native**
90+
91+
As another example, say you are getting the color scheme from the system as an
92+
integer value, used in JavaScript as an enum:
93+
94+
```javascript
95+
enum TestEnum {
96+
LIGHT = 1,
97+
DARK = 2,
98+
SYSTEM = 3,
99+
}
100+
101+
export interface Spec extends TurboModule {
102+
getColorScheme: () => TestEnum;
103+
}
104+
```
105+
106+
And you realize you actually need native to send `-1` for System instead of 3\.
107+
108+
```javascript
109+
enum TestEnum {
110+
LIGHT = 1,
111+
DARK = 2,
112+
SYSTEM = -1,
113+
}
114+
```
115+
116+
If you make this change and run the JavaScript on an old build, it might still
117+
send JavaScript the value 3, which your JavaScript isn’t handling anymore\!
118+
119+
This tool gives an error:
120+
121+
```javascript
122+
ColorManager: Object contained a property with a type mismatch
123+
-- getColorScheme: has conflicting type changes
124+
--new: ()=>Enum<number>
125+
--old: ()=>Enum<number>
126+
Function return types do not match
127+
--new: ()=>Enum<number>
128+
--old: ()=>Enum<number>
129+
Enum types do not match
130+
--new: Enum<number> {LIGHT = 1, DARK = 2, SYSTEM = -1}
131+
--old: Enum<number> {LIGHT = 1, DARK = 2, SYSTEM = 3}
132+
Enum contained a member with a type mismatch
133+
-- Member SYSTEM: has conflicting changes
134+
--new: -1
135+
--old: 3
136+
Numeric literals are not equal
137+
--new: -1
138+
--old: 3
139+
140+
```
141+
142+
## **Avoiding Breaking Changes**
143+
144+
You can use this tool to either detect changes locally to warn that you need to
145+
install a new native build, or when doing OTA you might need to guarantee that
146+
the changes in your PR are compatible with the native client they’ll be running
147+
in.
148+
149+
### **Example 1**
150+
151+
In example 1, when adding logError, it needs to be optional to be safe:
152+
153+
```javascript
154+
export interface Spec extends TurboModule {
155+
log: (eventName: string, content: string) => void;
156+
logError?: (message: string) => void;
157+
}
158+
```
159+
160+
That will enforce if you are using TypeScript or Flow that you check if the
161+
native client supports logError before calling it:
162+
163+
```javascript
164+
if (NativeAnalytics.logError) {
165+
NativeAnalytics.logError('Oh No! We hit a crash');
166+
}
167+
```
168+
169+
### **Example 2**
170+
171+
When you want to add '`system'` as a value to the union, modifying the existing
172+
union is not safe. You would need to add a new optional method that has that
173+
change. You can clean up the old method when you know that all of the builds you
174+
ever want to run this JavaScript on have native support.
175+
176+
```javascript
177+
export interface Spec extends TurboModule {
178+
+setColorScheme: (color: 'light' | 'dark') => void
179+
+setColorSchemeWithSystem?: (color: 'light' | 'dark' | 'system') => void
180+
}
181+
```
182+
183+
### **Example 3**
184+
185+
Changing a union case is similar to Example 2, you would either need a new
186+
method, or support the existing value and the new `-1`.
187+
188+
```
189+
enum TestEnum {
190+
LIGHT = 1,
191+
DARK = 2,
192+
SYSTEM = 3,
193+
SYSTEM_ALSO = -1,
194+
}
195+
```
196+
197+
## **Installation**
198+
199+
```
200+
yarn add @react-native/compatibility-check
201+
```
202+
203+
## **Usage**
204+
205+
To use this package, you’ll need a script that works something like this:
206+
207+
This script checks the compatibility of a React Native app's schema between two
208+
versions. It takes into account the changes made to the schema and determines
209+
whether they are compatible or not.
210+
211+
```javascript
212+
import {compareSchemas} from '@react-native/compatibility-check';
213+
const util = require('util');
214+
215+
async function run(argv: Argv, STDERR: string) {
216+
const debug = (log: mixed) => {
217+
argv.debug &&
218+
console.info(util.inspect(log, {showHidden: false, depth: null}));
219+
};
220+
221+
const currentSchema =
222+
JSON.parse(/*you'll read the file generated by codegen wherever it is in your app*/);
223+
const previousSchema =
224+
JSON.parse(/*you'll read the schema file that you persisted from when your native app was built*/);
225+
226+
const safetyResult = compareSchemas(currentSchema, previousSchema);
227+
228+
const summary = safetyResult.getSummary();
229+
switch (summary.status) {
230+
case 'ok':
231+
debug('No changes in boundary');
232+
console.log(JSON.stringify(summary));
233+
break;
234+
case 'patchable':
235+
debug('Changes in boundary, but are compatible');
236+
debug(result.getDebugInfo());
237+
console.log(JSON.stringify(summary));
238+
break;
239+
default:
240+
debug(result.getDebugInfo());
241+
console.error(JSON.stringify(result.getErrors()));
242+
throw new Error(`Incompatible changes in boundary`);
243+
}
244+
}
245+
```
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
* @format
9+
* @oncall react_native
10+
*/
11+
12+
export * from './src';
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{
2+
"name": "@react-native/compatibility-check",
3+
"version": "0.0.1",
4+
"description": "Check a React Native app's boundary between JS and Native for incompatibilities",
5+
"license": "MIT",
6+
"repository": {
7+
"type": "git",
8+
"url": "git+https://github.com/facebook/react-native.git",
9+
"directory": "packages/react-native-compatibility-check"
10+
},
11+
"homepage": "https://github.com/facebook/react-native/tree/HEAD/packages/react-native-compatibility-check#readme",
12+
"keywords": [
13+
"boundary",
14+
"crashes",
15+
"native",
16+
"codegen",
17+
"tools",
18+
"react-native"
19+
],
20+
"bugs": "https://github.com/facebook/react-native/issues",
21+
"engines": {
22+
"node": ">=18"
23+
},
24+
"exports": {
25+
".": "./src/index.js",
26+
"./package.json": "./package.json"
27+
},
28+
"files": [
29+
"dist"
30+
],
31+
"dependencies": {
32+
"@react-native/codegen": "0.79.0-main"
33+
},
34+
"devDependencies": {
35+
"flow-remove-types": "^2.237.2",
36+
"rimraf": "^3.0.2"
37+
}
38+
}

0 commit comments

Comments
 (0)