1import spawnAsync from '@expo/spawn-async'; 2import { Definitions } from 'dot'; 3import fs from 'fs'; 4import path from 'path'; 5 6import BundlerController from './BundlerController'; 7import { Application, DetoxTest } from './Config'; 8import { Platform } from './Platform'; 9import TemplateEvaluator from './TemplateEvaluator'; 10import { ProjectFile, TemplateFilesFactory, UserFile } from './TemplateFile'; 11import { killVirtualDevicesAsync } from './Utils'; 12 13export default class TemplateProject { 14 constructor( 15 protected config: Application, 16 protected name: string, 17 protected platform: Platform, 18 protected configFilePath: string 19 ) {} 20 21 getDefinitions(): Definitions { 22 return { 23 name: 'devcliente2e', 24 appEntryPoint: 'e2e/app/App', 25 }; 26 } 27 28 async createApplicationAsync(projectPath: string) { 29 // TODO: this assumes there is a parent folder 30 const parentFolder = path.resolve(projectPath, '..'); 31 if (!fs.existsSync(parentFolder)) { 32 fs.mkdirSync(parentFolder, { recursive: true }); 33 } 34 35 const appName = 'dev-client-e2e'; 36 await spawnAsync('yarn', ['create', 'expo-app', appName], { 37 stdio: 'inherit', 38 cwd: parentFolder, 39 }); 40 fs.renameSync(path.join(parentFolder, appName), projectPath); 41 42 const repoRoot = path.resolve(this.configFilePath, '..', '..', '..'); 43 const localCliBin = path.join(repoRoot, 'packages/@expo/cli/build/bin/cli'); 44 await spawnAsync(localCliBin, ['install', 'detox', 'jest'], { 45 stdio: 'inherit', 46 cwd: projectPath, 47 }); 48 49 // add local dependencies 50 let packageJson = JSON.parse(fs.readFileSync(path.join(projectPath, 'package.json'), 'utf-8')); 51 packageJson = { 52 ...packageJson, 53 dependencies: { 54 ...packageJson.dependencies, 55 'expo-dev-client': `file:${repoRoot}/packages/expo-dev-client`, 56 'expo-dev-menu-interface': `file:${repoRoot}/packages/expo-dev-menu-interface`, 57 'expo-status-bar': `file:${repoRoot}/packages/expo-status-bar`, 58 expo: `file:${repoRoot}/packages/expo`, 59 'jest-circus': packageJson.dependencies.jest, 60 }, 61 resolutions: { 62 ...packageJson.resolutions, 63 'expo-application': `file:${repoRoot}/packages/expo-application`, 64 'expo-asset': `file:${repoRoot}/packages/expo-asset`, 65 'expo-constants': `file:${repoRoot}/packages/expo-constants`, 66 'expo-dev-launcher': `file:${repoRoot}/packages/expo-dev-launcher`, 67 'expo-dev-menu': `file:${repoRoot}/packages/expo-dev-menu`, 68 'expo-file-system': `file:${repoRoot}/packages/expo-file-system`, 69 'expo-font': `file:${repoRoot}/packages/expo-font`, 70 'expo-keep-awake': `file:${repoRoot}/packages/expo-keep-awake`, 71 'expo-manifests': `file:${repoRoot}/packages/expo-manifests`, 72 'expo-modules-autolinking': `file:${repoRoot}/packages/expo-modules-autolinking`, 73 'expo-modules-core': `file:${repoRoot}/packages/expo-modules-core`, 74 'expo-updates-interface': `file:${repoRoot}/packages/expo-updates-interface`, 75 }, 76 }; 77 fs.writeFileSync( 78 path.join(projectPath, 'package.json'), 79 JSON.stringify(packageJson, null, 2), 80 'utf-8' 81 ); 82 83 // configure app.json 84 let appJson = JSON.parse(fs.readFileSync(path.join(projectPath, 'app.json'), 'utf-8')); 85 appJson = { 86 ...appJson, 87 expo: { 88 ...appJson.expo, 89 android: { ...appJson.android, package: 'com.testrunner' }, 90 ios: { ...appJson.ios, bundleIdentifier: 'com.testrunner' }, 91 }, 92 }; 93 fs.writeFileSync(path.join(projectPath, 'app.json'), JSON.stringify(appJson, null, 2), 'utf-8'); 94 95 // pack local template and prebuild 96 const localTemplatePath = path.join(repoRoot, 'templates', 'expo-template-bare-minimum'); 97 await spawnAsync('npm', ['pack', '--pack-destination', projectPath], { 98 cwd: localTemplatePath, 99 stdio: 'inherit', 100 }); 101 const templateVersion = require(path.join(localTemplatePath, 'package.json')).version; 102 103 await spawnAsync( 104 localCliBin, 105 ['prebuild', '--template', `expo-template-bare-minimum-${templateVersion}.tgz`], 106 { 107 stdio: 'inherit', 108 cwd: projectPath, 109 } 110 ); 111 112 const templateFiles = this.getTemplateFiles(); 113 await this.copyFilesAsync(projectPath, templateFiles); 114 await this.evaluateFiles(projectPath, templateFiles); 115 116 // workaround for instrumented unit test files not compiling in this 117 // configuration (ignored in .npmignore) 118 await spawnAsync('rm', ['-rf', 'node_modules/expo-dev-client/android/src/androidTest'], { 119 stdio: 'inherit', 120 cwd: projectPath, 121 }); 122 } 123 124 getTemplateFiles(): { [path: string]: ProjectFile } { 125 const tff = new TemplateFilesFactory('detox'); 126 127 const additionalFiles: { [path: string]: ProjectFile } = this.config.additionalFiles?.reduce( 128 (reducer, file) => ({ 129 ...reducer, 130 [file]: new UserFile(this.userFilePath(file)), 131 }), 132 {} 133 ); 134 135 if (this.config.android?.detoxTestFile) { 136 additionalFiles['android/app/src/androidTest/java/com/testrunner/DetoxTest.java'] = 137 new UserFile(this.userFilePath(this.config.android.detoxTestFile), Platform.Android); 138 } 139 140 return { 141 'android/build.gradle': tff.androidFile(), 142 'android/app/build.gradle': tff.androidFile(), 143 'android/app/src/androidTest/java/com/testrunner/DetoxTest.java': tff.androidFile(), 144 'android/app/src/main/java/com/testrunner/MainApplication.java': tff.androidFile(), 145 'index.js': tff.file(true), 146 'ios/devcliente2e/main.m': tff.iosFile(), 147 [this.config.detoxConfigFile]: new UserFile( 148 this.userFilePath(this.config.detoxConfigFile), 149 Platform.Both, 150 true 151 ), 152 ...additionalFiles, 153 }; 154 } 155 156 protected userFilePath(relativePath: string): string { 157 return path.join(this.configFilePath, '..', relativePath); 158 } 159 160 protected async copyFilesAsync(projectPath: string, files: { [path: string]: ProjectFile }) { 161 await Promise.all(Object.entries(files).map(([path, file]) => file.copy(projectPath, path))); 162 } 163 164 protected async evaluateFiles(projectPath: string, files: { [path: string]: ProjectFile }) { 165 const templateEvaluator = new TemplateEvaluator(this.getDefinitions()); 166 await Promise.all( 167 Object.entries(files).map(([path, file]) => 168 file.evaluate(projectPath, path, templateEvaluator) 169 ) 170 ); 171 } 172 173 async build(projectPath: string, test: DetoxTest): Promise<void> { 174 for (const conf of test.configurations) { 175 await spawnAsync('yarn', ['detox', 'build', '-c', conf], { 176 cwd: projectPath, 177 stdio: 'inherit', 178 }); 179 } 180 } 181 182 async run(projectPath: string, test: DetoxTest): Promise<void> { 183 let bundler: BundlerController | undefined; 184 try { 185 bundler = new BundlerController(projectPath); 186 187 if (test.shouldRunBundler) { 188 await bundler.start(); 189 } 190 191 for (const conf of test.configurations) { 192 await spawnAsync( 193 'yarn', 194 ['detox', 'test', '-c', conf, '--ci', '--headless', '--gpu', 'swiftshader_indirect'], 195 { 196 cwd: projectPath, 197 stdio: 'inherit', 198 } 199 ); 200 201 await killVirtualDevicesAsync(this.platform); 202 } 203 } finally { 204 // If bundler wasn't started is noop. 205 await bundler?.stop(); 206 } 207 } 208} 209