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 106 process.exit(0); 107} 108 109// NOTE(EvanBacon): Squat some directory names to help with migration, 110// users can still use folders named "send" or "eject" by using the fully qualified `npx expo start ./send`. 111if (!isSubcommand) { 112 const migrationMap: Record<string, string> = { 113 init: 'npx create-expo-app', 114 eject: 'npx expo prebuild', 115 web: 'npx expo start --web', 116 'start:web': 'npx expo start --web', 117 'build:ios': 'eas build -p ios', 118 'build:android': 'eas build -p android', 119 'client:install:ios': 'npx expo start --ios', 120 'client:install:android': 'npx expo start --android', 121 doctor: 'expo-cli doctor', 122 upgrade: 'expo-cli upgrade', 123 'customize:web': 'npx expo customize', 124 125 publish: 'eas update', 126 'publish:set': 'eas update', 127 'publish:rollback': 'eas update', 128 'publish:history': 'eas update', 129 'publish:details': 'eas update', 130 131 'build:web': 'npx expo export:web', 132 133 'credentials:manager': `eas credentials`, 134 'fetch:ios:certs': `eas credentials`, 135 'fetch:android:keystore': `eas credentials`, 136 'fetch:android:hashes': `eas credentials`, 137 'fetch:android:upload-cert': `eas credentials`, 138 'push:android:upload': `eas credentials`, 139 'push:android:show': `eas credentials`, 140 'push:android:clear': `eas credentials`, 141 url: `eas build:list`, 142 'url:ipa': `eas build:list`, 143 'url:apk': `eas build:list`, 144 webhooks: `eas webhook`, 145 'webhooks:add': `eas webhook:create`, 146 'webhooks:remove': `eas webhook:delete`, 147 'webhooks:update': `eas webhook:update`, 148 149 'build:status': `eas build:list`, 150 'upload:android': `eas submit -p android`, 151 'upload:ios': `eas submit -p ios`, 152 }; 153 154 // TODO: Log telemetry about invalid command used. 155 const subcommand = args._[0]; 156 if (subcommand in migrationMap) { 157 const replacement = migrationMap[subcommand]; 158 console.log(); 159 console.log( 160 chalk.yellow` {gray $} {bold expo ${subcommand}} is not supported in the local CLI, please use {bold ${replacement}} instead` 161 ); 162 console.log(); 163 process.exit(1); 164 } 165 const deprecated = ['send', 'client:ios']; 166 if (deprecated.includes(subcommand)) { 167 console.log(); 168 console.log(chalk.yellow` {gray $} {bold expo ${subcommand}} is deprecated`); 169 console.log(); 170 process.exit(1); 171 } 172} 173 174const command = isSubcommand ? args._[0] : defaultCmd; 175const commandArgs = isSubcommand ? args._.slice(1) : args._; 176 177// Push the help flag to the subcommand args. 178if (args['--help']) { 179 commandArgs.push('--help'); 180} 181 182// Install exit hooks 183process.on('SIGINT', () => process.exit(0)); 184process.on('SIGTERM', () => process.exit(0)); 185 186commands[command]().then((exec) => { 187 exec(commandArgs); 188 189 // NOTE(EvanBacon): Track some basic telemetry events indicating the command 190 // that was run. This can be disabled with the $EXPO_NO_TELEMETRY environment variable. 191 // We do this to determine how well deprecations are going before removing a command. 192 const { logEventAsync } = 193 require('../src/utils/analytics/rudderstackClient') as typeof import('../src/utils/analytics/rudderstackClient'); 194 logEventAsync('action', { 195 action: `expo ${command}`, 196 source: 'expo/cli', 197 source_version: process.env.__EXPO_VERSION, 198 }); 199}); 200