From 567e90521a98ad462b8b22859c5859a8d2e8e736 Mon Sep 17 00:00:00 2001 From: Maciej Stosio Date: Mon, 22 Jul 2024 14:58:37 +0200 Subject: [PATCH] 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` --- .github/workflows/check-archs-consistency.yml | 27 ++++ android/spotless.gradle | 2 +- .../svg/NativeSvgRenderableModuleSpec.java | 14 +- .../horcrux/svg/NativeSvgViewModuleSpec.java | 17 +-- package.json | 7 +- scripts/codegen-check-consistency.js | 3 + scripts/codegen-sync-archs.js | 3 + scripts/codegen-utils.js | 131 ++++++++++++++++++ 8 files changed, 187 insertions(+), 17 deletions(-) create mode 100644 .github/workflows/check-archs-consistency.yml create mode 100644 scripts/codegen-check-consistency.js create mode 100644 scripts/codegen-sync-archs.js create mode 100644 scripts/codegen-utils.js diff --git a/.github/workflows/check-archs-consistency.yml b/.github/workflows/check-archs-consistency.yml new file mode 100644 index 00000000..182f920d --- /dev/null +++ b/.github/workflows/check-archs-consistency.yml @@ -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 diff --git a/android/spotless.gradle b/android/spotless.gradle index 22c25382..6ac6843f 100644 --- a/android/spotless.gradle +++ b/android/spotless.gradle @@ -3,7 +3,7 @@ apply plugin: 'com.diffplug.spotless' spotless { java { - target 'src/main/java/**/*.java', 'src/paper/java/com/horcrux/svg/**/*.java' + target 'src/main/java/**/*.java' googleJavaFormat() } } \ No newline at end of file diff --git a/android/src/paper/java/com/horcrux/svg/NativeSvgRenderableModuleSpec.java b/android/src/paper/java/com/horcrux/svg/NativeSvgRenderableModuleSpec.java index b7184d3d..05d042e9 100644 --- a/android/src/paper/java/com/horcrux/svg/NativeSvgRenderableModuleSpec.java +++ b/android/src/paper/java/com/horcrux/svg/NativeSvgRenderableModuleSpec.java @@ -1,13 +1,15 @@ + /** - * This code was generated by - * [react-native-codegen](https://www.npmjs.com/package/react-native-codegen). + * This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen). * - *

Do not edit this file as changes may cause incorrect behavior and will be lost once the code - * is regenerated. + * Do not edit this file as changes may cause incorrect behavior and will be lost + * once the code is regenerated. * * @generated by codegen project: GenerateModuleJavaSpec.js + * * @nolint */ + package com.horcrux.svg; 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.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.ReactModuleWithSpec; import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.WritableMap; import com.facebook.react.turbomodule.core.interfaces.TurboModule; import javax.annotation.Nonnull; import javax.annotation.Nullable; -public abstract class NativeSvgRenderableModuleSpec extends ReactContextBaseJavaModule - implements TurboModule { +public abstract class NativeSvgRenderableModuleSpec extends ReactContextBaseJavaModule implements ReactModuleWithSpec, TurboModule { public static final String NAME = "RNSVGRenderableModule"; public NativeSvgRenderableModuleSpec(ReactApplicationContext reactContext) { diff --git a/android/src/paper/java/com/horcrux/svg/NativeSvgViewModuleSpec.java b/android/src/paper/java/com/horcrux/svg/NativeSvgViewModuleSpec.java index e20a6db2..397ee9b0 100644 --- a/android/src/paper/java/com/horcrux/svg/NativeSvgViewModuleSpec.java +++ b/android/src/paper/java/com/horcrux/svg/NativeSvgViewModuleSpec.java @@ -1,13 +1,15 @@ + /** - * This code was generated by - * [react-native-codegen](https://www.npmjs.com/package/react-native-codegen). + * This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen). * - *

Do not edit this file as changes may cause incorrect behavior and will be lost once the code - * is regenerated. + * Do not edit this file as changes may cause incorrect behavior and will be lost + * once the code is regenerated. * * @generated by codegen project: GenerateModuleJavaSpec.js + * * @nolint */ + package com.horcrux.svg; 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.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.ReactModuleWithSpec; import com.facebook.react.bridge.ReadableMap; import com.facebook.react.turbomodule.core.interfaces.TurboModule; import javax.annotation.Nonnull; import javax.annotation.Nullable; -public abstract class NativeSvgViewModuleSpec extends ReactContextBaseJavaModule - implements TurboModule { +public abstract class NativeSvgViewModuleSpec extends ReactContextBaseJavaModule implements ReactModuleWithSpec, TurboModule { public static final String NAME = "RNSVGSvgViewModule"; public NativeSvgViewModuleSpec(ReactApplicationContext reactContext) { @@ -35,6 +37,5 @@ public abstract class NativeSvgViewModuleSpec extends ReactContextBaseJavaModule @ReactMethod @DoNotStrip - public abstract void toDataURL( - @Nullable Double tag, @Nullable ReadableMap options, @Nullable Callback callback); + public abstract void toDataURL(@Nullable Double tag, @Nullable ReadableMap options, @Nullable Callback callback); } diff --git a/package.json b/package.json index accfaf55..bba55fff 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,9 @@ "prepare": "npm run bob && husky install", "release": "npm login && release-it", "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": { "react": "*", @@ -110,7 +112,8 @@ "{src,Example}/**/*.{js,ts,tsx}": "yarn format-js", "src/**/*.{js,ts,tsx}": "yarn lint", "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, "codegenConfig": { diff --git a/scripts/codegen-check-consistency.js b/scripts/codegen-check-consistency.js new file mode 100644 index 00000000..83ddabb4 --- /dev/null +++ b/scripts/codegen-check-consistency.js @@ -0,0 +1,3 @@ +const { checkCodegenIntegrity } = require('./codegen-utils'); + +checkCodegenIntegrity(); diff --git a/scripts/codegen-sync-archs.js b/scripts/codegen-sync-archs.js new file mode 100644 index 00000000..2829a58a --- /dev/null +++ b/scripts/codegen-sync-archs.js @@ -0,0 +1,3 @@ +const { generateCodegenJavaOldArch } = require('./codegen-utils'); + +generateCodegenJavaOldArch(); diff --git a/scripts/codegen-utils.js b/scripts/codegen-utils.js new file mode 100644 index 00000000..1f480a4c --- /dev/null +++ b/scripts/codegen-utils.js @@ -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 };