UNPKG

@react-native/compatibility-check

Version:

Check a React Native app's boundary between JS and Native for incompatibilities

246 lines (189 loc) 7.08 kB
# **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`); } } ```