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