Feat: add scripts to sync archs (#2357)

Changes moved from:
https://github.com/software-mansion/react-native-screens/pull/2224

## Description

When changing native props on Fabric, codegen generates corresponding
interfaces and delegates. To make sure both implementations are
consistent, we implement those interfaces on Paper too. Currently, after
generating interfaces using codegen, developer needs to copy
corresponding files for paper manually. This task adds Gradle task, that
automates this.

## Changes
Current assumption: 
Two scripts: `check-archs-consistency` and `sync-archs`. The first one
generates codegen interfaces and compares them with what we have for
paper, the second generates and copies for paper to sync the archs.
- sync is run when staged on changes to `src/paper`
- check is run on CI when `src/paper` or
`android/src/paper/java/com/facebook/react/viewmanagers` changes

## Test code and steps to reproduce

Open `src/fabric/LineNativeComponent.ts` or/and
`src/fabric/NativeSvgRenderableModule.ts` and remove any proper form
interface. Now:
- when building paper, interface should be updated
- when committing, interface should be updated
- if committed and pushed, Test consistency between Paper & Fabric
should fail :)
Brining back the prop and repeating up should cause the interface back
and CI green.
Posting changes in other places should cause CI task to run. 

You can also run those commands yourself using `yarn
check-archs-consistency` and `yarn sync-archs`
This commit is contained in:
Maciej Stosio
2024-07-22 14:58:37 +02:00
committed by GitHub
parent 2da02cf259
commit 567e90521a
8 changed files with 187 additions and 17 deletions

View File

@@ -0,0 +1,27 @@
name: Test consistency between Paper & Fabric
on:
pull_request:
branches:
- main
paths:
- src/fabric/**
- android/src/paper/java/com/facebook/react/viewmanagers/**
- android/src/paper/java/com/horcrux/svg/**
jobs:
check:
runs-on: ubuntu-latest
concurrency:
group: check-archs-consistency-${{ github.ref }}
cancel-in-progress: true
steps:
- name: checkout
uses: actions/checkout@v4
- name: Use Node.js 18
uses: actions/setup-node@v4
with:
node-version: 18
cache: 'yarn'
- name: Install node dependencies
run: yarn
- name: Check Android Paper & Fabric generated interfaces consistency
run: yarn check-archs-consistency

View File

@@ -3,7 +3,7 @@ apply plugin: 'com.diffplug.spotless'
spotless { spotless {
java { java {
target 'src/main/java/**/*.java', 'src/paper/java/com/horcrux/svg/**/*.java' target 'src/main/java/**/*.java'
googleJavaFormat() googleJavaFormat()
} }
} }

View File

