diff --git a/.github/workflows/android-build-test.yml b/.github/workflows/android-build-test.yml index a56f773d7b..d0444ef5f9 100644 --- a/.github/workflows/android-build-test.yml +++ b/.github/workflows/android-build-test.yml @@ -35,8 +35,12 @@ jobs: with: node-version: 18 cache: 'yarn' - cache-dependency-path: 'Example/yarn.lock' - - name: Install node dependencies + cache-dependency-path: | + yarn.lock + Example/yarn.lock + - name: Install dependencies + run: yarn + - name: Install Example app dependencies working-directory: ${{ env.WORKING_DIRECTORY }} run: yarn - name: Build app diff --git a/.github/workflows/check-archs-consistency.yml b/.github/workflows/check-archs-consistency.yml new file mode 100644 index 0000000000..2e8583b5e9 --- /dev/null +++ b/.github/workflows/check-archs-consistency.yml @@ -0,0 +1,26 @@ +name: Test consistency between Paper & Fabric +on: + pull_request: + branches: + - main + paths: + - src/fabric/** + - android/src/paper/java/com/facebook/react/viewmanagers/** +jobs: + check: + runs-on: ubuntu-latest + concurrency: + group: check-archs-consistency-${{ github.ref }} + cancel-in-progress: true + steps: + - name: checkout + uses: actions/checkout@v2 + - name: Use Node.js 18 + uses: actions/setup-node@v2 + 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/Example/android/app/build.gradle b/Example/android/app/build.gradle index 931486c89f..9f56f8ccca 100644 --- a/Example/android/app/build.gradle +++ b/Example/android/app/build.gradle @@ -135,3 +135,13 @@ dependencies { } apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project) +apply plugin: 'com.github.node-gradle.node' + +task syncArchs(type: NodeTask) { + group = 'Build' + description = 'Run sync beetwen Paper and Fabric arch' + yarn_install + script = file('../../../scripts/codegen-sync-archs.js') +} + +tasks['preBuild'].dependsOn(syncArchs) diff --git a/Example/android/build.gradle b/Example/android/build.gradle index 5406af3043..c52488c3ca 100644 --- a/Example/android/build.gradle +++ b/Example/android/build.gradle @@ -10,11 +10,13 @@ buildscript { repositories { google() mavenCentral() + gradlePluginPortal() // Added to run node ./scripts/codegen-sync-archs.js using syncArchs task } dependencies { classpath("com.android.tools.build:gradle") classpath("com.facebook.react:react-native-gradle-plugin") classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion") + classpath("com.github.node-gradle:gradle-node-plugin:7.0.2") // Added to run node ./scripts/codegen-sync-archs.js using syncArchs task } } diff --git a/Example/android/gradle.properties b/Example/android/gradle.properties index 729cdc9f66..4896485f57 100644 --- a/Example/android/gradle.properties +++ b/Example/android/gradle.properties @@ -49,5 +49,3 @@ hermesEnabled=true # This is necessary, because the code checking for multiple instances # detects package versions from other example applications in the repository. disableMultipleInstancesCheck=true - -isScreensExampleApp=true diff --git a/FabricExample/android/gradle.properties b/FabricExample/android/gradle.properties index be4b1f207c..99fc223edf 100644 --- a/FabricExample/android/gradle.properties +++ b/FabricExample/android/gradle.properties @@ -39,5 +39,3 @@ newArchEnabled=true # Use this property to enable or disable the Hermes JS engine. # If set to false, you will be using JSC instead. hermesEnabled=true - -isScreensExampleApp=true diff --git a/TVOSExample/android/gradle.properties b/TVOSExample/android/gradle.properties index 0d70f5169b..97e3e41af1 100644 --- a/TVOSExample/android/gradle.properties +++ b/TVOSExample/android/gradle.properties @@ -40,4 +40,3 @@ newArchEnabled=false # If set to false, you will be using JSC instead. hermesEnabled=true -isScreensExampleApp=true diff --git a/android/build.gradle b/android/build.gradle index fbc634305c..a61c220f9e 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -196,79 +196,3 @@ dependencies { } } } - -def isScreensExampleApp() { - return project.hasProperty('isScreensExampleApp') && project.property('isScreensExampleApp') == "true" -} - -def getAbsoluteCodegenArtifactsPaperDestination() { - if (!project.hasProperty('codegenArtifactsPaperDestination')) { - throw new Exception('[RNScreens] Please fill codegenArtifactsPaperDestination variable in android/gradle.properties correct path to paper paper destination') - } - - return "${project.rootDir}/../../${project.property('codegenArtifactsPaperDestination')}" -} - -def getAbsoluteCodegenArtifactsSource() { - if (!project.hasProperty('codegenArtifactsSource')) { - throw new Exception('[RNScreens] Please fill codegenArtifactsSource variable in android/gradle.properties correct path to codegenerated artifacts') - } - - return "${project.rootDir}/../../${project.property('codegenArtifactsSource')}" -} - - -tasks.register("copyCodegenArtifacts") { - group 'After build tasks' - description 'Tasks which copy codegen artifacts to paper architecture' - - if (!isScreensExampleApp() || !isNewArchitectureEnabled()) { - return - } - - dependsOn tasks.generateCodegenArtifactsFromSchema - - doLast { - - def absoluteCodegenArtifactsPaperDestination = getAbsoluteCodegenArtifactsPaperDestination() - def absoluteCodegenArtifactsSource = getAbsoluteCodegenArtifactsSource() - - def existingFiles = fileTree(absoluteCodegenArtifactsPaperDestination).matching { - include '**/*.java' - } - - def generatedFiles = fileTree(absoluteCodegenArtifactsSource).matching { - include '**/*.java' - } - - def existingFilesMap = [:] - - existingFiles.forEach { existingFile -> - existingFilesMap[existingFile.name] = 1 - } - - generatedFiles.forEach { generatedFile -> - if (!existingFilesMap.containsKey(generatedFile.name)) { - logger.warn("[RNScreens] ${generatedFile.name} not found in paper dir, if it's used in Android you need to copy it manually and implement yourself before using auto-copy feature") - } - } - - if (existingFiles.size() == 0) { - logger.warn("[RNScreens] Paper destination with codegen interfaces is empty. This might be okay if you don't have any interfaces/delegates used in Android, if that's not the case please check if codegenArtifactsPaperDestination in android/gradle.properties is correct") - } - - if (existingFiles.size() > generatedFiles.size()) { - throw new Exception("[RNScreens] Number od generated artifacts should be greather then or equal to paper interfaces. Please check if codegenArtifactsSource in android/gradle.properties is correct") - } - - copy { - from absoluteCodegenArtifactsSource - include existingFiles.collect { it.name } - into absoluteCodegenArtifactsPaperDestination - } - } -} - -if (isScreensExampleApp() && isNewArchitectureEnabled()) { - tasks.generateCodegenArtifactsFromSchema.finalizedBy('copyCodegenArtifacts') -} diff --git a/android/gradle.properties b/android/gradle.properties index c2b7b4bedf..708d4222b3 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -21,10 +21,3 @@ android.useAndroidX=true android.enableJetifier=true kotlin.code.style=official - -# Path to codegen output directory with this library view manager's interfaces & delegates. Used by `copyCodegenArtifacts` task that helps to synchronise newly generated files with their Paper conterparts. -codegenArtifactsSource=android/build/generated/source/codegen/java/com/facebook/react/viewmanagers - -# Path to directory with view manager's interfaces & delegates used while running on Paper architecture. This property is used as output path for `copyCodegenArtifacts` task. -# Used for task (copyCodegenArtifacts) that automates copying those interfaces/delegates when codegen is run -codegenArtifactsPaperDestination=android/src/paper/java/com/facebook/react/viewmanagers/ diff --git a/package.json b/package.json index 75c1d81735..391b7852ed 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,9 @@ "lint-android": "./android/gradlew -p android spotlessCheck -q", "lint": "yarn lint-js && yarn lint-android", "release": "yarn prepare && npm login && release-it", - "prepare": "bob build && husky install" + "prepare": "bob build && husky install", + "check-archs-consistency": "node ./scripts/codegen-check-consistency.js", + "sync-archs": "node ./scripts/codegen-sync-archs.js" }, "main": "lib/commonjs/index", "module": "lib/module/index", @@ -127,7 +129,8 @@ "cpp/**/*.{h,cpp}": "yarn format-cpp", "android/src/main/cpp/.{cpp, h}": "yarn format-android-cpp", "android/**/*.kt": "yarn format-android", - "ios/**/*.{h,m,mm,cpp}": "yarn format-ios" + "ios/**/*.{h,m,mm,cpp}": "yarn format-ios", + "src/fabric/*.ts": "yarn sync-archs" }, "react-native-builder-bob": { "source": "src", diff --git a/scripts/codegen-check-consistency.js b/scripts/codegen-check-consistency.js new file mode 100644 index 0000000000..83ddabb4af --- /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 0000000000..2829a58a28 --- /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 0000000000..f760240ad4 --- /dev/null +++ b/scripts/codegen-utils.js @@ -0,0 +1,124 @@ +const fs = require('fs'); +const path = require('path'); +const { execSync } = require('child_process'); +const packageJSON = require('../package.json'); + +const ERROR_PREFIX = 'RNScreens' +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_FOLDERS = 'java/com/facebook/react/viewmanagers'; +const CODEGEN_FILES_DIR = `${GENERATED_DIR}/source/codegen/${SOURCE_FOLDERS}`; +const OLD_ARCH_FILES_DIR = `${OLD_ARCH_DIR}/${SOURCE_FOLDERS}`; + +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(); + + const generatedFiles = fs.readdirSync(CODEGEN_FILES_DIR); + const oldArchFiles = fs.readdirSync(OLD_ARCH_FILES_DIR); + 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(oldArchFile => { + if (!fs.existsSync(`${CODEGEN_FILES_DIR}/${oldArchFile}`)) { + console.warn( + `[${ERROR_PREFIX}] ${existingFile.name} file does not exist in codegen artifacts source destination. Please check if you still need this interface/delagete.` + ); + } + }); + + oldArchFiles.forEach(file => { + exec(`cp -rf ${CODEGEN_FILES_DIR}/${file} ${OLD_ARCH_FILES_DIR}/${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(); + + const oldArchFiles = fs.readdirSync(OLD_ARCH_FILES_DIR); + oldArchFiles.forEach(file => { + compareFileAtTwoPaths(file, CODEGEN_FILES_DIR, OLD_ARCH_FILES_DIR); + }); +} + +module.exports = { generateCodegenJavaOldArch, checkCodegenIntegrity };