1/* eslint-env jest */ 2import { 3 isMultipartPartWithName, 4 parseMultipartMixedResponseAsync, 5} from '@expo/multipart-body-parser'; 6import execa from 'execa'; 7import fs from 'fs-extra'; 8import fetch from 'node-fetch'; 9import nullthrows from 'nullthrows'; 10import path from 'path'; 11 12import { 13 execute, 14 projectRoot, 15 getLoadedModulesAsync, 16 setupTestProjectAsync, 17 bin, 18 ensurePortFreeAsync, 19} from './utils'; 20 21const originalForceColor = process.env.FORCE_COLOR; 22const originalCI = process.env.CI; 23 24beforeAll(async () => { 25 await fs.mkdir(projectRoot, { recursive: true }); 26 process.env.FORCE_COLOR = '0'; 27 process.env.CI = '1'; 28}); 29 30afterAll(() => { 31 process.env.FORCE_COLOR = originalForceColor; 32 process.env.CI = originalCI; 33}); 34 35it('loads expected modules by default', async () => { 36 const modules = await getLoadedModulesAsync(`require('../../build/src/start').expoStart`); 37 expect(modules).toStrictEqual([ 38 '../node_modules/ansi-styles/index.js', 39 '../node_modules/arg/index.js', 40 '../node_modules/chalk/source/index.js', 41 '../node_modules/chalk/source/util.js', 42 '../node_modules/has-flag/index.js', 43 '../node_modules/supports-color/index.js', 44 '@expo/cli/build/src/log.js', 45 '@expo/cli/build/src/start/index.js', 46 '@expo/cli/build/src/utils/args.js', 47 '@expo/cli/build/src/utils/errors.js', 48 ]); 49}); 50 51it('runs `npx expo start --help`', async () => { 52 const results = await execute('start', '--help'); 53 expect(results.stdout).toMatchInlineSnapshot(` 54 " 55 Info 56 Start a local dev server for the app 57 58 Usage 59 $ npx expo start <dir> 60 61 Options 62 <dir> Directory of the Expo project. Default: Current working directory 63 -a, --android Opens your app in Expo Go on a connected Android device 64 -i, --ios Opens your app in Expo Go in a currently running iOS simulator on your computer 65 -w, --web Opens your app in a web browser 66 67 -c, --clear Clear the bundler cache 68 --max-workers <num> Maximum number of tasks to allow Metro to spawn 69 --no-dev Bundle in production mode 70 --minify Minify JavaScript 71 72 -m, --host <mode> Dev server hosting type. Default: lan 73 lan: Use the local network 74 tunnel: Use any network by tunnel through ngrok 75 localhost: Connect to the dev server over localhost 76 --tunnel Same as --host tunnel 77 --lan Same as --host lan 78 --localhost Same as --host localhost 79 80 --offline Skip network requests and use anonymous manifest signatures 81 --https Start the dev server with https protocol 82 --scheme <scheme> Custom URI protocol to use when launching an app 83 -p, --port <port> Port to start the dev server on (does not apply to web or tunnel). Default: 19000 84 85 --dev-client Experimental: Starts the bundler for use with the expo-development-client 86 --force-manifest-type <manifest-type> Override auto detection of manifest type 87 --private-key-path <path> Path to private key for code signing. Default: "private-key.pem" in the same directory as the certificate specified by the expo-updates configuration in app.json. 88 -h, --help Usage info 89 " 90 `); 91}); 92 93for (const args of [ 94 ['--lan', '--tunnel'], 95 ['--offline', '--localhost'], 96 ['--host', 'localhost', '--tunnel'], 97 ['-m', 'localhost', '--lan', '--offline'], 98]) { 99 it(`asserts invalid URL arguments on \`expo start ${args.join(' ')}\``, async () => { 100 await expect(execa('node', [bin, 'start', ...args], { cwd: projectRoot })).rejects.toThrowError( 101 /Specify at most one of/ 102 ); 103 }); 104} 105 106describe('server', () => { 107 // Kill port 108 const kill = () => ensurePortFreeAsync(19000); 109 110 beforeEach(async () => { 111 await kill(); 112 }); 113 114 afterAll(async () => { 115 await kill(); 116 }); 117 it( 118 'runs `npx expo start`', 119 async () => { 120 const projectRoot = await setupTestProjectAsync('basic-start', 'with-blank'); 121 await fs.remove(path.join(projectRoot, '.expo')); 122 123 const promise = execa('node', [bin, 'start'], { cwd: projectRoot }); 124 125 console.log('Starting server'); 126 127 await new Promise<void>((resolve, reject) => { 128 promise.on('close', (code: number) => { 129 reject( 130 code === 0 131 ? 'Server closed too early. Run `kill -9 $(lsof -ti:19000)` to kill the orphaned process.' 132 : code 133 ); 134 }); 135 136 promise.stdout?.on('data', (data) => { 137 const stdout = data.toString(); 138 console.log('output:', stdout); 139 if (stdout.includes('Logs for your project')) { 140 resolve(); 141 } 142 }); 143 }); 144 145 console.log('Fetching manifest'); 146 const response = await fetch('http://localhost:19000/', { 147 headers: { 148 'expo-platform': 'ios', 149 Accept: 'multipart/mixed', 150 }, 151 }); 152 153 const multipartParts = await parseMultipartMixedResponseAsync( 154 response.headers.get('content-type') as string, 155 await response.buffer() 156 ); 157 const manifestPart = nullthrows( 158 multipartParts.find((part) => isMultipartPartWithName(part, 'manifest')) 159 ); 160 161 const manifest = JSON.parse(manifestPart.body); 162 163 // Required for Expo Go 164 expect(manifest.extra.expoGo.packagerOpts).toStrictEqual({ 165 dev: true, 166 }); 167 expect(manifest.extra.expoGo.developer).toStrictEqual({ 168 projectRoot: expect.anything(), 169 tool: 'expo-cli', 170 }); 171 172 // URLs 173 expect(manifest.launchAsset.url).toBe( 174 'http://127.0.0.1:19000/node_modules/expo/AppEntry.bundle?platform=ios&dev=true&hot=false' 175 ); 176 expect(manifest.extra.expoGo.debuggerHost).toBe('127.0.0.1:19000'); 177 expect(manifest.extra.expoGo.logUrl).toBe('http://127.0.0.1:19000/logs'); 178 expect(manifest.extra.expoGo.mainModuleName).toBe('node_modules/expo/AppEntry'); 179 expect(manifest.extra.expoClient.hostUri).toBe('127.0.0.1:19000'); 180 181 // Manifest 182 expect(manifest.runtimeVersion).toBe('exposdk:47.0.0'); 183 expect(manifest.extra.expoClient.sdkVersion).toBe('47.0.0'); 184 expect(manifest.extra.expoClient.slug).toBe('basic-start'); 185 expect(manifest.extra.expoClient.name).toBe('basic-start'); 186 187 // Custom 188 expect(manifest.extra.expoGo.__flipperHack).toBe('React Native packager is running'); 189 190 console.log('Fetching bundle'); 191 const bundle = await fetch(manifest.launchAsset.url).then((res) => res.text()); 192 console.log('Fetched bundle: ', bundle.length); 193 expect(bundle.length).toBeGreaterThan(1000); 194 console.log('Finished'); 195 196 // Kill process. 197 promise.kill('SIGTERM'); 198 199 await promise; 200 }, 201 // Could take 45s depending on how fast npm installs 202 120 * 1000 203 ); 204}); 205