diff --git a/.github/actions/androidapp-road-test/action.yml b/.github/actions/androidapp-road-test/action.yml index 7b4a0e43..c0a6298f 100644 --- a/.github/actions/androidapp-road-test/action.yml +++ b/.github/actions/androidapp-road-test/action.yml @@ -19,6 +19,11 @@ inputs: required: false default: 'true' + skip-setup: + description: 'Skip yarn install/build when the caller already prepared the monorepo (e.g. Expo preview workflow)' + required: false + default: 'false' + runs: using: composite steps: @@ -29,6 +34,7 @@ runs: shell: bash - name: Setup + if: inputs.skip-setup != 'true' uses: ./.github/actions/setup with: restore-turbo-cache: ${{ inputs.restore-turbo-cache }} diff --git a/.github/actions/appleapp-road-test/action.yml b/.github/actions/appleapp-road-test/action.yml index 36e0b5c1..3c8ca06a 100644 --- a/.github/actions/appleapp-road-test/action.yml +++ b/.github/actions/appleapp-road-test/action.yml @@ -25,6 +25,11 @@ inputs: required: false default: 'true' + skip-setup: + description: 'Skip yarn install/build when the caller already prepared the monorepo (e.g. Expo preview workflow)' + required: false + default: 'false' + runs: using: composite steps: @@ -33,6 +38,7 @@ runs: shell: bash - name: Setup + if: inputs.skip-setup != 'true' uses: ./.github/actions/setup with: restore-turbo-cache: ${{ inputs.restore-turbo-cache }} diff --git a/.github/workflows/expo-beta-road-test.yml b/.github/workflows/expo-preview-road-test.yml similarity index 62% rename from .github/workflows/expo-beta-road-test.yml rename to .github/workflows/expo-preview-road-test.yml index 0b28d818..b718ac6f 100644 --- a/.github/workflows/expo-beta-road-test.yml +++ b/.github/workflows/expo-preview-road-test.yml @@ -1,5 +1,5 @@ -name: Expo beta road test -run-name: Expo beta road test +name: Expo preview road test +run-name: Expo preview road test on: schedule: @@ -13,7 +13,7 @@ permissions: jobs: prepare: - name: Detect Expo beta to test + name: Detect Expo preview to test runs-on: ubuntu-latest outputs: latest_version: ${{ steps.finalize.outputs.latest_version }} @@ -22,11 +22,11 @@ jobs: - name: Checkout uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 - - name: Detect latest Expo beta + - name: Detect latest Expo preview id: detect - run: node --experimental-strip-types --no-warnings ./scripts/check-expo-beta.ts --github-output "$GITHUB_OUTPUT" --github-step-summary "$GITHUB_STEP_SUMMARY" + run: node --experimental-strip-types --no-warnings ./scripts/check-expo-preview.ts --github-output "$GITHUB_OUTPUT" --github-step-summary "$GITHUB_STEP_SUMMARY" - - name: Check whether this Expo beta was already tested + - name: Check whether this Expo preview was already tested id: tested if: steps.detect.outputs.latest_version != '' env: @@ -34,7 +34,7 @@ jobs: 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' \ + --jq '.artifacts[] | select(.expired == false) | select(.name == "expo-preview-tested-${{ steps.detect.outputs.latest_version }}") | .id' \ | head -n 1) if [ -n "$artifact_id" ]; then @@ -61,7 +61,7 @@ jobs: } >> "$GITHUB_STEP_SUMMARY" android: - name: Android road test (Expo beta) + name: Android road test (Expo preview) runs-on: ubuntu-latest needs: prepare if: needs.prepare.outputs.should_test == 'true' @@ -69,8 +69,8 @@ jobs: - 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 "${{ needs.prepare.outputs.latest_version }}" --apply --github-step-summary "$GITHUB_STEP_SUMMARY" + - name: Generate and apply Expo preview to ExpoAppPreview + run: node --experimental-strip-types --no-warnings ./scripts/check-expo-preview.ts --expo-version "${{ needs.prepare.outputs.latest_version }}" --apply --github-step-summary "$GITHUB_STEP_SUMMARY" - name: Setup monorepo uses: ./.github/actions/setup @@ -79,26 +79,28 @@ jobs: env: YARN_ENABLE_IMMUTABLE_INSTALLS: 'false' - - name: Install Expo beta dependencies + - name: Install Expo preview dependencies env: DETOX_DISABLE_POSTINSTALL: '1' + YARN_ENABLE_IMMUTABLE_INSTALLS: 'false' run: | - cd apps/ExpoAppBeta - npx expo install --fix + yarn workspace @callstack/brownfield-example-expo-app-preview exec expo install --fix + yarn install - - name: Rebuild packages after Expo beta dependency updates + - name: Rebuild packages after Expo preview dependency updates run: yarn build - - name: Run ExpoAppBeta -> AndroidApp road test + - name: Run ExpoAppPreview -> 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 + flavor: expopreview + rn-project-path: apps/ExpoAppPreview + rn-project-maven-path: com/callstack/rnbrownfield/demo/expoapppreview/brownfieldlib restore-turbo-cache: 'false' + skip-setup: 'true' ios: - name: iOS road test (Expo beta) + name: iOS road test (Expo preview) runs-on: macos-26 needs: prepare if: needs.prepare.outputs.should_test == 'true' @@ -106,8 +108,8 @@ jobs: - 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 "${{ needs.prepare.outputs.latest_version }}" --apply --github-step-summary "$GITHUB_STEP_SUMMARY" + - name: Generate and apply Expo preview to ExpoAppPreview + run: node --experimental-strip-types --no-warnings ./scripts/check-expo-preview.ts --expo-version "${{ needs.prepare.outputs.latest_version }}" --apply --github-step-summary "$GITHUB_STEP_SUMMARY" - name: Setup monorepo uses: ./.github/actions/setup @@ -116,25 +118,27 @@ jobs: env: YARN_ENABLE_IMMUTABLE_INSTALLS: 'false' - - name: Install Expo beta dependencies + - name: Install Expo preview dependencies env: DETOX_DISABLE_POSTINSTALL: '1' + YARN_ENABLE_IMMUTABLE_INSTALLS: 'false' run: | - cd apps/ExpoAppBeta - npx expo install --fix + yarn workspace @callstack/brownfield-example-expo-app-preview exec expo install --fix + yarn install - - name: Rebuild packages after Expo beta dependency updates + - name: Rebuild packages after Expo preview dependency updates run: yarn build - - name: Run ExpoAppBeta -> AppleApp road test + - name: Run ExpoAppPreview -> AppleApp road test uses: ./.github/actions/appleapp-road-test with: - variant: expobeta - rn-project-path: apps/ExpoAppBeta + variant: expopreview + rn-project-path: apps/ExpoAppPreview restore-turbo-cache: 'false' + skip-setup: 'true' record-success: - name: Record tested Expo beta + name: Record tested Expo preview runs-on: ubuntu-latest needs: [prepare, android, ios] if: | @@ -142,20 +146,20 @@ jobs: needs.android.result == 'success' && needs.ios.result == 'success' steps: - - name: Create Expo beta success marker + - name: Create Expo preview success marker run: | - mkdir -p expo-beta-test-result - printf '%s\n' '${{ needs.prepare.outputs.latest_version }}' > expo-beta-test-result/version.txt + mkdir -p expo-preview-test-result + printf '%s\n' '${{ needs.prepare.outputs.latest_version }}' > expo-preview-test-result/version.txt - - name: Upload Expo beta success marker + - name: Upload Expo preview success marker uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: expo-beta-tested-${{ needs.prepare.outputs.latest_version }} - path: expo-beta-test-result/version.txt + name: expo-preview-tested-${{ needs.prepare.outputs.latest_version }} + path: expo-preview-test-result/version.txt retention-days: 90 create-issue-on-failure: - name: Create issue on Expo beta failure + name: Create issue on Expo preview failure runs-on: ubuntu-latest needs: [prepare, android, ios] if: | @@ -166,24 +170,24 @@ jobs: env: GH_TOKEN: ${{ github.token }} run: | - issue_title="Expo beta road test failed for ${{ needs.prepare.outputs.latest_version }}" + issue_title="Expo preview 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 }}' + gh issue comment "$existing_issue_number" --body $'A new failure was detected.\n\n- Expo preview 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 }}' \ + --body $'The Expo preview road test failed.\n\n- Expo preview 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 + name: Summarize Expo preview road test runs-on: ubuntu-latest needs: [prepare, android, ios, record-success, create-issue-on-failure] if: always() @@ -191,14 +195,14 @@ jobs: - name: Write workflow summary run: | { - echo "## Expo beta road test results" + echo "## Expo preview road test results" echo - echo "- Expo beta version: ${{ needs.prepare.outputs.latest_version || 'not found' }}" + echo "- Expo preview 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." + echo "No new untested Expo preview 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' }}" diff --git a/apps/.gitignore b/apps/.gitignore new file mode 100644 index 00000000..853d1748 --- /dev/null +++ b/apps/.gitignore @@ -0,0 +1 @@ +ExpoAppPreview diff --git a/apps/AndroidApp/app/build.gradle.kts b/apps/AndroidApp/app/build.gradle.kts index fc835136..d010cb01 100644 --- a/apps/AndroidApp/app/build.gradle.kts +++ b/apps/AndroidApp/app/build.gradle.kts @@ -39,7 +39,7 @@ android { create("expo56") { dimension = "app" } - create("expobeta") { + create("expopreview") { dimension = "app" } create("vanilla") { @@ -81,7 +81,7 @@ dependencies { add("expo57Implementation", libs.brownfieldlib.expo57) add("expo55Implementation", libs.brownfieldlib.expo55) add("expo56Implementation", libs.brownfieldlib.expo56) - add("expobetaImplementation", libs.brownfieldlib.expobeta) + add("expopreviewImplementation", libs.brownfieldlib.expopreview) add("vanillaImplementation", libs.brownfieldlib.vanilla) implementation(libs.androidx.fragment.compose) diff --git a/apps/AndroidApp/app/src/expopreview/java/com/callstack/brownfield/android/expo/BrownfieldStore.kt b/apps/AndroidApp/app/src/expopreview/java/com/callstack/brownfield/android/expo/BrownfieldStore.kt new file mode 100644 index 00000000..d3f31576 --- /dev/null +++ b/apps/AndroidApp/app/src/expopreview/java/com/callstack/brownfield/android/expo/BrownfieldStore.kt @@ -0,0 +1,4 @@ +package com.callstack.brownfield.android.example + +typealias BrownfieldStore = com.callstack.rnbrownfield.demo.expoapppreview.BrownfieldStore +typealias User = com.callstack.rnbrownfield.demo.expoapppreview.User diff --git a/apps/AndroidApp/app/src/expopreview/java/com/callstack/brownfield/android/expo/ReactNativeConstants.kt b/apps/AndroidApp/app/src/expopreview/java/com/callstack/brownfield/android/expo/ReactNativeConstants.kt new file mode 100644 index 00000000..61214dd5 --- /dev/null +++ b/apps/AndroidApp/app/src/expopreview/java/com/callstack/brownfield/android/expo/ReactNativeConstants.kt @@ -0,0 +1,6 @@ +package com.callstack.brownfield.android.example + +object ReactNativeConstants { + const val MAIN_MODULE_NAME = "main" + const val APP_NAME = "Android (Expo Preview)" +} diff --git a/apps/AndroidApp/app/src/expopreview/java/com/callstack/brownfield/android/expo/ReactNativeHostManager.kt b/apps/AndroidApp/app/src/expopreview/java/com/callstack/brownfield/android/expo/ReactNativeHostManager.kt new file mode 100644 index 00000000..d99af6c6 --- /dev/null +++ b/apps/AndroidApp/app/src/expopreview/java/com/callstack/brownfield/android/expo/ReactNativeHostManager.kt @@ -0,0 +1,3 @@ +package com.callstack.brownfield.android.example + +typealias ReactNativeHostManager = com.callstack.rnbrownfield.demo.expoapppreview.ReactNativeHostManager diff --git a/apps/AndroidApp/gradle/libs.versions.toml b/apps/AndroidApp/gradle/libs.versions.toml index 4766d46f..a3796e68 100644 --- a/apps/AndroidApp/gradle/libs.versions.toml +++ b/apps/AndroidApp/gradle/libs.versions.toml @@ -19,7 +19,7 @@ androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = brownfieldlib-expo57 = { module = "com.callstack.rnbrownfield.demo.expoapp57:brownfieldlib", version.ref = "brownfieldlib" } brownfieldlib-expo56 = { module = "com.callstack.rnbrownfield.demo.expoapp56:brownfieldlib", version.ref = "brownfieldlib" } 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-expopreview = { module = "com.callstack.rnbrownfield.demo.expoapppreview:brownfieldlib", version.ref = "brownfieldlib" } brownfieldlib-vanilla = { module = "com.rnapp:brownfieldlib", version.ref = "brownfieldlib" } junit = { group = "junit", name = "junit", version.ref = "junit" } androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } diff --git a/apps/AndroidApp/package.json b/apps/AndroidApp/package.json index 960fe394..64dc0d4d 100644 --- a/apps/AndroidApp/package.json +++ b/apps/AndroidApp/package.json @@ -7,7 +7,7 @@ "build:example:android-consumer:expo57": "./gradlew assembleExpo57Release", "build:example:android-consumer:expo56": "./gradlew assembleExpo56Release", "build:example:android-consumer:expo55": "./gradlew assembleExpo55Release", - "build:example:android-consumer:expobeta": "./gradlew assembleExpobetaRelease", + "build:example:android-consumer:expopreview": "./gradlew assembleExpopreviewRelease", "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 502e3e25..03014749 100644 --- a/apps/AppleApp/Brownfield Apple App.xcodeproj/project.pbxproj +++ b/apps/AppleApp/Brownfield Apple App.xcodeproj/project.pbxproj @@ -121,6 +121,28 @@ 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, ); }; }; + 7A1B2C3D4E5F60718293A110 /* ExpoFont.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 61591FDD2FF3D71E00BEFE92 /* ExpoFont.xcframework */; }; + 7A1B2C3D4E5F60718293A111 /* SDWebImageSVGCoder.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 618F4DBC2FF7C09F00A719EB /* SDWebImageSVGCoder.xcframework */; }; + 7A1B2C3D4E5F60718293A112 /* SDWebImage.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 618F4DBD2FF7C09F00A719EB /* SDWebImage.xcframework */; }; + 7A1B2C3D4E5F60718293A113 /* ExpoModulesCore.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 61591FE32FF3DC2100BEFE92 /* ExpoModulesCore.xcframework */; }; + 7A1B2C3D4E5F60718293A114 /* SDWebImageAVIFCoder.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 618F4DBF2FF7C09F00A719EB /* SDWebImageAVIFCoder.xcframework */; }; + 7A1B2C3D4E5F60718293A115 /* ExpoModulesWorklets.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 61591FE92FF3DEA600BEFE92 /* ExpoModulesWorklets.xcframework */; }; + 7A1B2C3D4E5F60718293A116 /* ExpoModulesJSI.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 61591FDA2FF399EC00BEFE92 /* ExpoModulesJSI.xcframework */; }; + 7A1B2C3D4E5F60718293A117 /* libavif.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 618F4DBB2FF7C09F00A719EB /* libavif.xcframework */; }; + 7A1B2C3D4E5F60718293A118 /* SDWebImageWebPCoder.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 618F4DBE2FF7C09F00A719EB /* SDWebImageWebPCoder.xcframework */; }; + 7A1B2C3D4E5F60718293A119 /* ExpoImage.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 61591FE62FF3DD6700BEFE92 /* ExpoImage.xcframework */; }; + 7A1B2C3D4E5F60718293A11A /* ExpoFileSystem.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 61591FDE2FF3D71E00BEFE92 /* ExpoFileSystem.xcframework */; }; + 7A1B2C3D4E5F60718293A120 /* ExpoFont.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 61591FDD2FF3D71E00BEFE92 /* ExpoFont.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 7A1B2C3D4E5F60718293A121 /* SDWebImageSVGCoder.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 618F4DBC2FF7C09F00A719EB /* SDWebImageSVGCoder.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 7A1B2C3D4E5F60718293A122 /* SDWebImage.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 618F4DBD2FF7C09F00A719EB /* SDWebImage.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 7A1B2C3D4E5F60718293A123 /* ExpoModulesCore.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 61591FE32FF3DC2100BEFE92 /* ExpoModulesCore.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 7A1B2C3D4E5F60718293A124 /* SDWebImageAVIFCoder.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 618F4DBF2FF7C09F00A719EB /* SDWebImageAVIFCoder.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 7A1B2C3D4E5F60718293A125 /* ExpoModulesWorklets.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 61591FE92FF3DEA600BEFE92 /* ExpoModulesWorklets.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 7A1B2C3D4E5F60718293A126 /* ExpoModulesJSI.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 61591FDA2FF399EC00BEFE92 /* ExpoModulesJSI.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 7A1B2C3D4E5F60718293A127 /* libavif.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 618F4DBB2FF7C09F00A719EB /* libavif.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 7A1B2C3D4E5F60718293A128 /* SDWebImageWebPCoder.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 618F4DBE2FF7C09F00A719EB /* SDWebImageWebPCoder.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 7A1B2C3D4E5F60718293A129 /* ExpoImage.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 61591FE62FF3DD6700BEFE92 /* ExpoImage.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 7A1B2C3D4E5F60718293A12A /* ExpoFileSystem.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 61591FDE2FF3D71E00BEFE92 /* ExpoFileSystem.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -222,11 +244,22 @@ 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 */, + 7A1B2C3D4E5F60718293A120 /* ExpoFont.xcframework in Embed Frameworks */, + 7A1B2C3D4E5F60718293A121 /* SDWebImageSVGCoder.xcframework in Embed Frameworks */, + 7A1B2C3D4E5F60718293A122 /* SDWebImage.xcframework in Embed Frameworks */, + 7A1B2C3D4E5F60718293A123 /* ExpoModulesCore.xcframework in Embed Frameworks */, + 7A1B2C3D4E5F60718293A124 /* SDWebImageAVIFCoder.xcframework in Embed Frameworks */, 7A1B2C3D4E5F60718293A10C /* hermesvm.xcframework in Embed Frameworks */, - 7A1B2C3D4E5F60718293A10D /* BrownfieldLib.xcframework in Embed Frameworks */, + 7A1B2C3D4E5F60718293A125 /* ExpoModulesWorklets.xcframework in Embed Frameworks */, + 7A1B2C3D4E5F60718293A126 /* ExpoModulesJSI.xcframework in Embed Frameworks */, 7A1B2C3D4E5F60718293A10E /* React.xcframework in Embed Frameworks */, + 7A1B2C3D4E5F60718293A127 /* libavif.xcframework in Embed Frameworks */, + 7A1B2C3D4E5F60718293A10A /* ReactNativeDependencies.xcframework in Embed Frameworks */, + 7A1B2C3D4E5F60718293A128 /* SDWebImageWebPCoder.xcframework in Embed Frameworks */, + 7A1B2C3D4E5F60718293A129 /* ExpoImage.xcframework in Embed Frameworks */, + 7A1B2C3D4E5F60718293A12A /* ExpoFileSystem.xcframework in Embed Frameworks */, + 7A1B2C3D4E5F60718293A10D /* BrownfieldLib.xcframework in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; @@ -261,33 +294,12 @@ 79B1D99F2FB4A61400A5F42B /* ReactNativeDependencies.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = ReactNativeDependencies.xcframework; path = package/ReactNativeDependencies.xcframework; sourceTree = ""; }; 79B8BE9B2FB7273600B94C6F /* Brownfield Apple App (ExpoApp55).app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Brownfield Apple App (ExpoApp55).app"; sourceTree = BUILT_PRODUCTS_DIR; }; 79B8BEB22FB8000000B94C6F /* Brownfield Apple App (ExpoApp56).app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Brownfield Apple App (ExpoApp56).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; }; + 7A1B2C3D4E5F60718293A202 /* Brownfield Apple App (ExpoAppPreview).app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Brownfield Apple App (ExpoAppPreview).app"; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ -/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ - 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 = ( - 7A1B2C3D4E5F60718293A203 /* Exceptions for "Brownfield Apple App" folder in "Brownfield Apple App (ExpoAppBeta)" target */, - ); path = "Brownfield Apple App"; sourceTree = ""; }; @@ -377,12 +389,23 @@ buildActionMask = 2147483647; files = ( 7A1B2C3D4E5F60718293A101 /* Brownie.xcframework in Frameworks */, + 7A1B2C3D4E5F60718293A105 /* ReactBrownfield.xcframework in Frameworks */, 7A1B2C3D4E5F60718293A102 /* BrownfieldNavigation.xcframework in Frameworks */, - 7A1B2C3D4E5F60718293A103 /* ReactNativeDependencies.xcframework in Frameworks */, + 7A1B2C3D4E5F60718293A110 /* ExpoFont.xcframework in Frameworks */, + 7A1B2C3D4E5F60718293A111 /* SDWebImageSVGCoder.xcframework in Frameworks */, + 7A1B2C3D4E5F60718293A112 /* SDWebImage.xcframework in Frameworks */, + 7A1B2C3D4E5F60718293A113 /* ExpoModulesCore.xcframework in Frameworks */, + 7A1B2C3D4E5F60718293A114 /* SDWebImageAVIFCoder.xcframework in Frameworks */, 7A1B2C3D4E5F60718293A104 /* hermesvm.xcframework in Frameworks */, - 7A1B2C3D4E5F60718293A105 /* ReactBrownfield.xcframework in Frameworks */, - 7A1B2C3D4E5F60718293A106 /* BrownfieldLib.xcframework in Frameworks */, + 7A1B2C3D4E5F60718293A115 /* ExpoModulesWorklets.xcframework in Frameworks */, + 7A1B2C3D4E5F60718293A116 /* ExpoModulesJSI.xcframework in Frameworks */, 7A1B2C3D4E5F60718293A107 /* React.xcframework in Frameworks */, + 7A1B2C3D4E5F60718293A117 /* libavif.xcframework in Frameworks */, + 7A1B2C3D4E5F60718293A103 /* ReactNativeDependencies.xcframework in Frameworks */, + 7A1B2C3D4E5F60718293A118 /* SDWebImageWebPCoder.xcframework in Frameworks */, + 7A1B2C3D4E5F60718293A119 /* ExpoImage.xcframework in Frameworks */, + 7A1B2C3D4E5F60718293A11A /* ExpoFileSystem.xcframework in Frameworks */, + 7A1B2C3D4E5F60718293A106 /* BrownfieldLib.xcframework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -434,7 +457,7 @@ 793C76A72EEBF938008A2A34 /* Brownfield Apple App (RNApp).app */, 79B8BE9B2FB7273600B94C6F /* Brownfield Apple App (ExpoApp55).app */, 79B8BEB22FB8000000B94C6F /* Brownfield Apple App (ExpoApp56).app */, - 7A1B2C3D4E5F60718293A202 /* Brownfield Apple App (ExpoAppBeta).app */, + 7A1B2C3D4E5F60718293A202 /* Brownfield Apple App (ExpoAppPreview).app */, 617FBE3E2FF7AA9D00348DD8 /* Brownfield Apple App (ExpoApp57).app */, ); name = Products; @@ -535,9 +558,9 @@ productReference = 79B8BEB22FB8000000B94C6F /* Brownfield Apple App (ExpoApp56).app */; productType = "com.apple.product-type.application"; }; - 7A1B2C3D4E5F60718293A301 /* Brownfield Apple App (ExpoAppBeta) */ = { + 7A1B2C3D4E5F60718293A301 /* Brownfield Apple App (ExpoAppPreview) */ = { isa = PBXNativeTarget; - buildConfigurationList = 7A1B2C3D4E5F60718293A405 /* Build configuration list for PBXNativeTarget "Brownfield Apple App (ExpoAppBeta)" */; + buildConfigurationList = 7A1B2C3D4E5F60718293A405 /* Build configuration list for PBXNativeTarget "Brownfield Apple App (ExpoAppPreview)" */; buildPhases = ( 7A1B2C3D4E5F60718293A302 /* Sources */, 7A1B2C3D4E5F60718293A204 /* Frameworks */, @@ -551,11 +574,11 @@ fileSystemSynchronizedGroups = ( 793C76A92EEBF938008A2A34 /* Brownfield Apple App */, ); - name = "Brownfield Apple App (ExpoAppBeta)"; + name = "Brownfield Apple App (ExpoAppPreview)"; packageProductDependencies = ( ); productName = "Brownfield Apple App"; - productReference = 7A1B2C3D4E5F60718293A202 /* Brownfield Apple App (ExpoAppBeta).app */; + productReference = 7A1B2C3D4E5F60718293A202 /* Brownfield Apple App (ExpoAppPreview).app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ @@ -599,7 +622,7 @@ 79B8BE832FB7273600B94C6F /* Brownfield Apple App (ExpoApp55) */, 79B8BEB42FB8000000B94C6F /* Brownfield Apple App (ExpoApp56) */, 617FBE102FF7AA9D00348DD8 /* Brownfield Apple App (ExpoApp57) */, - 7A1B2C3D4E5F60718293A301 /* Brownfield Apple App (ExpoAppBeta) */, + 7A1B2C3D4E5F60718293A301 /* Brownfield Apple App (ExpoAppPreview) */, ); }; /* End PBXProject section */ @@ -1915,7 +1938,7 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = "Release Expo"; }; - 7A1B2C3D4E5F60718293A405 /* Build configuration list for PBXNativeTarget "Brownfield Apple App (ExpoAppBeta)" */ = { + 7A1B2C3D4E5F60718293A405 /* Build configuration list for PBXNativeTarget "Brownfield Apple App (ExpoAppPreview)" */ = { isa = XCConfigurationList; buildConfigurations = ( 7A1B2C3D4E5F60718293A501 /* Debug Expo */, 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 Preview.xcscheme similarity index 92% rename from apps/AppleApp/Brownfield Apple App.xcodeproj/xcshareddata/xcschemes/Brownfield Apple App Expo Beta.xcscheme rename to apps/AppleApp/Brownfield Apple App.xcodeproj/xcshareddata/xcschemes/Brownfield Apple App Expo Preview.xcscheme index 6ffc555f..0c2ef9fa 100644 --- 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 Preview.xcscheme @@ -29,8 +29,8 @@ diff --git a/apps/AppleApp/package.json b/apps/AppleApp/package.json index 74b64618..c45e96f8 100644 --- a/apps/AppleApp/package.json +++ b/apps/AppleApp/package.json @@ -8,7 +8,7 @@ "build:example:ios-consumer:expo57": "node prepareXCFrameworks.js --appName ExpoApp57 && yarn internal::build::common -scheme \"Brownfield Apple App Expo 57\" -configuration Release", "build:example:ios-consumer:expo56": "node prepareXCFrameworks.js --appName ExpoApp56 && yarn internal::build::common -scheme \"Brownfield Apple App Expo 56\" -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:expopreview": "node prepareXCFrameworks.js --appName ExpoAppPreview && yarn internal::build::common -scheme \"Brownfield Apple App Expo Preview\" -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", "e2e:build:ios": "detox build --configuration ios.sim.debug", diff --git a/apps/ExpoApp57/brownfield.navigation.ts b/apps/ExpoApp57/brownfield.navigation.ts index b4bfe8c2..c42d5ca4 100644 --- a/apps/ExpoApp57/brownfield.navigation.ts +++ b/apps/ExpoApp57/brownfield.navigation.ts @@ -22,4 +22,14 @@ export interface BrownfieldNavigationSpec { * @param userId - The user's unique identifier */ navigateToReferrals(userId: string): void; + + /** + * Ask the native host to confirm an action. Resolves with the user's choice. + */ + requestNativeConfirmation(title: string): Promise; + + /** + * Show a native banner. The native host calls onDismiss when the banner is dismissed. + */ + showNativeBanner(message: string, onDismiss: () => void): void; } diff --git a/apps/README.md b/apps/README.md index f6dff596..e8d5e6c5 100644 --- a/apps/README.md +++ b/apps/README.md @@ -5,7 +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 - `ExpoApp55` - Expo application using Expo SDK v55 - `ExpoApp56` - a fresh Expo SDK v56 application wired into the same Brownfield packaging and consumer-app flows as the older Expo examples -- `ExpoAppBeta` - a temporary Expo app generated in CI to test new Expo beta releases before stable support is added +- `ExpoAppPreview` - a temporary Expo app generated in CI to test new Expo preview 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` @@ -13,5 +13,5 @@ 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 (ExpoApp55)` — uses the artifact from `ExpoApp55` (scheme **Brownfield Apple App Expo 55**, configuration `Release`) - `Brownfield Apple App (ExpoApp56)` — uses the artifact from `ExpoApp56` (scheme **Brownfield Apple App Expo 56**, 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:expo55`, `yarn build:example:ios-consumer:expo56`, or `yarn build:example:ios-consumer:expobeta` to copy XCFrameworks into `package/` and build the matching target. `yarn build:example:ios-consumer:expo` is an alias for `expo56`. + - `Brownfield Apple App (ExpoAppPreview)` — uses the artifact from `ExpoAppPreview` (scheme **Brownfield Apple App Expo Preview**, configuration `Release`) + From `apps/AppleApp`, run `yarn build:example:ios-consumer:vanilla`, `yarn build:example:ios-consumer:expo55`, `yarn build:example:ios-consumer:expo56`, or `yarn build:example:ios-consumer:expopreview` to copy XCFrameworks into `package/` and build the matching target. `yarn build:example:ios-consumer:expo` is an alias for `expo56`. diff --git a/scripts/__tests__/check-expo-beta.test.ts b/scripts/__tests__/check-expo-beta.test.ts deleted file mode 100644 index 3e42a0db..00000000 --- a/scripts/__tests__/check-expo-beta.test.ts +++ /dev/null @@ -1,43 +0,0 @@ -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/__tests__/check-expo-preview.test.ts b/scripts/__tests__/check-expo-preview.test.ts new file mode 100644 index 00000000..b45e78f0 --- /dev/null +++ b/scripts/__tests__/check-expo-preview.test.ts @@ -0,0 +1,138 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { + ensureConsumerNavigationSpec, + findLatestPreviewVersion, + replaceHomeScreenTitle, + replaceTemplateAppReferences, + updateExpoVersion, + type ExpoPackageJson, +} from '../check-expo-preview.ts'; + +test('updates ExpoApp package.json when a new preview version is provided', () => { + const packageJson: ExpoPackageJson = { + dependencies: { + expo: '~57.0.0', + }, + }; + + const updated = updateExpoVersion(packageJson, '57.0.0-preview.1'); + + assert.equal(updated, true); + assert.equal(packageJson.dependencies?.expo, '57.0.0-preview.1'); +}); + +test('does not rewrite ExpoApp package.json when the preview version is unchanged', () => { + const packageJson: ExpoPackageJson = { + dependencies: { + expo: '57.0.0-preview.1', + }, + }; + + const updated = updateExpoVersion(packageJson, '57.0.0-preview.1'); + + assert.equal(updated, false); + assert.equal(packageJson.dependencies?.expo, '57.0.0-preview.1'); +}); + +test('selects the newest Expo preview version', () => { + const latest = findLatestPreviewVersion([ + '57.0.0', + '57.0.0-canary-20260629-3010085', + '57.0.0-preview.1', + '57.0.0-preview.2', + '56.0.0-preview.9', + ]); + + assert.equal(latest, '57.0.0-preview.2'); +}); + +test('rewrites Expo template identifiers for ExpoApp57', () => { + const input = JSON.stringify( + { + name: '@callstack/brownfield-example-expo-app-57', + brownie: { + kotlin: + './android/brownfieldlib/src/main/java/com/callstack/rnbrownfield/demo/expoapp57/Generated/', + kotlinPackageName: 'com.callstack.rnbrownfield.demo.expoapp57', + }, + scripts: { + 'brownfield:prepare:android:ci': + 'cd .. && node ./scripts/prepare-android-build-gradle-for-ci.ts ExpoApp57', + }, + }, + null, + 2 + ); + + const output = replaceTemplateAppReferences(input, 57, 'ExpoApp57'); + + assert.match(output, /@callstack\/brownfield-example-expo-app-preview/); + assert.doesNotMatch(output, /brownfield-example-expo-app-57/); + assert.match(output, /com\.callstack\.rnbrownfield\.demo\.expoapppreview/); + assert.doesNotMatch(output, /expoapp57/); + assert.match(output, /ExpoAppPreview/); + assert.doesNotMatch(output, /ExpoApp57/); +}); + +test('rewrites brownfield.config.json for ExpoApp57 template', () => { + const input = JSON.stringify( + { + brownie: { + kotlin: + './android/brownfieldlib/src/main/java/com/callstack/rnbrownfield/demo/expoapp57/Generated/', + kotlinPackageName: 'com.callstack.rnbrownfield.demo.expoapp57', + }, + }, + null, + 2 + ); + + const output = replaceTemplateAppReferences(input, 57, 'ExpoApp57'); + + assert.match(output, /com\.callstack\.rnbrownfield\.demo\.expoapppreview/); + assert.doesNotMatch(output, /expoapp57/); +}); + +test('adds consumer road-test navigation methods when missing from template', () => { + const input = `export interface BrownfieldNavigationSpec { + navigateToSettings(user: UserType): void; + navigateToReferrals(userId: string): void; +}`; + + const output = ensureConsumerNavigationSpec(input); + + assert.match(output, /requestNativeConfirmation/); + assert.match(output, /showNativeBanner/); +}); + +test('does not duplicate consumer road-test navigation methods', () => { + const input = `export interface BrownfieldNavigationSpec { + navigateToReferrals(userId: string): void; + requestNativeConfirmation(title: string): Promise; +}`; + + const output = ensureConsumerNavigationSpec(input); + + assert.equal(output, input); +}); + +test('rewrites the home screen title for the latest Expo template version', () => { + const input = 'Welcome to Expo 57'; + + const output = replaceHomeScreenTitle(input, 57); + + assert.equal(output, 'Welcome to Expo Preview'); +}); + +test('fails loudly when expo dependency is missing', () => { + const packageJson: ExpoPackageJson = { + dependencies: {}, + }; + + assert.throws(() => updateExpoVersion(packageJson, '57.0.0-preview.1'), { + message: + /Could not locate dependencies\.expo in ExpoAppPreview\/package\.json/, + }); +}); diff --git a/scripts/check-expo-beta.ts b/scripts/check-expo-beta.ts deleted file mode 100644 index 88a348ca..00000000 --- a/scripts/check-expo-beta.ts +++ /dev/null @@ -1,281 +0,0 @@ -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_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 fetchLatestExpoCanaryVersion(): 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 canaryTag = data['dist-tags']?.canary; - if (canaryTag) { - return canaryTag; - } - - const versions = Object.keys(data.versions ?? {}) - .filter((version) => version.includes('canary')) - .sort((left, right) => left.localeCompare(right, undefined, { numeric: true })); - - 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 getExpoTemplateAppDir(): string { - const appsDir = path.join(REPO_ROOT, 'apps'); - const expoAppCandidates = fs - .readdirSync(appsDir, { withFileTypes: true }) - .filter((entry) => entry.isDirectory() && /^ExpoApp\d+$/.test(entry.name)) - .map((entry) => ({ - path: path.join(appsDir, entry.name), - version: Number(entry.name.replace('ExpoApp', '')), - })) - .sort((left, right) => right.version - left.version); - - const latestCandidate = expoAppCandidates[0]; - if (!latestCandidate) { - throw new Error('Could not find an Expo template app directory'); - } - - return latestCandidate.path; -} - -function replaceTemplateAppReferences(contents: string): string { - return contents - .replaceAll('@callstack/brownfield-example-expo-app-56', '@callstack/brownfield-example-expo-app-beta') - .replaceAll('@callstack/brownfield-example-expo-app-55', '@callstack/brownfield-example-expo-app-beta') - .replaceAll('com.callstack.rnbrownfield.demo.expoapp56', 'com.callstack.rnbrownfield.demo.expobeta') - .replaceAll('com.callstack.rnbrownfield.demo.expoapp55', 'com.callstack.rnbrownfield.demo.expobeta') - .replaceAll('./android/brownfieldlib/src/main/java/com/callstack/rnbrownfield/demo/expoapp56/Generated/', './android/brownfieldlib/src/main/java/com/callstack/rnbrownfield/demo/expobeta/Generated/') - .replaceAll('./android/brownfieldlib/src/main/java/com/callstack/rnbrownfield/demo/expoapp55/Generated/', './android/brownfieldlib/src/main/java/com/callstack/rnbrownfield/demo/expobeta/Generated/') - .replaceAll('ExpoApp56', 'ExpoAppBeta') - .replaceAll('ExpoApp55', 'ExpoAppBeta') - .replaceAll('expoapp56', 'expoappbeta') - .replaceAll('expoapp55', 'expoappbeta') - .replaceAll('expoappbeta56', 'expoappbeta') - .replaceAll('expoappbeta55', 'expoappbeta'); -} - -function generateExpoBetaApp(): void { - rmSync(EXPO_BETA_APP_DIR, { recursive: true, force: true }); - mkdirSync(path.dirname(EXPO_BETA_APP_DIR), { recursive: true }); - cpSync(getExpoTemplateAppDir(), EXPO_BETA_APP_DIR, { recursive: true }); - - updateFileContents(EXPO_BETA_PACKAGE_JSON_PATH, replaceTemplateAppReferences); - - updateFileContents(EXPO_BETA_APP_JSON_PATH, replaceTemplateAppReferences); - - const testPath = path.join(EXPO_BETA_APP_DIR, '__tests__', 'brownfield.example.test.tsx'); - if (existsSync(testPath)) { - updateFileContents(testPath, replaceTemplateAppReferences); - } - - const homeScreenPath = path.join(EXPO_BETA_APP_DIR, 'src', 'app', 'index.tsx'); - if (existsSync(homeScreenPath)) { - updateFileContents(homeScreenPath, (contents) => - contents - .replaceAll('Expo\u00a056', 'Expo\u00a0Beta') - .replaceAll('Expo\u00a055', 'Expo\u00a0Beta') - .replaceAll('Expo 56', 'Expo Beta') - .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 fetchLatestExpoCanaryVersion()); - - 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(); diff --git a/scripts/check-expo-preview.ts b/scripts/check-expo-preview.ts new file mode 100644 index 00000000..49fe4b1a --- /dev/null +++ b/scripts/check-expo-preview.ts @@ -0,0 +1,398 @@ +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_PREVIEW_APP_DIR = path.join(REPO_ROOT, 'apps', 'ExpoAppPreview'); +const EXPO_PREVIEW_PACKAGE_JSON_PATH = path.join( + EXPO_PREVIEW_APP_DIR, + 'package.json' +); +const EXPO_PREVIEW_APP_JSON_PATH = path.join(EXPO_PREVIEW_APP_DIR, 'app.json'); +const EXPO_PREVIEW_BROWNFIELD_CONFIG_PATH = path.join( + EXPO_PREVIEW_APP_DIR, + 'brownfield.config.json' +); +const EXPO_PREVIEW_NAVIGATION_SPEC_PATH = path.join( + EXPO_PREVIEW_APP_DIR, + 'brownfield.navigation.ts' +); +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 function findLatestPreviewVersion(versions: string[]): string | null { + const previewVersions = versions + .filter((version) => version.includes('-preview')) + .sort((left, right) => + left.localeCompare(right, undefined, { numeric: true }) + ); + + return previewVersions.at(-1) ?? null; +} + +export async function fetchLatestExpoPreviewVersion(): 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 { + versions?: Record; + }; + + return findLatestPreviewVersion(Object.keys(data.versions ?? {})); +} + +function updateFileContents( + filePath: string, + updater: (contents: string) => string +): void { + const contents = readFileSync(filePath, 'utf8'); + writeFileSync(filePath, updater(contents), 'utf8'); +} + +interface ExpoTemplateApp { + path: string; + version: number; + name: string; +} + +function getExpoTemplateApp(): ExpoTemplateApp { + const appsDir = path.join(REPO_ROOT, 'apps'); + const expoAppCandidates = fs + .readdirSync(appsDir, { withFileTypes: true }) + .filter((entry) => entry.isDirectory() && /^ExpoApp\d+$/.test(entry.name)) + .map((entry) => ({ + path: path.join(appsDir, entry.name), + version: Number(entry.name.replace('ExpoApp', '')), + name: entry.name, + })) + .sort((left, right) => right.version - left.version); + + const latestCandidate = expoAppCandidates[0]; + if (!latestCandidate) { + throw new Error('Could not find an Expo template app directory'); + } + + return latestCandidate; +} + +export function replaceTemplateAppReferences( + contents: string, + templateVersion: number, + templateName: string +): string { + const version = templateVersion.toString(); + + return contents + .replaceAll( + `@callstack/brownfield-example-expo-app-${version}`, + '@callstack/brownfield-example-expo-app-preview' + ) + .replaceAll( + `com.callstack.rnbrownfield.demo.expoapp${version}`, + 'com.callstack.rnbrownfield.demo.expoapppreview' + ) + .replaceAll( + `./android/brownfieldlib/src/main/java/com/callstack/rnbrownfield/demo/expoapp${version}/Generated/`, + './android/brownfieldlib/src/main/java/com/callstack/rnbrownfield/demo/expoapppreview/Generated/' + ) + .replaceAll(templateName, 'ExpoAppPreview') + .replaceAll(`expoapp${version}`, 'expoapppreview') + .replaceAll(`expoapppreview${version}`, 'expoapppreview'); +} + +export function replaceHomeScreenTitle( + contents: string, + templateVersion: number +): string { + const version = templateVersion.toString(); + + return contents + .replaceAll(`Expo\u00a0${version}`, 'Expo\u00a0Preview') + .replaceAll(`Expo ${version}`, 'Expo Preview'); +} + +const CONSUMER_ROAD_TEST_NAVIGATION_METHODS = ` + /** + * Ask the native host to confirm an action. Resolves with the user's choice. + */ + requestNativeConfirmation(title: string): Promise; + + /** + * Show a native banner. The native host calls onDismiss when the banner is dismissed. + */ + showNativeBanner(message: string, onDismiss: () => void): void; +`; + +export function ensureConsumerNavigationSpec(contents: string): string { + if (contents.includes('requestNativeConfirmation')) { + return contents; + } + + const updated = contents.replace( + /\n}\s*$/u, + `${CONSUMER_ROAD_TEST_NAVIGATION_METHODS}\n}` + ); + + if (updated === contents) { + throw new Error( + 'Could not locate end of BrownfieldNavigationSpec to inject consumer road-test methods' + ); + } + + return updated; +} + +function generateExpoPreviewApp(): void { + const templateApp = getExpoTemplateApp(); + const replaceReferences = (contents: string) => + replaceTemplateAppReferences( + contents, + templateApp.version, + templateApp.name + ); + + rmSync(EXPO_PREVIEW_APP_DIR, { recursive: true, force: true }); + mkdirSync(path.dirname(EXPO_PREVIEW_APP_DIR), { recursive: true }); + cpSync(templateApp.path, EXPO_PREVIEW_APP_DIR, { recursive: true }); + + updateFileContents(EXPO_PREVIEW_PACKAGE_JSON_PATH, replaceReferences); + + updateFileContents(EXPO_PREVIEW_APP_JSON_PATH, replaceReferences); + + if (existsSync(EXPO_PREVIEW_BROWNFIELD_CONFIG_PATH)) { + updateFileContents(EXPO_PREVIEW_BROWNFIELD_CONFIG_PATH, replaceReferences); + } + + if (existsSync(EXPO_PREVIEW_NAVIGATION_SPEC_PATH)) { + updateFileContents(EXPO_PREVIEW_NAVIGATION_SPEC_PATH, ensureConsumerNavigationSpec); + } + + const testPath = path.join( + EXPO_PREVIEW_APP_DIR, + '__tests__', + 'brownfield.example.test.tsx' + ); + if (existsSync(testPath)) { + updateFileContents(testPath, replaceReferences); + } + + const homeScreenPath = path.join( + EXPO_PREVIEW_APP_DIR, + 'src', + 'app', + 'index.tsx' + ); + if (existsSync(homeScreenPath)) { + updateFileContents(homeScreenPath, (contents) => + replaceHomeScreenTitle(contents, templateApp.version) + ); + } +} + +function readExpoPreviewPackageJson(): ExpoPackageJson { + return JSON.parse( + readFileSync(EXPO_PREVIEW_PACKAGE_JSON_PATH, 'utf8') + ) as ExpoPackageJson; +} + +function writeExpoPreviewPackageJson(packageJson: ExpoPackageJson): void { + writeFileSync( + EXPO_PREVIEW_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 ExpoAppPreview/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 preview check', + '', + `- Latest Expo preview: ${latestVersion ?? 'not found'}`, + `- Last tested Expo preview: ${testedVersion ?? 'none'}`, + `- Should run road tests: ${shouldTest ? 'yes' : 'no'}`, + `- ExpoAppPreview generated on-the-fly: ${generated ? 'yes' : 'no'}`, + `- ExpoAppPreview 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 fetchLatestExpoPreviewVersion()); + + 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 preview release found'); + return; + } + + const shouldTest = latestVersion !== options.testedVersion; + let applied = false; + let generated = false; + + if (shouldTest && options.apply) { + generateExpoPreviewApp(); + generated = true; + const packageJson = readExpoPreviewPackageJson(); + applied = updateExpoVersion(packageJson, latestVersion); + writeExpoPreviewPackageJson(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 preview: ${latestVersion}`); + console.log(`Last tested Expo preview: ${options.testedVersion ?? 'none'}`); + console.log(`Should test: ${shouldTest}`); + console.log(`Generated ExpoAppPreview: ${generated}`); + console.log(`Applied: ${applied}`); +} + +main();