diff --git a/.github/workflows/expo-beta-road-test.yml b/.github/workflows/expo-beta-road-test.yml new file mode 100644 index 00000000..2b5f7c21 --- /dev/null +++ b/.github/workflows/expo-beta-road-test.yml @@ -0,0 +1,186 @@ +name: Expo beta road test +run-name: Expo beta road test + +on: + schedule: + - cron: '0 6 */2 * *' + workflow_dispatch: + +permissions: + actions: read + contents: read + issues: write + +jobs: + prepare: + name: Detect Expo beta to test + runs-on: ubuntu-latest + outputs: + latest_version: ${{ steps.finalize.outputs.latest_version }} + should_test: ${{ steps.finalize.outputs.should_test }} + steps: + - name: Checkout + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 + + - name: Detect latest Expo beta + id: detect + run: node --experimental-strip-types --no-warnings ./scripts/check-expo-beta.ts --github-output "$GITHUB_OUTPUT" --github-step-summary "$GITHUB_STEP_SUMMARY" + + - name: Check whether this Expo beta was already tested + id: tested + if: steps.detect.outputs.latest_version != '' + env: + GH_TOKEN: ${{ github.token }} + run: | + artifact_id=$(gh api \ + "/repos/${{ github.repository }}/actions/artifacts?per_page=100" \ + --jq '.artifacts[] | select(.expired == false) | select(.name == "expo-beta-tested-${{ steps.detect.outputs.latest_version }}") | .id' \ + | head -n 1) + + if [ -n "$artifact_id" ]; then + echo "already_tested=true" >> "$GITHUB_OUTPUT" + else + echo "already_tested=false" >> "$GITHUB_OUTPUT" + fi + + - name: Finalize test decision + id: finalize + run: | + should_test="${{ steps.detect.outputs.should_test }}" + + if [ "${{ steps.tested.outputs.already_tested || 'false' }}" = 'true' ]; then + should_test='false' + fi + + echo "latest_version=${{ steps.detect.outputs.latest_version }}" >> "$GITHUB_OUTPUT" + echo "should_test=${should_test}" >> "$GITHUB_OUTPUT" + { + echo "- Already tested successfully: ${{ steps.tested.outputs.already_tested || 'false' }}" + echo "- Final should run road tests: ${should_test}" + echo + } >> "$GITHUB_STEP_SUMMARY" + + android: + name: Android road test (Expo beta) + runs-on: ubuntu-latest + needs: prepare + if: needs.prepare.outputs.should_test == 'true' + steps: + - name: Checkout + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 + + - name: Generate and apply Expo beta to ExpoAppBeta + run: node --experimental-strip-types --no-warnings ./scripts/check-expo-beta.ts --expo-version "~54.0.34" --apply --github-step-summary "$GITHUB_STEP_SUMMARY" + + - name: Install Expo beta dependencies + run: | + cd apps/ExpoAppBeta + npx expo install --fix + + - name: Run ExpoAppBeta -> AndroidApp road test + uses: ./.github/actions/androidapp-road-test + with: + flavor: expobeta + rn-project-path: apps/ExpoAppBeta + rn-project-maven-path: com/callstack/rnbrownfield/demo/expobeta/brownfieldlib + + ios: + name: iOS road test (Expo beta) + runs-on: macos-26 + needs: prepare + if: needs.prepare.outputs.should_test == 'true' + steps: + - name: Checkout + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 + + - name: Generate and apply Expo beta to ExpoAppBeta + run: node --experimental-strip-types --no-warnings ./scripts/check-expo-beta.ts --expo-version "~54.0.34" --apply --github-step-summary "$GITHUB_STEP_SUMMARY" + + - name: Install Expo beta dependencies + run: | + cd apps/ExpoAppBeta + npx expo install --fix + + - name: Run ExpoAppBeta -> AppleApp road test + uses: ./.github/actions/appleapp-road-test + with: + variant: expobeta + rn-project-path: apps/ExpoAppBeta + + record-success: + name: Record tested Expo beta + runs-on: ubuntu-latest + needs: [prepare, android, ios] + if: | + needs.prepare.outputs.should_test == 'true' && + needs.android.result == 'success' && + needs.ios.result == 'success' + steps: + - name: Create Expo beta success marker + run: | + mkdir -p expo-beta-test-result + printf '%s\n' '${{ needs.prepare.outputs.latest_version }}' > expo-beta-test-result/version.txt + + - name: Upload Expo beta success marker + uses: actions/upload-artifact@v4 + with: + name: expo-beta-tested-${{ needs.prepare.outputs.latest_version }} + path: expo-beta-test-result/version.txt + retention-days: 90 + + create-issue-on-failure: + name: Create issue on Expo beta failure + runs-on: ubuntu-latest + needs: [prepare, android, ios] + if: | + needs.prepare.outputs.should_test == 'true' && + (needs.android.result == 'failure' || needs.ios.result == 'failure') + steps: + - name: Create or reuse GitHub issue + env: + GH_TOKEN: ${{ github.token }} + run: | + issue_title="Expo beta road test failed for ${{ needs.prepare.outputs.latest_version }}" + existing_issue_number=$(gh issue list \ + --search "${issue_title} in:title state:open" \ + --json number \ + --jq '.[0].number // ""') + + if [ -n "$existing_issue_number" ]; then + gh issue comment "$existing_issue_number" --body $'A new failure was detected.\n\n- Expo beta version: `${{ needs.prepare.outputs.latest_version }}`\n- Android result: `${{ needs.android.result }}`\n- iOS result: `${{ needs.ios.result }}`\n- Workflow run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}' + exit 0 + fi + + gh issue create \ + --title "$issue_title" \ + --body $'The Expo beta road test failed.\n\n- Expo beta version: `${{ needs.prepare.outputs.latest_version }}`\n- Android result: `${{ needs.android.result }}`\n- iOS result: `${{ needs.ios.result }}`\n- Workflow run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}' \ + --label question + + summarize: + name: Summarize Expo beta road test + runs-on: ubuntu-latest + needs: [prepare, android, ios, record-success, create-issue-on-failure] + if: always() + steps: + - name: Write workflow summary + run: | + { + echo "## Expo beta road test results" + echo + echo "- Expo beta version: ${{ needs.prepare.outputs.latest_version || 'not found' }}" + echo "- Android result: ${{ needs.android.result || 'skipped' }}" + echo "- iOS result: ${{ needs.ios.result || 'skipped' }}" + echo + if [ "${{ needs.prepare.outputs.should_test }}" != "true" ]; then + echo "No new untested Expo beta required testing." + else + echo "- Success marker uploaded: ${{ needs.record-success.result == 'success' && 'yes' || 'no' }}" + echo "- Issue created/updated on failure: ${{ needs.create-issue-on-failure.result == 'success' && 'yes' || 'no' }}" + fi + } >> "$GITHUB_STEP_SUMMARY" + + - name: Fail if road test failed + if: | + needs.prepare.outputs.should_test == 'true' && + (needs.android.result == 'failure' || needs.ios.result == 'failure') + run: exit 1 diff --git a/apps/AndroidApp/app/build.gradle.kts b/apps/AndroidApp/app/build.gradle.kts index 7d11b815..d32b6717 100644 --- a/apps/AndroidApp/app/build.gradle.kts +++ b/apps/AndroidApp/app/build.gradle.kts @@ -35,6 +35,9 @@ android { create("expo55") { dimension = "app" } + create("expobeta") { + dimension = "app" + } create("vanilla") { dimension = "app" } @@ -72,6 +75,7 @@ dependencies { implementation(libs.androidx.compose.material3) implementation(libs.androidx.appcompat) add("expo55Implementation", libs.brownfieldlib.expo55) + add("expobetaImplementation", libs.brownfieldlib.expobeta) add("expo54Implementation", libs.brownfieldlib.expo54) add("vanillaImplementation", libs.brownfieldlib.vanilla) diff --git a/apps/AndroidApp/gradle/libs.versions.toml b/apps/AndroidApp/gradle/libs.versions.toml index 973d4ce6..f52bd562 100644 --- a/apps/AndroidApp/gradle/libs.versions.toml +++ b/apps/AndroidApp/gradle/libs.versions.toml @@ -17,6 +17,7 @@ gson = "2.13.2" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } brownfieldlib-expo55 = { module = "com.callstack.rnbrownfield.demo.expoapp55:brownfieldlib", version.ref = "brownfieldlib" } +brownfieldlib-expobeta = { module = "com.callstack.rnbrownfield.demo.expobeta:brownfieldlib", version.ref = "brownfieldlib" } brownfieldlib-expo54 = { module = "com.callstack.rnbrownfield.demo.expoapp54:brownfieldlib", version.ref = "brownfieldlib" } brownfieldlib-vanilla = { module = "com.rnapp:brownfieldlib", version.ref = "brownfieldlib" } junit = { group = "junit", name = "junit", version.ref = "junit" } diff --git a/apps/AndroidApp/package.json b/apps/AndroidApp/package.json index ec42f314..8ee08845 100644 --- a/apps/AndroidApp/package.json +++ b/apps/AndroidApp/package.json @@ -5,6 +5,7 @@ "scripts": { "build:example:android-consumer:expo": "./gradlew assembleExpo55Release", "build:example:android-consumer:expo55": "./gradlew assembleExpo55Release", + "build:example:android-consumer:expobeta": "./gradlew assembleExpobetaRelease", "build:example:android-consumer:expo54": "./gradlew assembleExpo54Release", "build:example:android-consumer:vanilla": "./gradlew assembleVanillaRelease" } diff --git a/apps/AppleApp/Brownfield Apple App.xcodeproj/project.pbxproj b/apps/AppleApp/Brownfield Apple App.xcodeproj/project.pbxproj index bfe6263d..6f3e91d6 100644 --- a/apps/AppleApp/Brownfield Apple App.xcodeproj/project.pbxproj +++ b/apps/AppleApp/Brownfield Apple App.xcodeproj/project.pbxproj @@ -45,6 +45,20 @@ 79B8BE932FB7273600B94C6F /* hermesvm.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 79B1D99C2FB4A61400A5F42B /* hermesvm.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 79B8BE942FB7273600B94C6F /* BrownfieldLib.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 79B1D9992FB4A61400A5F42B /* BrownfieldLib.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 79B8BE952FB7273600B94C6F /* React.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 79B1D99D2FB4A61400A5F42B /* React.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 7A1B2C3D4E5F60718293A101 /* Brownie.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 79B1D99B2FB4A61400A5F42B /* Brownie.xcframework */; }; + 7A1B2C3D4E5F60718293A102 /* BrownfieldNavigation.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 79B1D99A2FB4A61400A5F42B /* BrownfieldNavigation.xcframework */; }; + 7A1B2C3D4E5F60718293A103 /* ReactNativeDependencies.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 79B1D99F2FB4A61400A5F42B /* ReactNativeDependencies.xcframework */; }; + 7A1B2C3D4E5F60718293A104 /* hermesvm.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 79B1D99C2FB4A61400A5F42B /* hermesvm.xcframework */; }; + 7A1B2C3D4E5F60718293A105 /* ReactBrownfield.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 79B1D99E2FB4A61400A5F42B /* ReactBrownfield.xcframework */; }; + 7A1B2C3D4E5F60718293A106 /* BrownfieldLib.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 79B1D9992FB4A61400A5F42B /* BrownfieldLib.xcframework */; }; + 7A1B2C3D4E5F60718293A107 /* React.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 79B1D99D2FB4A61400A5F42B /* React.xcframework */; }; + 7A1B2C3D4E5F60718293A108 /* Brownie.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 79B1D99B2FB4A61400A5F42B /* Brownie.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 7A1B2C3D4E5F60718293A109 /* ReactBrownfield.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 79B1D99E2FB4A61400A5F42B /* ReactBrownfield.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 7A1B2C3D4E5F60718293A10A /* ReactNativeDependencies.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 79B1D99F2FB4A61400A5F42B /* ReactNativeDependencies.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 7A1B2C3D4E5F60718293A10B /* BrownfieldNavigation.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 79B1D99A2FB4A61400A5F42B /* BrownfieldNavigation.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 7A1B2C3D4E5F60718293A10C /* hermesvm.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 79B1D99C2FB4A61400A5F42B /* hermesvm.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 7A1B2C3D4E5F60718293A10D /* BrownfieldLib.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 79B1D9992FB4A61400A5F42B /* BrownfieldLib.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 7A1B2C3D4E5F60718293A10E /* React.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 79B1D99D2FB4A61400A5F42B /* React.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -97,6 +111,23 @@ name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; + 7A1B2C3D4E5F60718293A201 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + 7A1B2C3D4E5F60718293A108 /* Brownie.xcframework in Embed Frameworks */, + 7A1B2C3D4E5F60718293A109 /* ReactBrownfield.xcframework in Embed Frameworks */, + 7A1B2C3D4E5F60718293A10A /* ReactNativeDependencies.xcframework in Embed Frameworks */, + 7A1B2C3D4E5F60718293A10B /* BrownfieldNavigation.xcframework in Embed Frameworks */, + 7A1B2C3D4E5F60718293A10C /* hermesvm.xcframework in Embed Frameworks */, + 7A1B2C3D4E5F60718293A10D /* BrownfieldLib.xcframework in Embed Frameworks */, + 7A1B2C3D4E5F60718293A10E /* React.xcframework in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ @@ -110,11 +141,65 @@ 79B1D99F2FB4A61400A5F42B /* ReactNativeDependencies.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = ReactNativeDependencies.xcframework; path = package/ReactNativeDependencies.xcframework; sourceTree = ""; }; 79B8BE802FB7270E00B94C6F /* Brownfield Apple App (ExpoApp54).app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Brownfield Apple App (ExpoApp54).app"; sourceTree = BUILT_PRODUCTS_DIR; }; 79B8BE9B2FB7273600B94C6F /* Brownfield Apple App (ExpoApp55).app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Brownfield Apple App (ExpoApp55).app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 7A1B2C3D4E5F60718293A202 /* Brownfield Apple App (ExpoAppBeta).app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Brownfield Apple App (ExpoAppBeta).app"; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ +/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ + 79B8BE822FB7270F00B94C6F /* Exceptions for "Brownfield Apple App" folder in "Brownfield Apple App (ExpoApp54)" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Assets.xcassets, + BrownfieldAppleApp.swift, + components/ContentView.swift, + components/GreetingCard.swift, + components/MaterialCard.swift, + components/MessagesView.swift, + components/ReferralsScreen.swift, + components/SettingsScreen.swift, + components/Toast.swift, + ); + target = 79B8BE682FB7270E00B94C6F /* Brownfield Apple App (ExpoApp54) */; + }; + 79B8BE9C2FB7273600B94C6F /* Exceptions for "Brownfield Apple App" folder in "Brownfield Apple App (ExpoApp55)" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Assets.xcassets, + BrownfieldAppleApp.swift, + components/ContentView.swift, + components/GreetingCard.swift, + components/MaterialCard.swift, + components/MessagesView.swift, + components/ReferralsScreen.swift, + components/SettingsScreen.swift, + components/Toast.swift, + ); + target = 79B8BE832FB7273600B94C6F /* Brownfield Apple App (ExpoApp55) */; + }; + 7A1B2C3D4E5F60718293A203 /* Exceptions for "Brownfield Apple App" folder in "Brownfield Apple App (ExpoAppBeta)" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Assets.xcassets, + BrownfieldAppleApp.swift, + components/ContentView.swift, + components/GreetingCard.swift, + components/MaterialCard.swift, + components/MessagesView.swift, + components/ReferralsScreen.swift, + components/SettingsScreen.swift, + components/Toast.swift, + ); + target = 7A1B2C3D4E5F60718293A301 /* Brownfield Apple App (ExpoAppBeta) */; + }; +/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ + /* Begin PBXFileSystemSynchronizedRootGroup section */ 793C76A92EEBF938008A2A34 /* Brownfield Apple App */ = { isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + 79B8BE822FB7270F00B94C6F /* Exceptions for "Brownfield Apple App" folder in "Brownfield Apple App (ExpoApp54)" target */, + 79B8BE9C2FB7273600B94C6F /* Exceptions for "Brownfield Apple App" folder in "Brownfield Apple App (ExpoApp55)" target */, + 7A1B2C3D4E5F60718293A203 /* Exceptions for "Brownfield Apple App" folder in "Brownfield Apple App (ExpoAppBeta)" target */, + ); path = "Brownfield Apple App"; sourceTree = ""; }; @@ -161,6 +246,20 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 7A1B2C3D4E5F60718293A204 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 7A1B2C3D4E5F60718293A101 /* Brownie.xcframework in Frameworks */, + 7A1B2C3D4E5F60718293A102 /* BrownfieldNavigation.xcframework in Frameworks */, + 7A1B2C3D4E5F60718293A103 /* ReactNativeDependencies.xcframework in Frameworks */, + 7A1B2C3D4E5F60718293A104 /* hermesvm.xcframework in Frameworks */, + 7A1B2C3D4E5F60718293A105 /* ReactBrownfield.xcframework in Frameworks */, + 7A1B2C3D4E5F60718293A106 /* BrownfieldLib.xcframework in Frameworks */, + 7A1B2C3D4E5F60718293A107 /* React.xcframework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -193,6 +292,7 @@ 793C76A72EEBF938008A2A34 /* Brownfield Apple App (RNApp).app */, 79B8BE802FB7270E00B94C6F /* Brownfield Apple App (ExpoApp54).app */, 79B8BE9B2FB7273600B94C6F /* Brownfield Apple App (ExpoApp55).app */, + 7A1B2C3D4E5F60718293A202 /* Brownfield Apple App (ExpoAppBeta).app */, ); name = Products; sourceTree = ""; @@ -269,6 +369,29 @@ productReference = 79B8BE9B2FB7273600B94C6F /* Brownfield Apple App (ExpoApp55).app */; productType = "com.apple.product-type.application"; }; + 7A1B2C3D4E5F60718293A301 /* Brownfield Apple App (ExpoAppBeta) */ = { + isa = PBXNativeTarget; + buildConfigurationList = 7A1B2C3D4E5F60718293A405 /* Build configuration list for PBXNativeTarget "Brownfield Apple App (ExpoAppBeta)" */; + buildPhases = ( + 7A1B2C3D4E5F60718293A302 /* Sources */, + 7A1B2C3D4E5F60718293A204 /* Frameworks */, + 7A1B2C3D4E5F60718293A303 /* Resources */, + 7A1B2C3D4E5F60718293A201 /* Embed Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + 793C76A92EEBF938008A2A34 /* Brownfield Apple App */, + ); + name = "Brownfield Apple App (ExpoAppBeta)"; + packageProductDependencies = ( + ); + productName = "Brownfield Apple App"; + productReference = 7A1B2C3D4E5F60718293A202 /* Brownfield Apple App (ExpoAppBeta).app */; + productType = "com.apple.product-type.application"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -297,11 +420,13 @@ productRefGroup = 793C76A82EEBF938008A2A34 /* Products */; projectDirPath = ""; projectRoot = ""; - targets = ( - 793C76A62EEBF938008A2A34 /* Brownfield Apple App (RNApp) */, - 79B8BE682FB7270E00B94C6F /* Brownfield Apple App (ExpoApp54) */, - 79B8BE832FB7273600B94C6F /* Brownfield Apple App (ExpoApp55) */, - ); + targets = ( + 793C76A62EEBF938008A2A34 /* Brownfield Apple App (RNApp) */, + 79B8BE682FB7270E00B94C6F /* Brownfield Apple App (ExpoApp54) */, + 79B8BE832FB7273600B94C6F /* Brownfield Apple App (ExpoApp55) */, + 7A1B2C3D4E5F60718293A301 /* Brownfield Apple App (ExpoAppBeta) */, + ); + }; /* End PBXProject section */ @@ -327,6 +452,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 7A1B2C3D4E5F60718293A303 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -351,6 +483,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 7A1B2C3D4E5F60718293A302 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ /* Begin XCBuildConfiguration section */ @@ -1154,6 +1293,194 @@ }; name = "Release Vanilla"; }; + 7A1B2C3D4E5F60718293A501 /* Debug Expo */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; + ENABLE_APP_SANDBOX = YES; + ENABLE_PREVIEWS = YES; + ENABLE_USER_SELECTED_FILES = readonly; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "Brownfield-Apple-App-Info.plist"; + "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault; + "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UIViewControllerBasedStatusBarAppearance = "$(USE_EXPO_HOST_STATUS_BAR_APPEARANCE)"; + IPHONEOS_DEPLOYMENT_TARGET = 15.1; + LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; + "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 14.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.callstack.brownfield.ios.example.Brownfield-iOS-App"; + PRODUCT_NAME = "$(TARGET_NAME)"; + REGISTER_APP_GROUPS = YES; + SDKROOT = auto; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) USE_EXPO_HOST"; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2,7"; + USE_EXPO_HOST_STATUS_BAR_APPEARANCE = NO; + XROS_DEPLOYMENT_TARGET = 2.0; + }; + name = "Debug Expo"; + }; + 7A1B2C3D4E5F60718293A502 /* Release Expo */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; + ENABLE_APP_SANDBOX = YES; + ENABLE_PREVIEWS = YES; + ENABLE_USER_SELECTED_FILES = readonly; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "Brownfield-Apple-App-Info.plist"; + "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault; + "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UIViewControllerBasedStatusBarAppearance = "$(USE_EXPO_HOST_STATUS_BAR_APPEARANCE)"; + IPHONEOS_DEPLOYMENT_TARGET = 15.1; + LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; + "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 14.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.callstack.brownfield.ios.example.Brownfield-iOS-App"; + PRODUCT_NAME = "$(TARGET_NAME)"; + REGISTER_APP_GROUPS = YES; + SDKROOT = auto; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) USE_EXPO_HOST"; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2,7"; + USE_EXPO_HOST_STATUS_BAR_APPEARANCE = NO; + XROS_DEPLOYMENT_TARGET = 2.0; + }; + name = "Release Expo"; + }; + 7A1B2C3D4E5F60718293A503 /* Debug Vanilla */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; + ENABLE_APP_SANDBOX = YES; + ENABLE_PREVIEWS = YES; + ENABLE_USER_SELECTED_FILES = readonly; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "Brownfield-Apple-App-Info.plist"; + "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault; + "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UIViewControllerBasedStatusBarAppearance = "$(USE_EXPO_HOST_STATUS_BAR_APPEARANCE)"; + IPHONEOS_DEPLOYMENT_TARGET = 15.1; + LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; + "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 14.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.callstack.brownfield.ios.example.Brownfield-iOS-App"; + PRODUCT_NAME = "$(TARGET_NAME)"; + REGISTER_APP_GROUPS = YES; + SDKROOT = auto; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited)"; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2,7"; + USE_EXPO_HOST_STATUS_BAR_APPEARANCE = YES; + XROS_DEPLOYMENT_TARGET = 2.0; + }; + name = "Debug Vanilla"; + }; + 7A1B2C3D4E5F60718293A504 /* Release Vanilla */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; + ENABLE_APP_SANDBOX = YES; + ENABLE_PREVIEWS = YES; + ENABLE_USER_SELECTED_FILES = readonly; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "Brownfield-Apple-App-Info.plist"; + "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault; + "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UIViewControllerBasedStatusBarAppearance = "$(USE_EXPO_HOST_STATUS_BAR_APPEARANCE)"; + IPHONEOS_DEPLOYMENT_TARGET = 15.1; + LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; + "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 14.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.callstack.brownfield.ios.example.Brownfield-iOS-App"; + PRODUCT_NAME = "$(TARGET_NAME)"; + REGISTER_APP_GROUPS = YES; + SDKROOT = auto; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited)"; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2,7"; + USE_EXPO_HOST_STATUS_BAR_APPEARANCE = YES; + XROS_DEPLOYMENT_TARGET = 2.0; + }; + name = "Release Vanilla"; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -1201,6 +1528,17 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = "Release Expo"; }; + 7A1B2C3D4E5F60718293A405 /* Build configuration list for PBXNativeTarget "Brownfield Apple App (ExpoAppBeta)" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7A1B2C3D4E5F60718293A501 /* Debug Expo */, + 7A1B2C3D4E5F60718293A502 /* Release Expo */, + 7A1B2C3D4E5F60718293A503 /* Debug Vanilla */, + 7A1B2C3D4E5F60718293A504 /* Release Vanilla */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = "Release Expo"; + }; /* End XCConfigurationList section */ }; rootObject = 793C769F2EEBF938008A2A34 /* Project object */; diff --git a/apps/AppleApp/Brownfield Apple App.xcodeproj/xcshareddata/xcschemes/Brownfield Apple App Expo Beta.xcscheme b/apps/AppleApp/Brownfield Apple App.xcodeproj/xcshareddata/xcschemes/Brownfield Apple App Expo Beta.xcscheme new file mode 100644 index 00000000..6ffc555f --- /dev/null +++ b/apps/AppleApp/Brownfield Apple App.xcodeproj/xcshareddata/xcschemes/Brownfield Apple App Expo Beta.xcscheme @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + diff --git a/apps/AppleApp/package.json b/apps/AppleApp/package.json index 832bdbee..e1d00671 100644 --- a/apps/AppleApp/package.json +++ b/apps/AppleApp/package.json @@ -7,6 +7,7 @@ "build:example:ios-consumer:expo": "yarn build:example:ios-consumer:expo55", "build:example:ios-consumer:expo54": "node prepareXCFrameworks.js --appName ExpoApp54 && yarn internal::build::common -scheme \"Brownfield Apple App Expo 54\" -configuration Release", "build:example:ios-consumer:expo55": "node prepareXCFrameworks.js --appName ExpoApp55 && yarn internal::build::common -scheme \"Brownfield Apple App Expo 55\" -configuration Release", + "build:example:ios-consumer:expobeta": "node prepareXCFrameworks.js --appName ExpoAppBeta && yarn internal::build::common -scheme \"Brownfield Apple App Expo Beta\" -configuration Release", "build:example:ios-consumer:vanilla": "node prepareXCFrameworks.js --appName RNApp && yarn internal::build::common -scheme \"Brownfield Apple App Vanilla\" -configuration \"Release Vanilla\"", "internal::build::common": "xcodebuild -project \"Brownfield Apple App.xcodeproj\" -sdk iphonesimulator build CODE_SIGNING_ALLOWED=NO -derivedDataPath ./build" }, diff --git a/apps/README.md b/apps/README.md index 27c77191..6efc8c09 100644 --- a/apps/README.md +++ b/apps/README.md @@ -5,6 +5,7 @@ This directory contains demo projects showcasing the usage of the `react-native- - `RNApp` - the React Native application that is packaged to AAR and XCFramework archives and integrated into native projects - `ExpoApp54` - the Expo application that is packaged analogously to the above using React Native Brownfield Expo config plugin; this app uses Expo SDK v54, which is an important test case since pre-55 versions require additional configuration steps - `ExpoApp55` - another Expo application similar to `ExpoApp54`, but using Expo SDK v55 +- `ExpoAppBeta` - a temporary Expo app generated in CI to test new Expo beta releases before stable support is added - `AndroidApp` - the native Android application that integrates the RNApp AAR package (a "consumer" of the RNApp library); it comes in two flavors: - `expo` - which uses the artifact produced from `ExpoApp` - `vanilla` - which uses the artifact produced from `RNApp` @@ -12,8 +13,9 @@ This directory contains demo projects showcasing the usage of the `react-native- - `Brownfield Apple App (RNApp)` — vanilla; uses the artifact from `RNApp` (scheme **Brownfield Apple App Vanilla**, configuration `Release Vanilla`) - `Brownfield Apple App (ExpoApp54)` — uses the artifact from `ExpoApp54` (scheme **Brownfield Apple App Expo 54**, configuration `Release`) - `Brownfield Apple App (ExpoApp55)` — uses the artifact from `ExpoApp55` (scheme **Brownfield Apple App Expo 55**, configuration `Release`) + - `Brownfield Apple App (ExpoAppBeta)` — uses the artifact from `ExpoAppBeta` (scheme **Brownfield Apple App Expo Beta**, configuration `Release`) - From `apps/AppleApp`, run `yarn build:example:ios-consumer:vanilla`, `yarn build:example:ios-consumer:expo54`, or `yarn build:example:ios-consumer:expo55` to copy XCFrameworks into `package/` and build the matching target. + From `apps/AppleApp`, run `yarn build:example:ios-consumer:vanilla`, `yarn build:example:ios-consumer:expo54`, `yarn build:example:ios-consumer:expo55`, or `yarn build:example:ios-consumer:expobeta` to copy XCFrameworks into `package/` and build the matching target. ## Additional notes diff --git a/scripts/__tests__/check-expo-beta.test.ts b/scripts/__tests__/check-expo-beta.test.ts new file mode 100644 index 00000000..3e42a0db --- /dev/null +++ b/scripts/__tests__/check-expo-beta.test.ts @@ -0,0 +1,43 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { + updateExpoVersion, + type ExpoPackageJson, +} from '../check-expo-beta.ts'; + +test('updates ExpoApp package.json when a new beta version is provided', () => { + const packageJson: ExpoPackageJson = { + dependencies: { + expo: '~55.0.23', + }, + }; + + const updated = updateExpoVersion(packageJson, '56.0.0-beta.1'); + + assert.equal(updated, true); + assert.equal(packageJson.dependencies?.expo, '56.0.0-beta.1'); +}); + +test('does not rewrite ExpoApp package.json when the beta version is unchanged', () => { + const packageJson: ExpoPackageJson = { + dependencies: { + expo: '56.0.0-beta.1', + }, + }; + + const updated = updateExpoVersion(packageJson, '56.0.0-beta.1'); + + assert.equal(updated, false); + assert.equal(packageJson.dependencies?.expo, '56.0.0-beta.1'); +}); + +test('fails loudly when expo dependency is missing', () => { + const packageJson: ExpoPackageJson = { + dependencies: {}, + }; + + assert.throws(() => updateExpoVersion(packageJson, '56.0.0-beta.1'), { + message: /Could not locate dependencies\.expo in ExpoAppBeta\/package\.json/, + }); +}); diff --git a/scripts/check-expo-beta.ts b/scripts/check-expo-beta.ts new file mode 100644 index 00000000..cb6ed319 --- /dev/null +++ b/scripts/check-expo-beta.ts @@ -0,0 +1,253 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { cpSync, mkdirSync, rmSync, existsSync, readFileSync, writeFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; + +interface CliOptions { + expoVersion?: string; + testedVersion?: string; + apply?: boolean; + githubOutput?: string; + githubStepSummary?: string; +} + +export interface ExpoPackageJson { + name?: string; + dependencies?: Record; + devDependencies?: Record; + brownie?: { + kotlin?: string; + kotlinPackageName?: string; + }; + scripts?: Record; +} + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const REPO_ROOT = path.resolve(__dirname, '..'); +const EXPO_TEMPLATE_APP_DIR = path.join(REPO_ROOT, 'apps', 'ExpoApp55'); +const EXPO_BETA_APP_DIR = path.join(REPO_ROOT, 'apps', 'ExpoAppBeta'); +const EXPO_BETA_PACKAGE_JSON_PATH = path.join(EXPO_BETA_APP_DIR, 'package.json'); +const EXPO_BETA_APP_JSON_PATH = path.join(EXPO_BETA_APP_DIR, 'app.json'); +const EXPO_NPM_REGISTRY_URL = 'https://registry.npmjs.org/expo'; + +function parseArgs(argv: string[]): CliOptions { + const options: CliOptions = {}; + + for (let index = 0; index < argv.length; index += 1) { + const arg = argv[index]; + + if (arg === '--apply') { + options.apply = true; + continue; + } + + if (arg === '--expo-version') { + options.expoVersion = argv[index + 1]; + index += 1; + continue; + } + + if (arg === '--tested-version') { + options.testedVersion = argv[index + 1]; + index += 1; + continue; + } + + if (arg === '--github-output') { + options.githubOutput = argv[index + 1]; + index += 1; + continue; + } + + if (arg === '--github-step-summary') { + options.githubStepSummary = argv[index + 1]; + index += 1; + } + } + + return options; +} + +export async function fetchLatestExpoBetaVersion(): Promise { + const response = await fetch(EXPO_NPM_REGISTRY_URL); + + if (!response.ok) { + throw new Error(`Failed to fetch Expo metadata: ${response.status}`); + } + + const data = (await response.json()) as { + 'dist-tags'?: Record; + versions?: Record; + }; + + const betaTag = data['dist-tags']?.beta; + if (betaTag) { + return betaTag; + } + + const versions = Object.keys(data.versions ?? {}).filter((version) => + version.includes('beta') + ); + + return versions.at(-1) ?? null; +} + +function updateFileContents(filePath: string, updater: (contents: string) => string): void { + const contents = readFileSync(filePath, 'utf8'); + writeFileSync(filePath, updater(contents), 'utf8'); +} + +function generateExpoBetaApp(): void { + rmSync(EXPO_BETA_APP_DIR, { recursive: true, force: true }); + mkdirSync(path.dirname(EXPO_BETA_APP_DIR), { recursive: true }); + cpSync(EXPO_TEMPLATE_APP_DIR, EXPO_BETA_APP_DIR, { recursive: true }); + + updateFileContents(EXPO_BETA_PACKAGE_JSON_PATH, (contents) => + contents + .replaceAll('@callstack/brownfield-example-expo-app-55', '@callstack/brownfield-example-expo-app-beta') + .replaceAll('ExpoApp55', 'ExpoAppBeta') + .replaceAll('expoapp55', 'expoappbeta') + .replaceAll('expoapp56', 'expoappbeta') + .replaceAll('expoappbeta55', 'expoappbeta') + ); + + updateFileContents(EXPO_BETA_APP_JSON_PATH, (contents) => + contents + .replaceAll('ExpoApp55', 'ExpoAppBeta') + .replaceAll('expoapp55', 'expoappbeta') + .replaceAll('com.callstack.rnbrownfield.demo.expoapp55', 'com.callstack.rnbrownfield.demo.expobeta') + ); + + const testPath = path.join(EXPO_BETA_APP_DIR, '__tests__', 'brownfield.example.test.tsx'); + if (existsSync(testPath)) { + updateFileContents(testPath, (contents) => contents.replaceAll('ExpoApp55', 'ExpoAppBeta')); + } + + const homeScreenPath = path.join(EXPO_BETA_APP_DIR, 'src', 'app', 'index.tsx'); + if (existsSync(homeScreenPath)) { + updateFileContents(homeScreenPath, (contents) => contents.replaceAll('Expo\u00a055', 'Expo\u00a0Beta').replaceAll('Expo 55', 'Expo Beta')); + } +} + +function readExpoBetaPackageJson(): ExpoPackageJson { + return JSON.parse(readFileSync(EXPO_BETA_PACKAGE_JSON_PATH, 'utf8')) as ExpoPackageJson; +} + +function writeExpoBetaPackageJson(packageJson: ExpoPackageJson): void { + writeFileSync(EXPO_BETA_PACKAGE_JSON_PATH, `${JSON.stringify(packageJson, null, 2)}\n`, 'utf8'); +} + +export function updateExpoVersion(packageJson: ExpoPackageJson, expoVersion: string): boolean { + if (!packageJson.dependencies?.expo) { + throw new Error('Could not locate dependencies.expo in ExpoAppBeta/package.json'); + } + + if (packageJson.dependencies.expo === expoVersion) { + return false; + } + + packageJson.dependencies.expo = expoVersion; + return true; +} + +function appendKeyValueFile(filePath: string | undefined, entries: Record): void { + if (!filePath) { + return; + } + + const lines = Object.entries(entries).map(([key, value]) => `${key}=${value}`); + fs.appendFileSync(filePath, `${lines.join('\n')}\n`, 'utf8'); +} + +function appendSummary( + filePath: string | undefined, + { + latestVersion, + testedVersion, + shouldTest, + applied, + generated, + }: { + latestVersion: string | null; + testedVersion?: string; + shouldTest: boolean; + applied: boolean; + generated: boolean; + } +): void { + if (!filePath) { + return; + } + + const lines = [ + '## Expo beta check', + '', + `- Latest Expo beta: ${latestVersion ?? 'not found'}`, + `- Last tested Expo beta: ${testedVersion ?? 'none'}`, + `- Should run road tests: ${shouldTest ? 'yes' : 'no'}`, + `- ExpoAppBeta generated on-the-fly: ${generated ? 'yes' : 'no'}`, + `- ExpoAppBeta package.json updated: ${applied ? 'yes' : 'no'}`, + '', + ]; + + fs.appendFileSync(filePath, `${lines.join('\n')}\n`, 'utf8'); +} + +async function main(): Promise { + const options = parseArgs(process.argv.slice(2)); + const latestVersion = options.expoVersion ?? (await fetchLatestExpoBetaVersion()); + + if (!latestVersion) { + appendKeyValueFile(options.githubOutput, { + latest_version: '', + should_test: 'false', + applied: 'false', + generated: 'false', + }); + appendSummary(options.githubStepSummary, { + latestVersion: null, + testedVersion: options.testedVersion, + shouldTest: false, + applied: false, + generated: false, + }); + console.log('No Expo beta release found'); + return; + } + + const shouldTest = latestVersion !== options.testedVersion; + let applied = false; + let generated = false; + + if (shouldTest && options.apply) { + generateExpoBetaApp(); + generated = true; + const packageJson = readExpoBetaPackageJson(); + applied = updateExpoVersion(packageJson, latestVersion); + writeExpoBetaPackageJson(packageJson); + } + + appendKeyValueFile(options.githubOutput, { + latest_version: latestVersion, + should_test: shouldTest ? 'true' : 'false', + applied: applied ? 'true' : 'false', + generated: generated ? 'true' : 'false', + }); + + appendSummary(options.githubStepSummary, { + latestVersion, + testedVersion: options.testedVersion, + shouldTest, + applied, + generated, + }); + + console.log(`Latest Expo beta: ${latestVersion}`); + console.log(`Last tested Expo beta: ${options.testedVersion ?? 'none'}`); + console.log(`Should test: ${shouldTest}`); + console.log(`Generated ExpoAppBeta: ${generated}`); + console.log(`Applied: ${applied}`); +} + +main();