Package Exports
- @react-native/compatibility-check
- @react-native/compatibility-check/package.json
Readme
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 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:
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:
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
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:
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.
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:
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:
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:
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.
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.
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`);
}
}