1#!/usr/bin/env node 2import arg from 'arg'; 3import chalk from 'chalk'; 4import Debug from 'debug'; 5import { boolish } from 'getenv'; 6 7// Setup before requiring `debug`. 8if (boolish('EXPO_DEBUG', false)) { 9 Debug.enable('expo:*'); 10} else if (Debug.enabled('expo:')) { 11 process.env.EXPO_DEBUG = '1'; 12} 13 14const defaultCmd = 'start'; 15 16export type Command = (argv?: string[]) => void; 17 18const commands: { [command: string]: () => Promise<Command> } = { 19 // Add a new command here 20 'run:ios': () => import('../src/run/ios').then((i) => i.expoRunIos), 21 'run:android': () => import('../src/run/android').then((i) => i.expoRunAndroid), 22 start: () => import('../src/start').then((i) => i.expoStart), 23 prebuild: () => import('../src/prebuild').then((i) => i.expoPrebuild), 24 config: () => import('../src/config').then((i) => i.expoConfig), 25 export: () => import('../src/export').then((i) => i.expoExport), 26 'export:web': () => import('../src/export/web').then((i) => i.expoExportWeb), 27 28 // Auxiliary commands 29 install: () => import('../src/install').then((i) => i.expoInstall), 30 customize: () => import('../src/customize').then((i) => i.expoCustomize), 31 32 // Auth 33 login: () => import('../src/login').then((i) => i.expoLogin), 34 logout: () => import('../src/logout').then((i) => i.expoLogout), 35 register: () => import('../src/register').then((i) => i.expoRegister), 36 whoami: () => import('../src/whoami').then((i) => i.expoWhoami), 37}; 38 39const args = arg( 40 { 41 // Types 42 '--version': Boolean, 43 '--help': Boolean, 44 // NOTE(EvanBacon): This is here to silence warnings from processes that 45 // expect the global expo-cli. 46 '--non-interactive': Boolean, 47 48 // Aliases 49 '-v': '--version', 50 '-h': '--help', 51 }, 52 { 53 permissive: true, 54 } 55); 56 57if (args['--version']) { 58 // Version is added in the build script. 59 console.log(process.env.__EXPO_VERSION); 60 process.exit(0); 61} 62 63if (args['--non-interactive']) { 64 console.warn(chalk.yellow` {bold --non-interactive} is not supported, use {bold $CI=1} instead`); 65} 66 67// Check if we are running `npx expo <subcommand>` or `npx expo` 68const isSubcommand = Boolean(commands[args._[0]]); 69 70// Handle `--help` flag 71if (!isSubcommand && args['--help']) { 72 const { 73 login, 74 logout, 75 whoami, 76 register, 77 start, 78 install, 79 export: _export, 80 config, 81 customize, 82 prebuild, 83 'run:ios': runIos, 84 'run:android': runAndroid, 85 ...others 86 } = commands; 87 88 console.log(chalk` 89 {bold Usage} 90 {dim $} npx expo <command> 91 92 {bold Commands} 93 ${Object.keys({ start, export: _export, ...others }).join(', ')} 94 ${Object.keys({ 'run:ios': runIos, 'run:android': runAndroid, prebuild }).join(', ')} 95 ${Object.keys({ install, customize, config }).join(', ')} 96 {dim ${Object.keys({ login, logout, whoami, register }).join(', ')}} 97 98 {bold Options} 99 --version, -v Version number 100 --help, -h Usage info 101 102 For more info run a command with the {bold --help} flag 103 {dim $} npx expo start --help 104`); 105 process.exit(0); 106} 107 108// NOTE(EvanBacon): Squat some directory names to help with migration, 109// users can still use folders named "send" or "eject" by using the fully qualified `npx expo start ./send`. 110if (!isSubcommand) { 111 const migrationMap: Record<string, string> = { 112 init: 'npx create-expo-app', 113 eject: 'npx expo prebuild', 114 web: 'npx expo start --web', 115 'start:web': 'npx expo start --web', 116 'build:ios': 'eas build -p ios', 117 'build:android': 'eas build -p android', 118 'client:install:ios': 'npx expo start --ios', 119 'client:install:android': 'npx expo start --android', 120 doctor: 'expo-cli doctor', 121 upgrade: 'expo-cli upgrade', 122 'customize:web': 'npx expo customize', 123 124 publish: 'eas update', 125 'publish:set': 'eas update', 126 'publish:rollback': 'eas update', 127 'publish:history': 'eas update', 128 'publish:details': 'eas update', 129 130 'build:web': 'npx expo export:web', 131 132 'credentials:manager': `eas credentials`, 133 'fetch:ios:certs': `eas credentials`, 134 'fetch:android:keystore': `eas credentials`, 135 'fetch:android:hashes': `eas credentials`, 136 'fetch:android:upload-cert': `eas credentials`, 137 'push:android:upload': `eas credentials`, 138 'push:android:show': `eas credentials`, 139 'push:android:clear': `eas credentials`, 140 url: `eas build:list`, 141 'url:ipa': `eas build:list`, 142 'url:apk': `eas build:list`, 143 webhooks: `eas webhook`, 144 'webhooks:add': `eas webhook:create`, 145 'webhooks:remove': `eas webhook:delete`, 146 'webhooks:update': `eas webhook:update`, 147 148 'build:status': `eas build:list`, 149 'upload:android': `eas submit -p android`, 150 'upload:ios': `eas submit -p ios`, 151 }; 152 153 const subcommand = args._[0]; 154 if (subcommand in migrationMap) { 155 const replacement = migrationMap[subcommand]; 156 console.log(); 157 console.log( 158 chalk.yellow` {gray $} {bold expo ${subcommand}} is not supported in the local CLI, please use {bold ${replacement}} instead` 159 ); 160 console.log(); 161 process.exit(1); 162 } 163 const deprecated = ['send', 'client:ios']; 164 if (deprecated.includes(subcommand)) { 165 console.log(); 166 console.log(chalk.yellow` {gray $} {bold expo ${subcommand}} is deprecated`); 167 console.log(); 168 process.exit(1); 169 } 170} 171 172const command = isSubcommand ? args._[0] : defaultCmd; 173const commandArgs = isSubcommand ? args._.slice(1) : args._; 174 175// Push the help flag to the subcommand args. 176if (args['--help']) { 177 commandArgs.push('--help'); 178} 179 180// Install exit hooks 181process.on('SIGINT', () => process.exit(0)); 182process.on('SIGTERM', () => process.exit(0)); 183 184commands[command]().then((exec) => exec(commandArgs)); 185