@@ -1,13 +1,15 @@
/** /**
* This code was generated by * This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen).
* [react-native-codegen](https://www.npmjs.com/package/react-native-codegen).
* *
* <p>Do not edit this file as changes may cause incorrect behavior and will be lost once the code * Do not edit this file as changes may cause incorrect behavior and will be lost
* is regenerated. * once the code is regenerated.
* *
* @generated by codegen project: GenerateModuleJavaSpec.js * @generated by codegen project: GenerateModuleJavaSpec.js
*
* @nolint * @nolint
*/ */
package com.horcrux.svg; package com.horcrux.svg;
import com.facebook.proguard.annotations.DoNotStrip; import com.facebook.proguard.annotations.DoNotStrip;
@@ -15,14 +17,14 @@ import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod; import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.ReactModuleWithSpec;
import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.WritableMap; import com.facebook.react.bridge.WritableMap;
import com.facebook.react.turbomodule.core.interfaces.TurboModule; import com.facebook.react.turbomodule.core.interfaces.TurboModule;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import javax.annotation.Nullable; import javax.annotation.Nullable;
public abstract class NativeSvgRenderableModuleSpec extends ReactContextBaseJavaModule public abstract class NativeSvgRenderableModuleSpec extends ReactContextBaseJavaModule implements ReactModuleWithSpec, TurboModule {
implements TurboModule {
public static final String NAME = "RNSVGRenderableModule"; public static final String NAME = "RNSVGRenderableModule";
public NativeSvgRenderableModuleSpec(ReactApplicationContext reactContext) { public NativeSvgRenderableModuleSpec(ReactApplicationContext reactContext) {

View File

@@ -1,13 +1,15 @@
/** /**
* This code was generated by * This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen).
* [react-native-codegen](https://www.npmjs.com/package/react-native-codegen).
* *
* <p>Do not edit this file as changes may cause incorrect behavior and will be lost once the code * Do not edit this file as changes may cause incorrect behavior and will be lost
* is regenerated. * once the code is regenerated.
* *
* @generated by codegen project: GenerateModuleJavaSpec.js * @generated by codegen project: GenerateModuleJavaSpec.js
*
* @nolint * @nolint
*/ */
package com.horcrux.svg; package com.horcrux.svg;
import com.facebook.proguard.annotations.DoNotStrip; import com.facebook.proguard.annotations.DoNotStrip;
@@ -15,13 +17,13 @@ import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod; import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.ReactModuleWithSpec;
import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.turbomodule.core.interfaces.TurboModule; import com.facebook.react.turbomodule.core.interfaces.TurboModule;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import javax.annotation.Nullable; import javax.annotation.Nullable;
public abstract class NativeSvgViewModuleSpec extends ReactContextBaseJavaModule public abstract class NativeSvgViewModuleSpec extends ReactContextBaseJavaModule implements ReactModuleWithSpec, TurboModule {
implements TurboModule {
public static final String NAME = "RNSVGSvgViewModule"; public static final String NAME = "RNSVGSvgViewModule";
public NativeSvgViewModuleSpec(ReactApplicationContext reactContext) { public NativeSvgViewModuleSpec(ReactApplicationContext reactContext) {
@@ -35,6 +37,5 @@ public abstract class NativeSvgViewModuleSpec extends ReactContextBaseJavaModule
@ReactMethod @ReactMethod
@DoNotStrip @DoNotStrip
public abstract void toDataURL( public abstract void toDataURL(@Nullable Double tag, @Nullable ReadableMap options, @Nullable Callback callback);
@Nullable Double tag, @Nullable ReadableMap options, @Nullable Callback callback);
} }

View File

@@ -57,7 +57,9 @@
"prepare": "npm run bob && husky install", "prepare": "npm run bob && husky install",
"release": "npm login && release-it", "release": "npm login && release-it",
"test": "npm run lint && npm run tsc", "test": "npm run lint && npm run tsc",
"tsc": "tsc --noEmit" "tsc": "tsc --noEmit",
"check-archs-consistency": "node ./scripts/codegen-check-consistency.js",
"sync-archs": "node ./scripts/codegen-sync-archs.js"
}, },
"peerDependencies": { "peerDependencies": {
"react": "*", "react": "*",
@@ -110,7 +112,8 @@
"{src,Example}/**/*.{js,ts,tsx}": "yarn format-js", "{src,Example}/**/*.{js,ts,tsx}": "yarn format-js",
"src/**/*.{js,ts,tsx}": "yarn lint", "src/**/*.{js,ts,tsx}": "yarn lint",
"apple/**/*.{h,m,mm,cpp}": "yarn format-ios", "apple/**/*.{h,m,mm,cpp}": "yarn format-ios",
"android/src/**/*.java": "yarn format-java" "android/src/**/*.java": "yarn format-java",
"src/fabric/*.ts": "yarn sync-archs"
}, },
"nativePackage": true, "nativePackage": true,
"codegenConfig": { "codegenConfig": {

View File

@@ -0,0 +1,3 @@
const { checkCodegenIntegrity } = require('./codegen-utils');
checkCodegenIntegrity();

View File

@@ -0,0 +1,3 @@
const { generateCodegenJavaOldArch } = require('./codegen-utils');
generateCodegenJavaOldArch();

131
scripts/codegen-utils.js Normal file
View File

@@ -0,0 +1,131 @@
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const packageJSON = require('../package.json');
const ERROR_PREFIX = 'RNSVG';
const ROOT_DIR = path.resolve(__dirname, '..');
const ANDROID_DIR = path.resolve(ROOT_DIR, 'android');
const GENERATED_DIR = path.resolve(ANDROID_DIR, 'build/generated');
const OLD_ARCH_DIR = path.resolve(ANDROID_DIR, 'src/paper');
const SPECS_DIR = path.resolve(ROOT_DIR, packageJSON.codegenConfig.jsSrcsDir);
const PACKAGE_NAME = packageJSON.codegenConfig.android.javaPackageName;
const RN_DIR = path.resolve(ROOT_DIR, 'node_modules/react-native');
const RN_CODEGEN_DIR = path.resolve(
ROOT_DIR,
'node_modules/@react-native/codegen'
);
const SOURCE_FOLDER = 'java/com/facebook/react/viewmanagers';
const SOURCE_FOLDER_HORCRUX = 'java/com/horcrux/svg';
const SOURCE_FOLDERS = [
{codegenPath: `${GENERATED_DIR}/source/codegen/${SOURCE_FOLDER}`, oldArchPath: `${OLD_ARCH_DIR}/${SOURCE_FOLDER}`},
{codegenPath: `${GENERATED_DIR}/source/codegen/${SOURCE_FOLDER_HORCRUX}`, oldArchPath: `${OLD_ARCH_DIR}/${SOURCE_FOLDER_HORCRUX}`},
]
function exec(command) {
console.log(`[${ERROR_PREFIX}]> ` + command);
execSync(command);
}
function fixOldArchJavaForRN72Compat(dir) {
// see https://github.com/rnmapbox/maps/issues/3193
const files = fs.readdirSync(dir);
files.forEach(file => {
const filePath = path.join(dir, file);
const fileExtension = path.extname(file);
if (fileExtension === '.java') {
let fileContent = fs.readFileSync(filePath, 'utf-8');
let newFileContent = fileContent.replace(
/extends ReactContextBaseJavaModule implements TurboModule/g,
'extends ReactContextBaseJavaModule implements ReactModuleWithSpec, TurboModule'
);
if (fileContent !== newFileContent) {
// also insert an import line with `import com.facebook.react.bridge.ReactModuleWithSpec;`
newFileContent = newFileContent.replace(
/import com.facebook.react.bridge.ReactMethod;/,
'import com.facebook.react.bridge.ReactMethod;\nimport com.facebook.react.bridge.ReactModuleWithSpec;'
);
console.log(' => fixOldArchJava applied to:', filePath);
fs.writeFileSync(filePath, newFileContent, 'utf-8');
}
} else if (fs.lstatSync(filePath).isDirectory()) {
fixOldArchJavaForRN72Compat(filePath);
}
});
}
async function generateCodegen() {
exec(`rm -rf ${GENERATED_DIR}`);
exec(`mkdir -p ${GENERATED_DIR}/source/codegen/`);
exec(
`node ${RN_CODEGEN_DIR}/lib/cli/combine/combine-js-to-schema-cli.js --platform android ${GENERATED_DIR}/source/codegen/schema.json ${SPECS_DIR}`
);
exec(
`node ${RN_DIR}/scripts/generate-specs-cli.js --platform android --schemaPath ${GENERATED_DIR}/source/codegen/schema.json --outputDir ${GENERATED_DIR}/source/codegen --javaPackageName ${PACKAGE_NAME}`
);
fixOldArchJavaForRN72Compat(`${GENERATED_DIR}/source/codegen/java/`);
}
async function generateCodegenJavaOldArch() {
await generateCodegen();
SOURCE_FOLDERS.forEach(({codegenPath, oldArchPath}) => {
const generatedFiles = fs.readdirSync(codegenPath);
const oldArchFiles = fs.readdirSync(oldArchPath);
const existingFilesSet = new Set(oldArchFiles.map(fileName => fileName));
generatedFiles.forEach(generatedFile => {
if (!existingFilesSet.has(generatedFile)) {
console.warn(
`[${ERROR_PREFIX}] ${generatedFile} not found in paper dir, if it's used on Android you need to copy it manually and implement yourself before using auto-copy feature.`
);
}
});
if (oldArchFiles.length === 0) {
console.warn(
`[${ERROR_PREFIX}] Paper destination with codegen interfaces is empty. This might be okay if you don't have any interfaces/delegates used on Android, otherwise please check if OLD_ARCH_DIR and SOURCE_FOLDERS are set properly.`
);
}
oldArchFiles.forEach(file => {
if (!fs.existsSync(`${codegenPath}/${file}`)) {
console.warn(
`[${ERROR_PREFIX}] ${file} file does not exist in codegen artifacts source destination. Please check if you still need this interface/delagete.`
);
} else {
exec(`cp -rf ${codegenPath}/${file} ${oldArchPath}/${file}`);
}
});
});
}
function compareFileAtTwoPaths(filename, firstPath, secondPath) {
const fileA = fs.readFileSync(`${firstPath}/${filename}`, 'utf-8');
const fileB = fs.readFileSync(`${secondPath}/${filename}`, 'utf-8');
if (fileA !== fileB) {
throw new Error(
`[${ERROR_PREFIX}] File ${filename} is different at ${firstPath} and ${secondPath}. Make sure you commited codegen autogenerated files.`
);
}
}
async function checkCodegenIntegrity() {
await generateCodegen();
SOURCE_FOLDERS.forEach(({codegenPath, oldArchPath}) => {
const oldArchFiles = fs.readdirSync(oldArchPath);
oldArchFiles.forEach(file => {
compareFileAtTwoPaths(file, codegenPath, oldArchPath);
});
});
}
module.exports = { generateCodegenJavaOldArch, checkCodegenIntegrity };