From 564719e83a891522ff7006521e4c262d96cfea69 Mon Sep 17 00:00:00 2001 From: artus9033 Date: Mon, 13 Jul 2026 09:57:29 +0200 Subject: [PATCH 01/10] fix: check-expo-beta script not to conflict in WS --- scripts/__tests__/check-expo-beta.test.ts | 41 +++++- scripts/check-expo-beta.ts | 146 ++++++++++++++++------ 2 files changed, 149 insertions(+), 38 deletions(-) diff --git a/scripts/__tests__/check-expo-beta.test.ts b/scripts/__tests__/check-expo-beta.test.ts index 3e42a0db..aaaeb76a 100644 --- a/scripts/__tests__/check-expo-beta.test.ts +++ b/scripts/__tests__/check-expo-beta.test.ts @@ -2,6 +2,8 @@ import assert from 'node:assert/strict'; import test from 'node:test'; import { + replaceHomeScreenTitle, + replaceTemplateAppReferences, updateExpoVersion, type ExpoPackageJson, } from '../check-expo-beta.ts'; @@ -32,12 +34,49 @@ test('does not rewrite ExpoApp package.json when the beta version is unchanged', assert.equal(packageJson.dependencies?.expo, '56.0.0-beta.1'); }); +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-beta/); + assert.doesNotMatch(output, /brownfield-example-expo-app-57/); + assert.match(output, /com\.callstack\.rnbrownfield\.demo\.expobeta/); + assert.doesNotMatch(output, /expoapp57/); + assert.match(output, /ExpoAppBeta/); + assert.doesNotMatch(output, /ExpoApp57/); +}); + +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 Beta'); +}); + 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/, + message: + /Could not locate dependencies\.expo in ExpoAppBeta\/package\.json/, }); }); diff --git a/scripts/check-expo-beta.ts b/scripts/check-expo-beta.ts index 88a348ca..ad25624b 100644 --- a/scripts/check-expo-beta.ts +++ b/scripts/check-expo-beta.ts @@ -1,6 +1,13 @@ import fs from 'node:fs'; import path from 'node:path'; -import { cpSync, mkdirSync, rmSync, existsSync, readFileSync, writeFileSync } from 'node:fs'; +import { + cpSync, + mkdirSync, + rmSync, + existsSync, + readFileSync, + writeFileSync, +} from 'node:fs'; import { fileURLToPath } from 'node:url'; interface CliOptions { @@ -26,7 +33,10 @@ 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_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'; @@ -87,17 +97,28 @@ export async function fetchLatestExpoCanaryVersion(): Promise { const versions = Object.keys(data.versions ?? {}) .filter((version) => version.includes('canary')) - .sort((left, right) => left.localeCompare(right, undefined, { numeric: true })); + .sort((left, right) => + left.localeCompare(right, undefined, { numeric: true }) + ); return versions.at(-1) ?? null; } -function updateFileContents(filePath: string, updater: (contents: string) => string): void { +function updateFileContents( + filePath: string, + updater: (contents: string) => string +): void { const contents = readFileSync(filePath, 'utf8'); writeFileSync(filePath, updater(contents), 'utf8'); } -function getExpoTemplateAppDir(): string { +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 }) @@ -105,6 +126,7 @@ function getExpoTemplateAppDir(): string { .map((entry) => ({ path: path.join(appsDir, entry.name), version: Number(entry.name.replace('ExpoApp', '')), + name: entry.name, })) .sort((left, right) => right.version - left.version); @@ -113,62 +135,106 @@ function getExpoTemplateAppDir(): string { throw new Error('Could not find an Expo template app directory'); } - return latestCandidate.path; + 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-beta' + ) + .replaceAll( + `com.callstack.rnbrownfield.demo.expoapp${version}`, + 'com.callstack.rnbrownfield.demo.expobeta' + ) + .replaceAll( + `./android/brownfieldlib/src/main/java/com/callstack/rnbrownfield/demo/expoapp${version}/Generated/`, + './android/brownfieldlib/src/main/java/com/callstack/rnbrownfield/demo/expobeta/Generated/' + ) + .replaceAll(templateName, 'ExpoAppBeta') + .replaceAll(`expoapp${version}`, 'expoappbeta') + .replaceAll(`expoappbeta${version}`, 'expoappbeta'); } -function replaceTemplateAppReferences(contents: string): string { +export function replaceHomeScreenTitle( + contents: string, + templateVersion: number +): string { + const version = templateVersion.toString(); + 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'); + .replaceAll(`Expo\u00a0${version}`, 'Expo\u00a0Beta') + .replaceAll(`Expo ${version}`, 'Expo Beta'); } function generateExpoBetaApp(): void { + const templateApp = getExpoTemplateApp(); + const replaceReferences = (contents: string) => + replaceTemplateAppReferences( + contents, + templateApp.version, + templateApp.name + ); + 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 }); + cpSync(templateApp.path, EXPO_BETA_APP_DIR, { recursive: true }); - updateFileContents(EXPO_BETA_PACKAGE_JSON_PATH, replaceTemplateAppReferences); + updateFileContents(EXPO_BETA_PACKAGE_JSON_PATH, replaceReferences); - updateFileContents(EXPO_BETA_APP_JSON_PATH, replaceTemplateAppReferences); + updateFileContents(EXPO_BETA_APP_JSON_PATH, replaceReferences); - const testPath = path.join(EXPO_BETA_APP_DIR, '__tests__', 'brownfield.example.test.tsx'); + const testPath = path.join( + EXPO_BETA_APP_DIR, + '__tests__', + 'brownfield.example.test.tsx' + ); if (existsSync(testPath)) { - updateFileContents(testPath, replaceTemplateAppReferences); + updateFileContents(testPath, replaceReferences); } - const homeScreenPath = path.join(EXPO_BETA_APP_DIR, 'src', 'app', 'index.tsx'); + 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') + replaceHomeScreenTitle(contents, templateApp.version) ); } } function readExpoBetaPackageJson(): ExpoPackageJson { - return JSON.parse(readFileSync(EXPO_BETA_PACKAGE_JSON_PATH, 'utf8')) as 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'); + writeFileSync( + EXPO_BETA_PACKAGE_JSON_PATH, + `${JSON.stringify(packageJson, null, 2)}\n`, + 'utf8' + ); } -export function updateExpoVersion(packageJson: ExpoPackageJson, expoVersion: string): boolean { +export function updateExpoVersion( + packageJson: ExpoPackageJson, + expoVersion: string +): boolean { if (!packageJson.dependencies?.expo) { - throw new Error('Could not locate dependencies.expo in ExpoAppBeta/package.json'); + throw new Error( + 'Could not locate dependencies.expo in ExpoAppBeta/package.json' + ); } if (packageJson.dependencies.expo === expoVersion) { @@ -179,12 +245,17 @@ export function updateExpoVersion(packageJson: ExpoPackageJson, expoVersion: str return true; } -function appendKeyValueFile(filePath: string | undefined, entries: Record): void { +function appendKeyValueFile( + filePath: string | undefined, + entries: Record +): void { if (!filePath) { return; } - const lines = Object.entries(entries).map(([key, value]) => `${key}=${value}`); + const lines = Object.entries(entries).map( + ([key, value]) => `${key}=${value}` + ); fs.appendFileSync(filePath, `${lines.join('\n')}\n`, 'utf8'); } @@ -224,7 +295,8 @@ function appendSummary( async function main(): Promise { const options = parseArgs(process.argv.slice(2)); - const latestVersion = options.expoVersion ?? (await fetchLatestExpoCanaryVersion()); + const latestVersion = + options.expoVersion ?? (await fetchLatestExpoCanaryVersion()); if (!latestVersion) { appendKeyValueFile(options.githubOutput, { From 89f843d4c90afb353aa3321115231f7733ee0cbe Mon Sep 17 00:00:00 2001 From: artus9033 Date: Mon, 13 Jul 2026 15:51:35 +0200 Subject: [PATCH 02/10] fix(ci): sync Expo beta deps and skip redundant road-test setup Ensure expo install --fix runs through Yarn and is followed by a root yarn install so canary Expo packages (e.g. expo-image) are installed correctly. Skip setup in road-test actions for the Expo beta workflow to avoid reinstalling stable deps over the canary set. Ignore generated ExpoAppBeta in apps. --- .github/actions/androidapp-road-test/action.yml | 6 ++++++ .github/actions/appleapp-road-test/action.yml | 6 ++++++ .github/workflows/expo-beta-road-test.yml | 12 ++++++++---- apps/.gitignore | 1 + 4 files changed, 21 insertions(+), 4 deletions(-) create mode 100644 apps/.gitignore diff --git a/.github/actions/androidapp-road-test/action.yml b/.github/actions/androidapp-road-test/action.yml index 7b4a0e43..bf744c22 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 beta 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..3056ee22 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 beta 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-beta-road-test.yml index 0b28d818..f83a6fb9 100644 --- a/.github/workflows/expo-beta-road-test.yml +++ b/.github/workflows/expo-beta-road-test.yml @@ -82,9 +82,10 @@ jobs: - name: Install Expo beta 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-beta exec expo install --fix + yarn install - name: Rebuild packages after Expo beta dependency updates run: yarn build @@ -96,6 +97,7 @@ jobs: rn-project-path: apps/ExpoAppBeta rn-project-maven-path: com/callstack/rnbrownfield/demo/expobeta/brownfieldlib restore-turbo-cache: 'false' + skip-setup: 'true' ios: name: iOS road test (Expo beta) @@ -119,9 +121,10 @@ jobs: - name: Install Expo beta 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-beta exec expo install --fix + yarn install - name: Rebuild packages after Expo beta dependency updates run: yarn build @@ -132,6 +135,7 @@ jobs: variant: expobeta rn-project-path: apps/ExpoAppBeta restore-turbo-cache: 'false' + skip-setup: 'true' record-success: name: Record tested Expo beta diff --git a/apps/.gitignore b/apps/.gitignore new file mode 100644 index 00000000..e044dd27 --- /dev/null +++ b/apps/.gitignore @@ -0,0 +1 @@ +ExpoAppBeta From a0b4bc5f91fdd3c8858a8872138686da6ff37c05 Mon Sep 17 00:00:00 2001 From: artus9033 Date: Mon, 13 Jul 2026 16:49:49 +0200 Subject: [PATCH 03/10] feat: use expo -preview versions instead of canary tag --- .../actions/androidapp-road-test/action.yml | 2 +- .github/actions/appleapp-road-test/action.yml | 2 +- ...ad-test.yml => expo-preview-road-test.yml} | 82 +++++++------- apps/.gitignore | 2 +- apps/AndroidApp/app/build.gradle.kts | 4 +- apps/AndroidApp/gradle/libs.versions.toml | 2 +- apps/AndroidApp/package.json | 2 +- .../project.pbxproj | 22 ++-- ...rownfield Apple App Expo Preview.xcscheme} | 4 +- apps/AppleApp/package.json | 2 +- apps/README.md | 6 +- ...eta.test.ts => check-expo-preview.test.ts} | 43 +++++--- ...eck-expo-beta.ts => check-expo-preview.ts} | 102 +++++++++--------- 13 files changed, 143 insertions(+), 132 deletions(-) rename .github/workflows/{expo-beta-road-test.yml => expo-preview-road-test.yml} (66%) rename apps/AppleApp/Brownfield Apple App.xcodeproj/xcshareddata/xcschemes/{Brownfield Apple App Expo Beta.xcscheme => Brownfield Apple App Expo Preview.xcscheme} (92%) rename scripts/__tests__/{check-expo-beta.test.ts => check-expo-preview.test.ts} (59%) rename scripts/{check-expo-beta.ts => check-expo-preview.ts} (72%) diff --git a/.github/actions/androidapp-road-test/action.yml b/.github/actions/androidapp-road-test/action.yml index bf744c22..c0a6298f 100644 --- a/.github/actions/androidapp-road-test/action.yml +++ b/.github/actions/androidapp-road-test/action.yml @@ -20,7 +20,7 @@ inputs: default: 'true' skip-setup: - description: 'Skip yarn install/build when the caller already prepared the monorepo (e.g. Expo beta workflow)' + description: 'Skip yarn install/build when the caller already prepared the monorepo (e.g. Expo preview workflow)' required: false default: 'false' diff --git a/.github/actions/appleapp-road-test/action.yml b/.github/actions/appleapp-road-test/action.yml index 3056ee22..3c8ca06a 100644 --- a/.github/actions/appleapp-road-test/action.yml +++ b/.github/actions/appleapp-road-test/action.yml @@ -26,7 +26,7 @@ inputs: default: 'true' skip-setup: - description: 'Skip yarn install/build when the caller already prepared the monorepo (e.g. Expo beta workflow)' + description: 'Skip yarn install/build when the caller already prepared the monorepo (e.g. Expo preview workflow)' required: false default: 'false' diff --git a/.github/workflows/expo-beta-road-test.yml b/.github/workflows/expo-preview-road-test.yml similarity index 66% rename from .github/workflows/expo-beta-road-test.yml rename to .github/workflows/expo-preview-road-test.yml index f83a6fb9..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,28 +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: | - yarn workspace @callstack/brownfield-example-expo-app-beta exec 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' @@ -108,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 @@ -118,27 +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: | - yarn workspace @callstack/brownfield-example-expo-app-beta exec 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: | @@ -146,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: | @@ -170,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() @@ -195,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 index e044dd27..853d1748 100644 --- a/apps/.gitignore +++ b/apps/.gitignore @@ -1 +1 @@ -ExpoAppBeta +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/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..d9f80641 100644 --- a/apps/AppleApp/Brownfield Apple App.xcodeproj/project.pbxproj +++ b/apps/AppleApp/Brownfield Apple App.xcodeproj/project.pbxproj @@ -261,11 +261,11 @@ 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 */ = { + 7A1B2C3D4E5F60718293A203 /* Exceptions for "Brownfield Apple App" folder in "Brownfield Apple App (ExpoAppPreview)" target */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( Assets.xcassets, @@ -278,7 +278,7 @@ components/SettingsScreen.swift, components/Toast.swift, ); - target = 7A1B2C3D4E5F60718293A301 /* Brownfield Apple App (ExpoAppBeta) */; + target = 7A1B2C3D4E5F60718293A301 /* Brownfield Apple App (ExpoAppPreview) */; }; /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ @@ -286,7 +286,7 @@ 793C76A92EEBF938008A2A34 /* Brownfield Apple App */ = { isa = PBXFileSystemSynchronizedRootGroup; exceptions = ( - 7A1B2C3D4E5F60718293A203 /* Exceptions for "Brownfield Apple App" folder in "Brownfield Apple App (ExpoAppBeta)" target */, + 7A1B2C3D4E5F60718293A203 /* Exceptions for "Brownfield Apple App" folder in "Brownfield Apple App (ExpoAppPreview)" target */, ); path = "Brownfield Apple App"; sourceTree = ""; @@ -434,7 +434,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 +535,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 +551,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 +599,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 +1915,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/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-preview.test.ts similarity index 59% rename from scripts/__tests__/check-expo-beta.test.ts rename to scripts/__tests__/check-expo-preview.test.ts index aaaeb76a..7ba414d8 100644 --- a/scripts/__tests__/check-expo-beta.test.ts +++ b/scripts/__tests__/check-expo-preview.test.ts @@ -2,36 +2,49 @@ import assert from 'node:assert/strict'; import test from 'node:test'; import { + findLatestPreviewVersion, replaceHomeScreenTitle, replaceTemplateAppReferences, updateExpoVersion, type ExpoPackageJson, -} from '../check-expo-beta.ts'; +} from '../check-expo-preview.ts'; -test('updates ExpoApp package.json when a new beta version is provided', () => { +test('updates ExpoApp package.json when a new preview version is provided', () => { const packageJson: ExpoPackageJson = { dependencies: { - expo: '~55.0.23', + expo: '~57.0.0', }, }; - const updated = updateExpoVersion(packageJson, '56.0.0-beta.1'); + const updated = updateExpoVersion(packageJson, '57.0.0-preview.1'); assert.equal(updated, true); - assert.equal(packageJson.dependencies?.expo, '56.0.0-beta.1'); + assert.equal(packageJson.dependencies?.expo, '57.0.0-preview.1'); }); -test('does not rewrite ExpoApp package.json when the beta version is unchanged', () => { +test('does not rewrite ExpoApp package.json when the preview version is unchanged', () => { const packageJson: ExpoPackageJson = { dependencies: { - expo: '56.0.0-beta.1', + expo: '57.0.0-preview.1', }, }; - const updated = updateExpoVersion(packageJson, '56.0.0-beta.1'); + const updated = updateExpoVersion(packageJson, '57.0.0-preview.1'); assert.equal(updated, false); - assert.equal(packageJson.dependencies?.expo, '56.0.0-beta.1'); + 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', () => { @@ -54,11 +67,11 @@ test('rewrites Expo template identifiers for ExpoApp57', () => { const output = replaceTemplateAppReferences(input, 57, 'ExpoApp57'); - assert.match(output, /@callstack\/brownfield-example-expo-app-beta/); + assert.match(output, /@callstack\/brownfield-example-expo-app-preview/); assert.doesNotMatch(output, /brownfield-example-expo-app-57/); - assert.match(output, /com\.callstack\.rnbrownfield\.demo\.expobeta/); + assert.match(output, /com\.callstack\.rnbrownfield\.demo\.expoapppreview/); assert.doesNotMatch(output, /expoapp57/); - assert.match(output, /ExpoAppBeta/); + assert.match(output, /ExpoAppPreview/); assert.doesNotMatch(output, /ExpoApp57/); }); @@ -67,7 +80,7 @@ test('rewrites the home screen title for the latest Expo template version', () = const output = replaceHomeScreenTitle(input, 57); - assert.equal(output, 'Welcome to Expo Beta'); + assert.equal(output, 'Welcome to Expo Preview'); }); test('fails loudly when expo dependency is missing', () => { @@ -75,8 +88,8 @@ test('fails loudly when expo dependency is missing', () => { dependencies: {}, }; - assert.throws(() => updateExpoVersion(packageJson, '56.0.0-beta.1'), { + assert.throws(() => updateExpoVersion(packageJson, '57.0.0-preview.1'), { message: - /Could not locate dependencies\.expo in ExpoAppBeta\/package\.json/, + /Could not locate dependencies\.expo in ExpoAppPreview\/package\.json/, }); }); diff --git a/scripts/check-expo-beta.ts b/scripts/check-expo-preview.ts similarity index 72% rename from scripts/check-expo-beta.ts rename to scripts/check-expo-preview.ts index ad25624b..c9e6dbd3 100644 --- a/scripts/check-expo-beta.ts +++ b/scripts/check-expo-preview.ts @@ -32,12 +32,12 @@ export interface ExpoPackageJson { 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, +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_BETA_APP_JSON_PATH = path.join(EXPO_BETA_APP_DIR, 'app.json'); +const EXPO_PREVIEW_APP_JSON_PATH = path.join(EXPO_PREVIEW_APP_DIR, 'app.json'); const EXPO_NPM_REGISTRY_URL = 'https://registry.npmjs.org/expo'; function parseArgs(argv: string[]): CliOptions { @@ -78,7 +78,17 @@ function parseArgs(argv: string[]): CliOptions { return options; } -export async function fetchLatestExpoCanaryVersion(): Promise { +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) { @@ -86,22 +96,10 @@ export async function fetchLatestExpoCanaryVersion(): Promise { } 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; + return findLatestPreviewVersion(Object.keys(data.versions ?? {})); } function updateFileContents( @@ -148,19 +146,19 @@ export function replaceTemplateAppReferences( return contents .replaceAll( `@callstack/brownfield-example-expo-app-${version}`, - '@callstack/brownfield-example-expo-app-beta' + '@callstack/brownfield-example-expo-app-preview' ) .replaceAll( `com.callstack.rnbrownfield.demo.expoapp${version}`, - 'com.callstack.rnbrownfield.demo.expobeta' + '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/expobeta/Generated/' + './android/brownfieldlib/src/main/java/com/callstack/rnbrownfield/demo/expoapppreview/Generated/' ) - .replaceAll(templateName, 'ExpoAppBeta') - .replaceAll(`expoapp${version}`, 'expoappbeta') - .replaceAll(`expoappbeta${version}`, 'expoappbeta'); + .replaceAll(templateName, 'ExpoAppPreview') + .replaceAll(`expoapp${version}`, 'expoapppreview') + .replaceAll(`expoapppreview${version}`, 'expoapppreview'); } export function replaceHomeScreenTitle( @@ -170,11 +168,11 @@ export function replaceHomeScreenTitle( const version = templateVersion.toString(); return contents - .replaceAll(`Expo\u00a0${version}`, 'Expo\u00a0Beta') - .replaceAll(`Expo ${version}`, 'Expo Beta'); + .replaceAll(`Expo\u00a0${version}`, 'Expo\u00a0Preview') + .replaceAll(`Expo ${version}`, 'Expo Preview'); } -function generateExpoBetaApp(): void { +function generateExpoPreviewApp(): void { const templateApp = getExpoTemplateApp(); const replaceReferences = (contents: string) => replaceTemplateAppReferences( @@ -183,16 +181,16 @@ function generateExpoBetaApp(): void { templateApp.name ); - rmSync(EXPO_BETA_APP_DIR, { recursive: true, force: true }); - mkdirSync(path.dirname(EXPO_BETA_APP_DIR), { recursive: true }); - cpSync(templateApp.path, EXPO_BETA_APP_DIR, { recursive: true }); + 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_BETA_PACKAGE_JSON_PATH, replaceReferences); + updateFileContents(EXPO_PREVIEW_PACKAGE_JSON_PATH, replaceReferences); - updateFileContents(EXPO_BETA_APP_JSON_PATH, replaceReferences); + updateFileContents(EXPO_PREVIEW_APP_JSON_PATH, replaceReferences); const testPath = path.join( - EXPO_BETA_APP_DIR, + EXPO_PREVIEW_APP_DIR, '__tests__', 'brownfield.example.test.tsx' ); @@ -201,7 +199,7 @@ function generateExpoBetaApp(): void { } const homeScreenPath = path.join( - EXPO_BETA_APP_DIR, + EXPO_PREVIEW_APP_DIR, 'src', 'app', 'index.tsx' @@ -213,15 +211,15 @@ function generateExpoBetaApp(): void { } } -function readExpoBetaPackageJson(): ExpoPackageJson { +function readExpoPreviewPackageJson(): ExpoPackageJson { return JSON.parse( - readFileSync(EXPO_BETA_PACKAGE_JSON_PATH, 'utf8') + readFileSync(EXPO_PREVIEW_PACKAGE_JSON_PATH, 'utf8') ) as ExpoPackageJson; } -function writeExpoBetaPackageJson(packageJson: ExpoPackageJson): void { +function writeExpoPreviewPackageJson(packageJson: ExpoPackageJson): void { writeFileSync( - EXPO_BETA_PACKAGE_JSON_PATH, + EXPO_PREVIEW_PACKAGE_JSON_PATH, `${JSON.stringify(packageJson, null, 2)}\n`, 'utf8' ); @@ -233,7 +231,7 @@ export function updateExpoVersion( ): boolean { if (!packageJson.dependencies?.expo) { throw new Error( - 'Could not locate dependencies.expo in ExpoAppBeta/package.json' + 'Could not locate dependencies.expo in ExpoAppPreview/package.json' ); } @@ -280,13 +278,13 @@ function appendSummary( } const lines = [ - '## Expo beta check', + '## Expo preview check', '', - `- Latest Expo beta: ${latestVersion ?? 'not found'}`, - `- Last tested Expo beta: ${testedVersion ?? 'none'}`, + `- Latest Expo preview: ${latestVersion ?? 'not found'}`, + `- Last tested Expo preview: ${testedVersion ?? 'none'}`, `- Should run road tests: ${shouldTest ? 'yes' : 'no'}`, - `- ExpoAppBeta generated on-the-fly: ${generated ? 'yes' : 'no'}`, - `- ExpoAppBeta package.json updated: ${applied ? 'yes' : 'no'}`, + `- ExpoAppPreview generated on-the-fly: ${generated ? 'yes' : 'no'}`, + `- ExpoAppPreview package.json updated: ${applied ? 'yes' : 'no'}`, '', ]; @@ -296,7 +294,7 @@ function appendSummary( async function main(): Promise { const options = parseArgs(process.argv.slice(2)); const latestVersion = - options.expoVersion ?? (await fetchLatestExpoCanaryVersion()); + options.expoVersion ?? (await fetchLatestExpoPreviewVersion()); if (!latestVersion) { appendKeyValueFile(options.githubOutput, { @@ -312,7 +310,7 @@ async function main(): Promise { applied: false, generated: false, }); - console.log('No Expo beta release found'); + console.log('No Expo preview release found'); return; } @@ -321,11 +319,11 @@ async function main(): Promise { let generated = false; if (shouldTest && options.apply) { - generateExpoBetaApp(); + generateExpoPreviewApp(); generated = true; - const packageJson = readExpoBetaPackageJson(); + const packageJson = readExpoPreviewPackageJson(); applied = updateExpoVersion(packageJson, latestVersion); - writeExpoBetaPackageJson(packageJson); + writeExpoPreviewPackageJson(packageJson); } appendKeyValueFile(options.githubOutput, { @@ -343,10 +341,10 @@ async function main(): Promise { generated, }); - console.log(`Latest Expo beta: ${latestVersion}`); - console.log(`Last tested Expo beta: ${options.testedVersion ?? 'none'}`); + console.log(`Latest Expo preview: ${latestVersion}`); + console.log(`Last tested Expo preview: ${options.testedVersion ?? 'none'}`); console.log(`Should test: ${shouldTest}`); - console.log(`Generated ExpoAppBeta: ${generated}`); + console.log(`Generated ExpoAppPreview: ${generated}`); console.log(`Applied: ${applied}`); } From 46963eb222c65c636171a4c0abb9294d454422d8 Mon Sep 17 00:00:00 2001 From: artus9033 Date: Mon, 13 Jul 2026 17:37:42 +0200 Subject: [PATCH 04/10] fix: add expopreview android sourceset --- .../callstack/brownfield/android/expo/BrownfieldStore.kt | 4 ++++ .../brownfield/android/expo/ReactNativeConstants.kt | 6 ++++++ .../brownfield/android/expo/ReactNativeHostManager.kt | 3 +++ 3 files changed, 13 insertions(+) create mode 100644 apps/AndroidApp/app/src/expopreview/java/com/callstack/brownfield/android/expo/BrownfieldStore.kt create mode 100644 apps/AndroidApp/app/src/expopreview/java/com/callstack/brownfield/android/expo/ReactNativeConstants.kt create mode 100644 apps/AndroidApp/app/src/expopreview/java/com/callstack/brownfield/android/expo/ReactNativeHostManager.kt 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 From 32ca4e9c47aa7bcf00404b59fcc12ba0b3aec7df Mon Sep 17 00:00:00 2001 From: artus9033 Date: Mon, 13 Jul 2026 17:37:52 +0200 Subject: [PATCH 05/10] WIP: test the workflow on this branch --- .github/workflows/expo-preview-road-test.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/expo-preview-road-test.yml b/.github/workflows/expo-preview-road-test.yml index b718ac6f..41552639 100644 --- a/.github/workflows/expo-preview-road-test.yml +++ b/.github/workflows/expo-preview-road-test.yml @@ -2,6 +2,9 @@ name: Expo preview road test run-name: Expo preview road test on: + push: + branches: + - fix/expo-beta-test schedule: - cron: '0 6 */2 * *' workflow_dispatch: From 780b4613e9d674c391f3a88ed41544c1bc25fc51 Mon Sep 17 00:00:00 2001 From: artus9033 Date: Mon, 13 Jul 2026 19:28:45 +0200 Subject: [PATCH 06/10] fix: push brownfield.config.json to fix ExpoPreviewApp builds in CI --- scripts/__tests__/check-expo-preview.test.ts | 19 +++++++++++++++++++ scripts/check-expo-preview.ts | 8 ++++++++ 2 files changed, 27 insertions(+) diff --git a/scripts/__tests__/check-expo-preview.test.ts b/scripts/__tests__/check-expo-preview.test.ts index 7ba414d8..7dd909dd 100644 --- a/scripts/__tests__/check-expo-preview.test.ts +++ b/scripts/__tests__/check-expo-preview.test.ts @@ -75,6 +75,25 @@ test('rewrites Expo template identifiers for ExpoApp57', () => { 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('rewrites the home screen title for the latest Expo template version', () => { const input = 'Welcome to Expo 57'; diff --git a/scripts/check-expo-preview.ts b/scripts/check-expo-preview.ts index c9e6dbd3..16638c88 100644 --- a/scripts/check-expo-preview.ts +++ b/scripts/check-expo-preview.ts @@ -38,6 +38,10 @@ const EXPO_PREVIEW_PACKAGE_JSON_PATH = path.join( '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_NPM_REGISTRY_URL = 'https://registry.npmjs.org/expo'; function parseArgs(argv: string[]): CliOptions { @@ -189,6 +193,10 @@ function generateExpoPreviewApp(): void { updateFileContents(EXPO_PREVIEW_APP_JSON_PATH, replaceReferences); + if (existsSync(EXPO_PREVIEW_BROWNFIELD_CONFIG_PATH)) { + updateFileContents(EXPO_PREVIEW_BROWNFIELD_CONFIG_PATH, replaceReferences); + } + const testPath = path.join( EXPO_PREVIEW_APP_DIR, '__tests__', From e8b4195f651cc84f1e8f55e4be1c0c2ab33d5f06 Mon Sep 17 00:00:00 2001 From: artus9033 Date: Mon, 13 Jul 2026 19:33:44 +0200 Subject: [PATCH 07/10] fix: linker errors for ExpoAppPreview in AppleApp --- .../project.pbxproj | 75 ++++++++++++------- 1 file changed, 49 insertions(+), 26 deletions(-) diff --git a/apps/AppleApp/Brownfield Apple App.xcodeproj/project.pbxproj b/apps/AppleApp/Brownfield Apple App.xcodeproj/project.pbxproj index d9f80641..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; @@ -264,30 +297,9 @@ 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 (ExpoAppPreview)" 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 (ExpoAppPreview) */; - }; -/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ - /* Begin PBXFileSystemSynchronizedRootGroup section */ 793C76A92EEBF938008A2A34 /* Brownfield Apple App */ = { isa = PBXFileSystemSynchronizedRootGroup; - exceptions = ( - 7A1B2C3D4E5F60718293A203 /* Exceptions for "Brownfield Apple App" folder in "Brownfield Apple App (ExpoAppPreview)" 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; }; From b1703fc95d26db6839875dc4a1e5bfa839bd232b Mon Sep 17 00:00:00 2001 From: artus9033 Date: Mon, 13 Jul 2026 20:11:24 +0200 Subject: [PATCH 08/10] fix: align Expo preview navigation spec with Android consumer delegate --- apps/ExpoApp57/brownfield.navigation.ts | 10 +++++++ scripts/__tests__/check-expo-preview.test.ts | 24 +++++++++++++++ scripts/check-expo-preview.ts | 31 ++++++++++++++++++++ 3 files changed, 65 insertions(+) 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/scripts/__tests__/check-expo-preview.test.ts b/scripts/__tests__/check-expo-preview.test.ts index 7dd909dd..b45e78f0 100644 --- a/scripts/__tests__/check-expo-preview.test.ts +++ b/scripts/__tests__/check-expo-preview.test.ts @@ -2,6 +2,7 @@ import assert from 'node:assert/strict'; import test from 'node:test'; import { + ensureConsumerNavigationSpec, findLatestPreviewVersion, replaceHomeScreenTitle, replaceTemplateAppReferences, @@ -94,6 +95,29 @@ test('rewrites brownfield.config.json for ExpoApp57 template', () => { 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'; diff --git a/scripts/check-expo-preview.ts b/scripts/check-expo-preview.ts index 16638c88..42ea659b 100644 --- a/scripts/check-expo-preview.ts +++ b/scripts/check-expo-preview.ts @@ -42,6 +42,10 @@ 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 { @@ -176,6 +180,29 @@ export function replaceHomeScreenTitle( .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; + } + + return contents.replace( + ' navigateToReferrals(userId: string): void;\n}', + ` navigateToReferrals(userId: string): void;${CONSUMER_ROAD_TEST_NAVIGATION_METHODS}\n}` + ); +} + function generateExpoPreviewApp(): void { const templateApp = getExpoTemplateApp(); const replaceReferences = (contents: string) => @@ -197,6 +224,10 @@ function generateExpoPreviewApp(): void { 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__', From 9b6b8d62e760c25be63a5dbd72e47c6fc3911131 Mon Sep 17 00:00:00 2001 From: artus9033 Date: Tue, 14 Jul 2026 09:15:45 +0200 Subject: [PATCH 09/10] Revert "WIP: test the workflow on this branch" This reverts commit 32ca4e9c47aa7bcf00404b59fcc12ba0b3aec7df. --- .github/workflows/expo-preview-road-test.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/expo-preview-road-test.yml b/.github/workflows/expo-preview-road-test.yml index 41552639..b718ac6f 100644 --- a/.github/workflows/expo-preview-road-test.yml +++ b/.github/workflows/expo-preview-road-test.yml @@ -2,9 +2,6 @@ name: Expo preview road test run-name: Expo preview road test on: - push: - branches: - - fix/expo-beta-test schedule: - cron: '0 6 */2 * *' workflow_dispatch: From e4418d5a1db057de0765d5627045f90f51963e48 Mon Sep 17 00:00:00 2001 From: Artur Morys-Magiera Date: Tue, 14 Jul 2026 11:03:50 +0200 Subject: [PATCH 10/10] fix: harden check-expo-preview.ts Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- scripts/check-expo-preview.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/scripts/check-expo-preview.ts b/scripts/check-expo-preview.ts index 42ea659b..49fe4b1a 100644 --- a/scripts/check-expo-preview.ts +++ b/scripts/check-expo-preview.ts @@ -197,10 +197,18 @@ export function ensureConsumerNavigationSpec(contents: string): string { return contents; } - return contents.replace( - ' navigateToReferrals(userId: string): void;\n}', - ` navigateToReferrals(userId: string): void;${CONSUMER_ROAD_TEST_NAVIGATION_METHODS}\n}` + 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 {