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