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 // NOTE(EvanBacon): Ensure every bundler-related command sets `NODE_ENV` as expected for the command. 21 'run:ios': () => import('../src/run/ios').then((i) => i.expoRunIos), 22 'run:android': () => import('../src/run/android').then((i) => i.expoRunAndroid), 23 start: () => import('../src/start').then((i) => i.expoStart), 24 prebuild: () => import('../src/prebuild').then((i) => i.expoPrebuild), 25 config: () => import('../src/config').then((i) => i.expoConfig), 26 export: () => import('../src/export').then((i) => i.expoExport), 27 'export:web': () => import('../src/export/web').then((i) => i.expoExportWeb), 28 29 // Auxiliary commands 30 install: () => import('../src/install').then((i) => i.expoInstall), 31 customize: () => import('../src/customize').then((i) => i.expoCustomize), 32 33 // Auth 34 login: () => import('../src/login').then((i) => i.expoLogin), 35 logout: () => import('../src/logout').then((i) => i.expoLogout), 36 register: () => import('../src/register').then((i) => i.expoRegister), 37 whoami: () => import('../src/whoami').then((i) => i.expoWhoami), 38}; 39 40const args = arg( 41 { 42 // Types 43 '--version': Boolean, 44 '--help': Boolean, 45 // NOTE(EvanBacon): This is here to silence warnings from processes that 46 // expect the global expo-cli. 47 '--non-interactive': Boolean, 48 49 // Aliases 50 '-v': '--version', 51 '-h': '--help', 52 }, 53 { 54 permissive: true, 55 } 56); 57 58if (args['--version']) { 59 // Version is added in the build script. 60 console.log(process.env.__EXPO_VERSION); 61 process.exit(0); 62} 63 64if (args['--non-interactive']) { 65 console.warn(chalk.yellow` {bold --non-interactive} is not supported, use {bold $CI=1} instead`); 66} 67 68// Check if we are running `npx expo <subcommand>` or `npx expo` 69const isSubcommand = Boolean(commands[args._[0]]); 70 71// Handle `--help` flag 72if (!isSubcommand && args['--help']) { 73 const { 74 login, 75 logout, 76 whoami, 77 register, 78 start, 79 install, 80 export: _export, 81 config, 82 customize, 83 prebuild, 84 'run:ios': runIos, 85 'run:android': runAndroid, 86 ...others 87 } = commands; 88 89 console.log(chalk` 90 {bold Usage} 91 {dim $} npx expo <command> 92 93 {bold Commands} 94 ${Object.keys({ start, export: _export, ...others }).join(', ')} 95 ${Object.keys({ 'run:ios': runIos, 'run:android': runAndroid, prebuild }).join(', ')} 96 ${Object.keys({ install, customize, config }).join(', ')} 97 {dim ${Object.keys({ login, logout, whoami, register }).join(', ')}} 98 99 {bold Options} 100 --version, -v Version number 101 --help, -h Usage info 102 103 For more info run a command with the {bold --help} flag 104 {dim $} npx expo start --help 105`); 106 107 process.exit(0); 108} 109 110// NOTE(EvanBacon): Squat some directory names to help with migration, 111// users can still use folders named "send" or "eject" by using the fully qualified `npx expo start ./send`. 112if (!isSubcommand) { 113 const migrationMap: Record<string, string> = { 114 init: 'npx create-expo-app', 115 eject: 'npx expo prebuild', 116 web: 'npx expo start --web', 117 'start:web': 'npx expo start --web', 118 'build:ios': 'eas build -p ios', 119 'build:android': 'eas build -p android', 120 'client:install:ios': 'npx expo start --ios', 121 'client:install:android': 'npx expo start --android', 122 doctor: 'expo-cli doctor', 123 upgrade: 'expo-cli upgrade', 124 'customize:web': 'npx expo customize', 125 126 publish: 'eas update', 127 'publish:set': 'eas update', 128 'publish:rollback': 'eas update', 129 'publish:history': 'eas update', 130 'publish:details': 'eas update', 131 132 'build:web': 'npx expo export:web', 133 134 'credentials:manager': `eas credentials`, 135 'fetch:ios:certs': `eas credentials`, 136 'fetch:android:keystore': `eas credentials`, 137 'fetch:android:hashes': `eas credentials`, 138 'fetch:android:upload-cert': `eas credentials`, 139 'push:android:upload': `eas credentials`, 140 'push:android:show': `eas credentials`, 141 'push:android:clear': `eas credentials`, 142 url: `eas build:list`, 143 'url:ipa': `eas build:list`, 144 'url:apk': `eas build:list`, 145 webhooks: `eas webhook`, 146 'webhooks:add': `eas webhook:create`, 147 'webhooks:remove': `eas webhook:delete`, 148 'webhooks:update': `eas webhook:update`, 149 150 'build:status': `eas build:list`, 151 'upload:android': `eas submit -p android`, 152 'upload:ios': `eas submit -p ios`, 153 }; 154 155 // TODO: Log telemetry about invalid command used. 156 const subcommand = args._[0]; 157 if (subcommand in migrationMap) { 158 const replacement = migrationMap[subcommand]; 159 console.log(); 160 console.log( 161 chalk.yellow` {gray $} {bold expo ${subcommand}} is not supported in the local CLI, please use {bold ${replacement}} instead` 162 ); 163 console.log(); 164 process.exit(1); 165 } 166 const deprecated = ['send', 'client:ios']; 167 if (deprecated.includes(subcommand)) { 168 console.log(); 169 console.log(chalk.yellow` {gray $} {bold expo ${subcommand}} is deprecated`); 170 console.log(); 171 process.exit(1); 172 } 173} 174 175const command = isSubcommand ? args._[0] : defaultCmd; 176const commandArgs = isSubcommand ? args._.slice(1) : args._; 177 178// Push the help flag to the subcommand args. 179if (args['--help']) { 180 commandArgs.push('--help'); 181} 182 183// Install exit hooks 184process.on('SIGINT', () => process.exit(0)); 185process.on('SIGTERM', () => process.exit(0)); 186 187commands[command]().then((exec) => { 188 exec(commandArgs); 189 190 // NOTE(EvanBacon): Track some basic telemetry events indicating the command 191 // that was run. This can be disabled with the $EXPO_NO_TELEMETRY environment variable. 192 // We do this to determine how well deprecations are going before removing a command. 193 const { logEventAsync } = 194 require('../src/utils/analytics/rudderstackClient') as typeof import('../src/utils/analytics/rudderstackClient'); 195 logEventAsync('action', { 196 action: `expo ${command}`, 197 source: 'expo/cli', 198 source_version: process.env.__EXPO_VERSION, 199 }); 200}); 201