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