xref: /expo/packages/@expo/cli/bin/cli.ts (revision 5cea9e5b)
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