#!/usr/bin/env yarn --silent ts-node --transpile-only import spawnAsync from '@expo/spawn-async'; import { rmSync, existsSync } from 'fs'; import fs from 'fs/promises'; import nullthrows from 'nullthrows'; import path from 'path'; /* * Change this to your own Expo account name */ export const EXPO_ACCOUNT_NAME = process.env.EXPO_ACCOUNT_NAME || 'myusername'; /** * Repository root directory */ export const repoRoot = nullthrows( process.env.EXPO_REPO_ROOT || process.env.EAS_BUILD_WORKINGDIR, 'EXPO_REPO_ROOT is not defined' ); const dirName = __dirname; /* eslint-disable-line */ // Package dependencies in chunks based on peer dependencies. function getExpoDependencyChunks({ includeDevClient, includeTV, includeSplashScreen, }: { includeDevClient: boolean; includeTV: boolean; includeSplashScreen: boolean; }) { return [ ['@expo/config-types', '@expo/env', '@expo/json-file'], ['@expo/config'], ['@expo/config-plugins'], ['@expo/plist'], ['expo-modules-core'], ['unimodules-app-loader'], ['expo-task-manager'], ['@expo/cli', 'expo', 'expo-asset', 'expo-modules-autolinking'], ['expo-manifests'], ['@expo/prebuild-config', '@expo/metro-config', 'expo-constants'], ['@expo/image-utils'], [ 'babel-preset-expo', 'expo-application', 'expo-device', 'expo-eas-client', 'expo-file-system', 'expo-font', 'expo-json-utils', 'expo-keep-awake', 'expo-status-bar', 'expo-structured-headers', 'expo-updates', 'expo-updates-interface', ], ...(includeSplashScreen ? [['expo-splash-screen']] : []), ...(includeDevClient || includeTV ? [['expo-dev-menu-interface'], ['expo-dev-menu'], ['expo-dev-launcher'], ['expo-dev-client']] : []), ...(includeTV ? [ [ 'expo-app-integrity', 'expo-audio', 'expo-av', 'expo-background-task', 'expo-blur', 'expo-crypto', 'expo-image', 'expo-image-loader', 'expo-image-manipulator', 'expo-insights', 'expo-linear-gradient', 'expo-linking', 'expo-localization', 'expo-media-library', 'expo-network', 'expo-secure-store', 'expo-sqlite', 'expo-symbols', 'expo-system-ui', 'expo-ui', 'expo-video', 'expo-video-thumbnails', ], ] : []), ]; } function getExpoDependencyNamesForDependencyChunks(expoDependencyChunks: string[][]): string[] { return expoDependencyChunks.flat(); } const expoResolutions: { [key: string]: string } = {}; /** * Executes `npm pack` on one of the Expo packages used in updates E2E * Adds a dateTime stamp to the version to ensure that it is unique and that * only this version will be used when yarn installs dependencies in the test app. */ async function packExpoDependency( repoRoot: string, projectRoot: string, destPath: string, dependencyName: string ) { // Pack up the named Expo package into the destination folder const dependencyComponents = dependencyName.split('/'); let dependencyPath: string; if (dependencyComponents[0] === '@expo') { dependencyPath = path.resolve( repoRoot, 'packages', dependencyComponents[0], dependencyComponents[1] ); } else { dependencyPath = path.resolve(repoRoot, 'packages', dependencyComponents[0]); } // Save a copy of package.json const packageJsonPath = path.resolve(dependencyPath, 'package.json'); const packageJsonCopyPath = `${packageJsonPath}-original`; await fs.copyFile(packageJsonPath, packageJsonCopyPath); // Extract the version from package.json const packageJson = require(packageJsonPath); const originalVersion = packageJson.version; // Add string to the version to ensure that yarn uses the tarball and not the published version const e2eVersion = `${originalVersion}-${new Date().getTime()}`; await fs.writeFile( packageJsonPath, JSON.stringify( { ...packageJson, version: e2eVersion, }, null, 2 ) ); let dependencyTarballPath: string; try { dependencyTarballPath = await spawnNpmPackAsync({ cwd: dependencyPath, outputDir: destPath }); } finally { // Restore the original package JSON await fs.copyFile(packageJsonCopyPath, packageJsonPath); await fs.rm(packageJsonCopyPath); } // Return the dependency in the form needed by package.json, as a relative path const dependency = `.${path.sep}${path.relative(projectRoot, dependencyTarballPath)}`; return { dependency, e2eVersion, }; } async function spawnNpmPackAsync({ cwd, outputDir, }: { cwd: string; outputDir: string; }): Promise { const { stdout } = await spawnAsync( 'npm', // Run `npm pack --json` without the script logging (see: https://github.com/npm/cli/issues/7354) ['--foreground-scripts=false', 'pack', '--json', '--pack-destination', outputDir], { cwd, stdio: 'pipe' } ); // Validate the tarball output data const output = JSON.parse(stdout); if (output.length !== 1) { throw new Error(`Expected a single tarball to be created, received: ${output.length}`); } // Return the absolute path to the tarball return path.join(outputDir, output[0].filename); } async function copyCommonFixturesToProject( projectRoot: string, fileList: string[], { appJsFileName, repoRoot, isTV = false, }: { appJsFileName: string; repoRoot: string; isTV: boolean } ) { // copy App.tsx from test fixtures const appJsSourcePath = path.resolve(dirName, '..', 'fixtures', appJsFileName); const appJsDestinationPath = path.resolve(projectRoot, 'App.tsx'); let appJsFileContents = await fs.readFile(appJsSourcePath, 'utf-8'); appJsFileContents = appJsFileContents .replace('UPDATES_HOST', nullthrows(process.env.UPDATES_HOST)) .replace('UPDATES_PORT', nullthrows(process.env.UPDATES_PORT)); await fs.writeFile(appJsDestinationPath, appJsFileContents, 'utf-8'); // pack up project files const projectFilesSourcePath = path.join(dirName, '..', 'fixtures', 'project_files'); const projectFilesTarballPath = path.join(projectRoot, 'project_files.tgz'); const tarArgs = ['zcf', projectFilesTarballPath, ...fileList]; await spawnAsync('tar', tarArgs, { cwd: projectFilesSourcePath, stdio: 'inherit', }); // unpack project files in project directory await spawnAsync('tar', ['zxf', projectFilesTarballPath], { cwd: projectRoot, stdio: 'inherit', }); // remove project files archive await fs.rm(projectFilesTarballPath); // copy .prettierrc await fs.copyFile(path.resolve(repoRoot, '.prettierrc'), path.join(projectRoot, '.prettierrc')); // Modify specific files for TV if (isTV) { // Add TV environment variable to EAS build config const easJsonPath = path.resolve(projectRoot, 'eas.json'); let easJson = require(easJsonPath); easJson = { ...easJson, build: { ...easJson.build, updates_testing_debug: { ...easJson.build.updates_testing_debug, env: { ...easJson.build.updates_testing_debug.env, TEST_TV_BUILD: '1', }, }, updates_testing_release: { ...easJson.build.updates_testing_release, env: { ...easJson.build.updates_testing_release.env, TEST_TV_BUILD: '1', }, }, }, }; await fs.rm(easJsonPath); await fs.writeFile(easJsonPath, JSON.stringify(easJson, null, 2), { encoding: 'utf-8' }); } } /** * Adds all the dependencies and other properties needed for the E2E test app */ async function preparePackageJson( projectRoot: string, repoRoot: string, configureE2E: boolean, isTV: boolean, shouldGenerateTestUpdateBundles: boolean, includeDevClient: boolean, useCustomInit: boolean ) { // Create the project subfolder to hold NPM tarballs built from the current state of the repo const dependenciesPath = path.join(projectRoot, 'dependencies'); await fs.mkdir(dependenciesPath); const allDependencyChunks = getExpoDependencyChunks({ includeDevClient, includeTV: isTV, includeSplashScreen: !useCustomInit, }); console.time('Done packing dependencies'); for (const dependencyChunk of allDependencyChunks) { await Promise.all( dependencyChunk.map(async (dependencyName) => { console.log(`Packing ${dependencyName}...`); console.time(`Packaged ${dependencyName}`); const result = await packExpoDependency( repoRoot, projectRoot, dependenciesPath, dependencyName ); expoResolutions[dependencyName] = result.dependency; console.timeEnd(`Packaged ${dependencyName}`); }) ); } console.timeEnd('Done packing dependencies'); const extraScriptsGenerateTestUpdateBundlesPart = shouldGenerateTestUpdateBundles ? { 'generate-test-update-bundles': 'npx ts-node ./scripts/generate-test-update-bundles', } : { 'generate-test-update-bundles': 'echo 1', }; const extraScriptsAssetExclusion = { 'reset-to-embedded': 'npx ts-node ./scripts/reset-app.ts App.tsx.embedded; (rm -rf android/build android/app/build)', 'set-to-update-1': 'npx ts-node ./scripts/reset-app.ts App.tsx.update1; eas update --branch=main --message=Update1', 'set-to-update-2': 'npx ts-node ./scripts/reset-app.ts App.tsx.update2; eas update --branch=main --message=Update2', }; // Additional scripts and dependencies for Maestro testing const extraScripts = configureE2E ? { start: 'expo start --private-key-path ./keys/private-key.pem', 'ios:pod-install-old-arch': 'npx pod-install', 'ios:pod-install': 'RCT_USE_PREBUILT_RNCORE=1 RCT_USE_RN_DEP=1 npx pod-install', 'maestro:android:debug:build': 'cd android; ./gradlew :app:assembleDebug; cd ..', 'maestro:android:debug:install': 'adb install android/app/build/outputs/apk/debug/app-debug.apk', 'maestro:android:release:build': 'cd android; ./gradlew :app:assembleRelease; cd ..', 'maestro:android:release:install': 'adb install android/app/build/outputs/apk/release/app-release.apk', 'maestro:android:uninstall': 'adb uninstall dev.expo.updatese2e', 'maestro:ios:debug:build': 'set -o pipefail && xcodebuild -workspace ios/updatese2e.xcworkspace -scheme updatese2e -configuration Debug -sdk iphonesimulator -arch arm64 -derivedDataPath ios/build | npx @expo/xcpretty', 'maestro:ios:debug:install': 'xcrun simctl install booted ios/build/Build/Products/Debug-iphonesimulator/updatese2e.app', 'maestro:ios:release:build': 'set -o pipefail && xcodebuild -workspace ios/updatese2e.xcworkspace -scheme updatese2e -configuration Release -sdk iphonesimulator -arch arm64 -derivedDataPath ios/build | npx @expo/xcpretty', 'maestro:ios:release:install': 'xcrun simctl install booted ios/build/Build/Products/Release-iphonesimulator/updatese2e.app', 'maestro:ios:uninstall': 'xcrun simctl uninstall booted dev.expo.updatese2e', 'eas-build-pre-install': './eas-hooks/eas-build-pre-install.sh', 'eas-build-on-success': './eas-hooks/eas-build-on-success.sh', 'check-android-emulator': 'npx ts-node ./scripts/check-android-emulator.ts', 'tvos:build': 'set -o pipefail && xcodebuild -workspace ios/updatese2e.xcworkspace -scheme updatese2e -configuration Debug -sdk appletvsimulator -arch arm64 -derivedDataPath ios/build | npx @expo/xcpretty', postinstall: 'patch-package', 'start:dev-client': 'npx expo start --private-key-path ./keys/private-key.pem > /dev/null 2>&1 &', ...extraScriptsGenerateTestUpdateBundlesPart, } : extraScriptsAssetExclusion; const extraDevDependencies = configureE2E ? { '@config-plugins/detox': '^9.0.0', '@types/express': '^5.0.3', express: '^5.1.0', 'form-data': '^4.0.0', prettier: '^2.8.1', 'patch-package': '^8.0.0', } : {}; // Remove the default Expo dependencies from create-expo-app let packageJson = JSON.parse(await fs.readFile(path.join(projectRoot, 'package.json'), 'utf-8')); for (const dependencyName of getExpoDependencyNamesForDependencyChunks(allDependencyChunks)) { if (packageJson.dependencies[dependencyName]) { delete packageJson.dependencies[dependencyName]; } } // Add dependencies and resolutions to package.json packageJson = { ...packageJson, scripts: { ...packageJson.scripts, ...extraScripts, }, dependencies: { ...expoResolutions, ...packageJson.dependencies, }, devDependencies: { '@types/react': '~19.0.10', ...extraDevDependencies, ...packageJson.devDependencies, 'ts-node': '10.9.2', typescript: '5.8.3', }, resolutions: { ...expoResolutions, ...packageJson.resolutions, typescript: '5.8.3', '@isaacs/cliui': 'npm:cliui@8.0.1', // Fix string-width ESM error }, }; if (isTV) { packageJson = { ...packageJson, dependencies: { ...packageJson.dependencies, 'react-native': 'npm:react-native-tvos@0.81.4-0', '@react-native-tvos/config-tv': '^0.1.4', }, expo: { install: { exclude: ['react-native', 'typescript'], }, }, }; } const packageJsonString = JSON.stringify(packageJson, null, 2); await fs.writeFile(path.join(projectRoot, 'package.json'), packageJsonString, 'utf-8'); } /** * Adds our E2E test native modules to both iOS and Android expo-updates code. * Returns a function that cleans up these changes to the repo once E2E setup is complete */ async function prepareLocalUpdatesModule(repoRoot: string) { // copy UpdatesE2ETest exported module into the local package const iosE2ETestModuleSwiftPath = path.join( repoRoot, 'packages', 'expo-updates', 'ios', 'EXUpdates', 'E2ETestModule.swift' ); const androidE2ETestModuleKTPath = path.join( repoRoot, 'packages', 'expo-updates', 'android', 'src', 'main', 'java', 'expo', 'modules', 'updates', 'UpdatesE2ETestModule.kt' ); await fs.copyFile( path.resolve(dirName, '..', 'fixtures', 'E2ETestModule.swift'), iosE2ETestModuleSwiftPath ); await fs.copyFile( path.resolve(dirName, '..', 'fixtures', 'UpdatesE2ETestModule.kt'), androidE2ETestModuleKTPath ); // Add E2ETestModule to expo-module.config.json const expoModuleConfigFilePath = path.join( repoRoot, 'packages', 'expo-updates', 'expo-module.config.json' ); const originalExpoModuleConfigJsonString = await fs.readFile(expoModuleConfigFilePath, 'utf-8'); const originalExpoModuleConfig = JSON.parse(originalExpoModuleConfigJsonString); const expoModuleConfig = { ...originalExpoModuleConfig, apple: { ...originalExpoModuleConfig.apple, modules: [...originalExpoModuleConfig.apple.modules, 'E2ETestModule'], }, android: { ...originalExpoModuleConfig.android, modules: [ ...originalExpoModuleConfig.android.modules, 'expo.modules.updates.UpdatesE2ETestModule', ], }, }; await fs.writeFile(expoModuleConfigFilePath, JSON.stringify(expoModuleConfig, null, 2), 'utf-8'); // Return cleanup function return async () => { await fs.writeFile(expoModuleConfigFilePath, originalExpoModuleConfigJsonString, 'utf-8'); await fs.rm(iosE2ETestModuleSwiftPath, { force: true }); await fs.rm(androidE2ETestModuleKTPath, { force: true }); }; } /** * Modifies app.json in the E2E test app to add the properties we need */ function transformAppJsonForE2E( appJson: any, projectName: string, runtimeVersion: string, isTV: boolean ) { const plugins: any[] = [ 'expo-updates', [ '@config-plugins/detox', { skipProguard: true, subdomains: Array.from(new Set(['10.0.2.2', 'localhost', process.env.UPDATES_HOST])), }, ], ]; if (isTV) { plugins.push([ '@react-native-tvos/config-tv', { isTV: true, tvosDeploymentTarget: '15.1', showVerboseWarnings: true, }, ]); } return { ...appJson, expo: { ...appJson.expo, name: projectName, runtimeVersion, plugins, newArchEnabled: true, android: { ...appJson.expo.android, package: 'dev.expo.updatese2e' }, ios: { ...appJson.expo.ios, bundleIdentifier: 'dev.expo.updatese2e' }, updates: { ...appJson.expo.updates, url: `http://${process.env.UPDATES_HOST}:${process.env.UPDATES_PORT}/update`, assetPatternsToBeBundled: ['includedAssets/*'], useNativeDebug: true, requestHeaders: { 'expo-channel-name': 'default', }, }, extra: { eas: { projectId: '55685a57-9cf3-442d-9ba8-65c7b39849ef', }, }, }, }; } export function transformAppJsonForE2EWithOldArch( appJson: any, projectName: string, runtimeVersion: string, isTV: boolean ) { const transformedForE2E = transformAppJsonForE2E(appJson, projectName, runtimeVersion, isTV); return { ...transformedForE2E, expo: { ...transformedForE2E.expo, newArchEnabled: false, }, }; } /** * Modifies app.json in the E2E test app to add the properties we need, and sets the runtime version policy to fingerprint */ export function transformAppJsonForE2EWithFingerprint( appJson: any, projectName: string, runtimeVersion: string, isTV: boolean ) { const transformedForE2E = transformAppJsonForE2EWithFallbackToCacheTimeout( appJson, projectName, runtimeVersion, isTV ); return { ...transformedForE2E, expo: { ...transformedForE2E.expo, runtimeVersion: { policy: 'fingerprint', }, }, }; } /** * Modifies app.json in the E2E test app to add the properties we need, and turns off updates native debug */ export function transformAppJsonForE2EWithDevClient( appJson: any, projectName: string, runtimeVersion: string, isTV: boolean ) { const transformedForE2E = transformAppJsonForE2EWithFallbackToCacheTimeout( appJson, projectName, runtimeVersion, isTV ); delete transformedForE2E.expo.updates.useNativeDebug; return transformedForE2E; } export function transformAppJsonForE2EWithBrickingMeasuresDisabled( appJson: any, projectName: string, runtimeVersion: string, isTV: boolean ) { const transformedForE2E = transformAppJsonForE2EWithFallbackToCacheTimeout( appJson, projectName, runtimeVersion, isTV ); return { ...transformedForE2E, expo: { ...transformedForE2E.expo, updates: { ...transformedForE2E.expo.updates, disableAntiBrickingMeasures: true, }, }, }; } /** * Modifies app.json in the E2E test app to add the properties we need, plus a fallback to cache timeout for testing startup procedure */ export function transformAppJsonForE2EWithFallbackToCacheTimeout( appJson: any, projectName: string, runtimeVersion: string, isTV: boolean ) { const transformedForE2E = transformAppJsonForE2E(appJson, projectName, runtimeVersion, isTV); return { ...transformedForE2E, expo: { ...transformedForE2E.expo, updates: { ...transformedForE2E.expo.updates, fallbackToCacheTimeout: 5000, }, }, }; } /** * Modifies app.json in the updates-disabled E2E test app to add the properties we need */ export function transformAppJsonForUpdatesDisabledE2E( appJson: any, projectName: string, runtimeVersion: string ) { const plugins: any[] = [ 'expo-updates', [ '@config-plugins/detox', { skipProguard: true, subdomains: Array.from(new Set(['10.0.2.2', 'localhost', process.env.UPDATES_HOST])), }, ], ]; return { ...appJson, expo: { ...appJson.expo, name: projectName, runtimeVersion, plugins, newArchEnabled: true, android: { ...appJson.expo.android, package: 'dev.expo.updatese2e' }, ios: { ...appJson.expo.ios, bundleIdentifier: 'dev.expo.updatese2e' }, updates: { enabled: false, useNativeDebug: true, }, extra: { eas: { projectId: '55685a57-9cf3-442d-9ba8-65c7b39849ef', }, }, }, }; } async function configureUpdatesSigningAsync(projectRoot: string) { console.time('generate and configure code signing'); // generate and configure code signing await spawnAsync( 'yarn', [ 'expo-updates', 'codesigning:generate', '--key-output-directory', 'keys', '--certificate-output-directory', 'certs', '--certificate-validity-duration-years', '1', '--certificate-common-name', 'E2E Test App', ], { cwd: projectRoot, stdio: 'inherit' } ); await spawnAsync( 'yarn', [ 'expo-updates', 'codesigning:configure', '--certificate-input-directory', 'certs', '--key-input-directory', 'keys', ], { cwd: projectRoot, stdio: 'inherit' } ); // Archive the keys so that they are not filtered out when uploading to EAS await spawnAsync('tar', ['cf', 'keys.tar', 'keys'], { cwd: projectRoot, stdio: 'inherit' }); console.timeEnd('generate and configure code signing'); } export async function initAsync( projectRoot: string, { repoRoot, runtimeVersion, localCliBin, configureE2E = true, transformAppJson = transformAppJsonForE2E, isTV = false, shouldGenerateTestUpdateBundles = true, shouldConfigureCodeSigning = true, includeDevClient = false, useCustomInit = false, }: { repoRoot: string; runtimeVersion: string; localCliBin: string; configureE2E?: boolean; transformAppJson?: ( appJson: any, projectName: string, runtimeVersion: string, isTV: boolean ) => void; isTV?: boolean; shouldGenerateTestUpdateBundles?: boolean; shouldConfigureCodeSigning?: boolean; includeDevClient?: boolean; useCustomInit?: boolean; } ) { console.log('Creating expo app'); const workingDir = path.dirname(projectRoot); const projectName = path.basename(projectRoot); if (!process.env.CI && existsSync(projectRoot)) { console.log(`Deleting existing project at ${projectRoot}...`); rmSync(projectRoot, { recursive: true, force: true }); } // pack typescript template const templateName = 'expo-template-blank-typescript'; const localTSTemplatePath = path.join(repoRoot, 'templates', templateName); const localTSTemplatePathName = await spawnNpmPackAsync({ cwd: localTSTemplatePath, outputDir: repoRoot, }); // initialize project (do not do NPM install, we do that later) await spawnAsync( 'yarn', [ 'create', 'expo-app', projectName, '--yes', '--no-install', '--template', localTSTemplatePathName, ], { cwd: workingDir, stdio: 'inherit', } ); // We are done with template tarball await fs.rm(localTSTemplatePathName); let cleanupLocalUpdatesModule: (() => Promise) | undefined; if (configureE2E) { cleanupLocalUpdatesModule = await prepareLocalUpdatesModule(repoRoot); } await preparePackageJson( projectRoot, repoRoot, configureE2E, isTV, shouldGenerateTestUpdateBundles, includeDevClient, useCustomInit ); // configure app.json let appJson = JSON.parse(await fs.readFile(path.join(projectRoot, 'app.json'), 'utf-8')); appJson = transformAppJson(appJson, projectName, runtimeVersion, isTV); await fs.writeFile(path.join(projectRoot, 'app.json'), JSON.stringify(appJson, null, 2), 'utf-8'); // Install node modules with local tarballs await spawnAsync('yarn', [], { cwd: projectRoot, stdio: 'inherit', }); if (configureE2E && shouldConfigureCodeSigning) { await configureUpdatesSigningAsync(projectRoot); } // pack local template and prebuild, but do not reinstall NPM const prebuildTemplateName = 'expo-template-bare-minimum'; const localTemplatePath = path.join(repoRoot, 'templates', prebuildTemplateName); const localTemplatePathName = await spawnNpmPackAsync({ cwd: localTemplatePath, outputDir: projectRoot, }); await spawnAsync(localCliBin, ['prebuild', '--no-install', '--template', localTemplatePathName], { env: { ...process.env, EXPO_DEBUG: '1', CI: '1', }, cwd: projectRoot, stdio: 'inherit', }); // We are done with template tarball await fs.rm(localTemplatePathName); // Restore expo dependencies after prebuild const packageJsonPath = path.resolve(projectRoot, 'package.json'); let packageJsonString = await fs.readFile(packageJsonPath, 'utf-8'); const packageJson = JSON.parse(packageJsonString); packageJson.dependencies.expo = packageJson.resolutions.expo; packageJsonString = JSON.stringify(packageJson, null, 2); await fs.rm(packageJsonPath); await fs.writeFile(packageJsonPath, packageJsonString, 'utf-8'); await spawnAsync('yarn', [], { cwd: projectRoot, stdio: 'inherit', }); // enable proguard on Android, and custom init if needed await fs.appendFile( path.join(projectRoot, 'android', 'gradle.properties'), `\nandroid.enableMinifyInReleaseBuilds=true${useCustomInit ? '\nEX_UPDATES_CUSTOM_INIT=true' : ''}`, 'utf-8' ); // Append additional Proguard rule await fs.appendFile( path.join(projectRoot, 'android', 'app', 'proguard-rules.pro'), [ '', '-keep class org.apache.commons.** { *; }', '-dontwarn androidx.appcompat.graphics.drawable.DrawableWrapper', '-dontwarn com.facebook.react.views.slider.**', '-dontwarn javax.lang.model.element.Modifier', '-dontwarn org.checkerframework.checker.nullness.qual.EnsuresNonNullIf', '-dontwarn org.checkerframework.dataflow.qual.Pure', '-keep class com.google.common.util.concurrent.ListenableFuture { *; }', '', ].join('\n'), 'utf-8' ); // Add custom init to iOS Podfile.properties.json if needed if (useCustomInit) { const podfilePropertiesJsonPath = path.join(projectRoot, 'ios', 'Podfile.properties.json'); const podfilePropertiesJsonString = await fs.readFile(podfilePropertiesJsonPath, { encoding: 'utf-8', }); const podfilePropertiesJson: any = JSON.parse(podfilePropertiesJsonString); podfilePropertiesJson.updatesCustomInit = 'true'; await fs.writeFile(podfilePropertiesJsonPath, JSON.stringify(podfilePropertiesJson, null, 2), { encoding: 'utf-8', }); } const customInitSourcesDirectory = path.join( repoRoot, 'packages', 'expo-updates', 'e2e', 'fixtures', 'custom_init' ); // If custom init, copy native source files if (useCustomInit) { const filesToCopyForCustomInit = [ { sourcePath: path.join(customInitSourcesDirectory, 'AppDelegate.swift'), destPath: path.join(projectRoot, 'ios', 'updatese2e', 'AppDelegate.swift'), }, { sourcePath: path.join(customInitSourcesDirectory, 'MainApplication.kt'), destPath: path.join( projectRoot, 'android', 'app', 'src', 'main', 'java', 'dev', 'expo', 'updatese2e', 'MainApplication.kt' ), }, { sourcePath: path.join(customInitSourcesDirectory, 'MainActivity.kt'), destPath: path.join( projectRoot, 'android', 'app', 'src', 'main', 'java', 'dev', 'expo', 'updatese2e', 'MainActivity.kt' ), }, ]; for (const fileToCopy of filesToCopyForCustomInit) { await fs.copyFile(fileToCopy.sourcePath, fileToCopy.destPath); } } // Cleanup local updates module if needed if (cleanupLocalUpdatesModule) { await cleanupLocalUpdatesModule(); } return projectRoot; } export async function setupE2EAppAsync( projectRoot: string, { localCliBin, repoRoot, isTV = false }: { localCliBin: string; repoRoot: string; isTV?: boolean } ) { await copyCommonFixturesToProject( projectRoot, ['tsconfig.json', '.env', 'eas.json', 'maestro', 'includedAssets', 'scripts'], { appJsFileName: 'App.tsx', repoRoot, isTV } ); // install extra fonts package await spawnAsync(localCliBin, ['install', '@expo-google-fonts/inter'], { cwd: projectRoot, stdio: 'inherit', }); } export async function setupManualTestAppAsync(projectRoot: string, repoRoot: string) { // Copy API test app and other fixtures to project await copyCommonFixturesToProject( projectRoot, ['tsconfig.json', 'assetsInUpdates', 'embeddedAssets', 'scripts'], { appJsFileName: 'App-apitest.tsx', repoRoot, isTV: false } ); const projectName = path.basename(projectRoot); // disable JS debugging on Android const mainApplicationPath = path.join( projectRoot, 'android', 'app', 'src', 'main', 'java', 'com', EXPO_ACCOUNT_NAME, projectName, 'MainApplication.kt' ); const mainApplicationText = await fs.readFile(mainApplicationPath, { encoding: 'utf-8' }); const mainApplicationTextModified = mainApplicationText.replace('BuildConfig.DEBUG', 'false'); await fs.writeFile(mainApplicationPath, mainApplicationTextModified, { encoding: 'utf-8' }); } export async function setupUpdatesDisabledE2EAppAsync( projectRoot: string, { localCliBin, repoRoot }: { localCliBin: string; repoRoot: string } ) { await copyCommonFixturesToProject( projectRoot, ['tsconfig.json', '.env', 'eas.json', 'maestro', 'includedAssets', 'scripts'], { appJsFileName: 'App-updates-disabled.tsx', repoRoot, isTV: false, } ); // install extra fonts package await spawnAsync(localCliBin, ['install', '@expo-google-fonts/inter'], { cwd: projectRoot, stdio: 'inherit', }); } export async function setupUpdatesErrorRecoveryE2EAppAsync( projectRoot: string, { localCliBin, repoRoot }: { localCliBin: string; repoRoot: string } ) { await copyCommonFixturesToProject( projectRoot, ['tsconfig.json', '.env', 'eas.json', 'maestro', 'includedAssets', 'scripts'], { appJsFileName: 'App.tsx', repoRoot, isTV: false } ); // install extra fonts package await spawnAsync(localCliBin, ['install', '@expo-google-fonts/inter'], { cwd: projectRoot, stdio: 'inherit', }); } export async function setupUpdatesFingerprintE2EAppAsync( projectRoot: string, { localCliBin, repoRoot }: { localCliBin: string; repoRoot: string } ) { await copyCommonFixturesToProject( projectRoot, [ 'tsconfig.json', '.fingerprintignore', '.env', 'eas.json', 'maestro', 'includedAssets', 'scripts', ], { appJsFileName: 'App.tsx', repoRoot, isTV: false } ); // install extra fonts package await spawnAsync(localCliBin, ['install', '@expo-google-fonts/inter'], { cwd: projectRoot, stdio: 'inherit', }); } export async function setupUpdatesStartupE2EAppAsync( projectRoot: string, { localCliBin, repoRoot }: { localCliBin: string; repoRoot: string } ) { await copyCommonFixturesToProject( projectRoot, ['tsconfig.json', '.env', 'eas.json', 'maestro', 'includedAssets', 'scripts'], { appJsFileName: 'App.tsx', repoRoot, isTV: false } ); // install extra fonts package await spawnAsync(localCliBin, ['install', '@expo-google-fonts/inter'], { cwd: projectRoot, stdio: 'inherit', }); } export async function setupUpdatesBrickingMeasuresDisabledE2EAppAsync( projectRoot: string, { localCliBin, repoRoot }: { localCliBin: string; repoRoot: string } ) { await copyCommonFixturesToProject( projectRoot, ['tsconfig.json', '.env', 'eas.json', 'maestro', 'includedAssets', 'scripts'], { appJsFileName: 'App.tsx', repoRoot, isTV: false } ); // install extra fonts package await spawnAsync(localCliBin, ['install', '@expo-google-fonts/inter'], { cwd: projectRoot, stdio: 'inherit', }); } export async function setupUpdatesDevClientE2EAppAsync( projectRoot: string, { localCliBin, repoRoot, isTV }: { localCliBin: string; repoRoot: string; isTV?: boolean } ) { await copyCommonFixturesToProject( projectRoot, ['tsconfig.json', '.env', 'eas.json', 'maestro', 'includedAssets', 'scripts'], { appJsFileName: 'App.tsx', repoRoot, isTV: isTV ?? false } ); // install extra fonts package await spawnAsync(localCliBin, ['install', '@expo-google-fonts/inter'], { cwd: projectRoot, stdio: 'inherit', }); }