1import JsonFile from '@expo/json-file'; 2import { 3 isMultipartPartWithName, 4 parseMultipartMixedResponseAsync, 5} from '@expo/multipart-body-parser'; 6import spawnAsync from '@expo/spawn-async'; 7import { Project, UrlUtils } from '@expo/xdl'; 8import chalk from 'chalk'; 9import crypto from 'crypto'; 10import ip from 'ip'; 11import fetch, { Response } from 'node-fetch'; 12import os from 'os'; 13import path from 'path'; 14 15import { getExpoRepositoryRootDir } from '../Directories'; 16import { getHomeSDKVersionAsync } from '../ProjectVersions'; 17 18interface Manifest { 19 id: string; 20 createdAt: string; 21 runtimeVersion: string; 22 metadata: { [key: string]: string }; 23 extra: { 24 eas: { 25 projectId: string; 26 }; 27 expoClient?: { 28 name: string; 29 }; 30 }; 31} 32 33// some files are absent on turtle builders and we don't want log errors there 34const isTurtle = !!process.env.TURTLE_WORKING_DIR_PATH; 35 36const EXPO_DIR = getExpoRepositoryRootDir(); 37 38type AssetRequestHeaders = { authorization: string }; 39 40async function getManifestBodyAsync(response: Response): Promise<{ 41 manifest: Manifest; 42 assetRequestHeaders: { 43 [assetKey: string]: AssetRequestHeaders; 44 }; 45}> { 46 const contentType = response.headers.get('content-type'); 47 if (!contentType) { 48 throw new Error('The multipart manifest response is missing the content-type header'); 49 } 50 51 if (contentType === 'application/expo+json' || contentType === 'application/json') { 52 const text = await response.text(); 53 return { manifest: JSON.parse(text), assetRequestHeaders: {} }; 54 } 55 56 const bodyBuffer = await response.arrayBuffer(); 57 const multipartParts = await parseMultipartMixedResponseAsync( 58 contentType, 59 Buffer.from(bodyBuffer) 60 ); 61 62 const manifestPart = multipartParts.find((part) => isMultipartPartWithName(part, 'manifest')); 63 if (!manifestPart) { 64 throw new Error('The multipart manifest response is missing the manifest part'); 65 } 66 67 const extensionsPart = multipartParts.find((part) => isMultipartPartWithName(part, 'extensions')); 68 const assetRequestHeaders = extensionsPart 69 ? JSON.parse(extensionsPart.body).assetRequestHeaders 70 : {}; 71 72 return { manifest: JSON.parse(manifestPart.body), assetRequestHeaders }; 73} 74 75async function getManifestAsync( 76 url: string, 77 platform: string 78): Promise<{ 79 manifest: Manifest; 80 assetRequestHeaders: { 81 [assetKey: string]: AssetRequestHeaders; 82 }; 83}> { 84 const response = await fetch(url.replace('exp://', 'http://').replace('exps://', 'https://'), { 85 method: 'GET', 86 headers: { 87 accept: 'multipart/mixed,application/expo+json,application/json', 88 'expo-platform': platform, 89 }, 90 }); 91 return await getManifestBodyAsync(response); 92} 93 94async function getSavedDevHomeEASUpdateUrlAsync(): Promise<string> { 95 const devHomeConfig = await new JsonFile(path.join(EXPO_DIR, 'dev-home-config.json')).readAsync(); 96 return devHomeConfig.url as string; 97} 98 99function kernelManifestAndAssetRequestHeadersObjectToJson(obj: { 100 manifest: Manifest; 101 assetRequestHeaders: { 102 [assetKey: string]: AssetRequestHeaders; 103 }; 104}) { 105 return JSON.stringify(obj); 106} 107 108export default { 109 async TEST_APP_URI() { 110 if (process.env.TEST_SUITE_URI) { 111 return process.env.TEST_SUITE_URI; 112 } else { 113 try { 114 const testSuitePath = path.join(__dirname, '..', '..', '..', 'apps', 'test-suite'); 115 const status = await Project.currentStatus(testSuitePath); 116 if (status === 'running') { 117 return await UrlUtils.constructManifestUrlAsync(testSuitePath); 118 } else { 119 return ''; 120 } 121 } catch { 122 return ''; 123 } 124 } 125 }, 126 127 async TEST_CONFIG() { 128 if (process.env.TEST_CONFIG) { 129 return process.env.TEST_CONFIG; 130 } else { 131 return ''; 132 } 133 }, 134 135 async TEST_SERVER_URL() { 136 let url = 'TODO'; 137 138 try { 139 const lanAddress = ip.address(); 140 const localServerUrl = `http://${lanAddress}:3013`; 141 const response = await fetch(`${localServerUrl}/expo-test-server-status`, { timeout: 500 }); 142 const data = await response.text(); 143 if (data === 'running!') { 144 url = localServerUrl; 145 } 146 } catch {} 147 148 return url; 149 }, 150 151 async TEST_RUN_ID() { 152 return process.env.UNIVERSE_BUILD_ID || crypto.randomUUID(); 153 }, 154 155 async BUILD_MACHINE_LOCAL_HOSTNAME() { 156 if (process.env.SHELL_APP_BUILDER) { 157 return ''; 158 } 159 160 try { 161 const result = await spawnAsync('scutil', ['--get', 'LocalHostName']); 162 return `${result.stdout.trim()}.local`; 163 } catch (e) { 164 if (e.code !== 'ENOENT') { 165 console.error(e.stack); 166 } 167 return os.hostname(); 168 } 169 }, 170 171 async DEV_PUBLISHED_KERNEL_MANIFEST(platform) { 172 let manifestAndAssetRequestHeaders: { 173 manifest: Manifest; 174 assetRequestHeaders: { 175 [assetKey: string]: AssetRequestHeaders; 176 }; 177 }; 178 let savedDevHomeUrl: string | undefined; 179 try { 180 savedDevHomeUrl = await getSavedDevHomeEASUpdateUrlAsync(); 181 manifestAndAssetRequestHeaders = await getManifestAsync(savedDevHomeUrl, platform); 182 } catch (e) { 183 const msg = `Unable to download manifest from ${savedDevHomeUrl ?? '(error)'}: ${e.message}`; 184 console[isTurtle ? 'debug' : 'error'](msg); 185 return ''; 186 } 187 188 return kernelManifestAndAssetRequestHeadersObjectToJson(manifestAndAssetRequestHeaders); 189 }, 190 191 async BUILD_MACHINE_KERNEL_MANIFEST(platform) { 192 if (process.env.SHELL_APP_BUILDER) { 193 return ''; 194 } 195 196 if (process.env.CI) { 197 console.log('Skip fetching local manifest on CI.'); 198 return ''; 199 } 200 201 const pathToHome = 'home'; 202 const url = await UrlUtils.constructManifestUrlAsync(path.join(EXPO_DIR, pathToHome)); 203 204 try { 205 const manifestAndAssetRequestHeaders = await getManifestAsync(url, platform); 206 207 if (manifestAndAssetRequestHeaders.manifest.extra?.expoClient?.name !== 'expo-home') { 208 console.log( 209 `Manifest at ${url} is not expo-home; using published kernel manifest instead...` 210 ); 211 return ''; 212 } 213 return kernelManifestAndAssetRequestHeadersObjectToJson(manifestAndAssetRequestHeaders); 214 } catch { 215 console.error( 216 chalk.red( 217 `Unable to generate manifest from ${chalk.cyan( 218 pathToHome 219 )}: Failed to fetch manifest from ${chalk.cyan(url)}` 220 ) 221 ); 222 return ''; 223 } 224 }, 225 226 async TEMPORARY_SDK_VERSION(): Promise<string> { 227 return await getHomeSDKVersionAsync(); 228 }, 229 230 INITIAL_URL() { 231 return null; 232 }, 233}; 234