xref: /expo/packages/@expo/cli/bin/cli.ts (revision 1a3a1db5)
18d307f52SEvan Bacon#!/usr/bin/env node
28d307f52SEvan Baconimport arg from 'arg';
38d307f52SEvan Baconimport chalk from 'chalk';
42ff06c55SEvan Baconimport Debug from 'debug';
52ff06c55SEvan Baconimport { boolish } from 'getenv';
62ff06c55SEvan Bacon
72ff06c55SEvan Bacon// Setup before requiring `debug`.
82ff06c55SEvan Baconif (boolish('EXPO_DEBUG', false)) {
92ff06c55SEvan Bacon  Debug.enable('expo:*');
102ff06c55SEvan Bacon} else if (Debug.enabled('expo:')) {
112ff06c55SEvan Bacon  process.env.EXPO_DEBUG = '1';
122ff06c55SEvan Bacon}
138d307f52SEvan Bacon
148d307f52SEvan Baconconst defaultCmd = 'start';
158d307f52SEvan Bacon
168d307f52SEvan Baconexport type Command = (argv?: string[]) => void;
178d307f52SEvan Bacon
188d307f52SEvan Baconconst commands: { [command: string]: () => Promise<Command> } = {
198d307f52SEvan Bacon  // Add a new command here
202dd43328SEvan Bacon  // NOTE(EvanBacon): Ensure every bundler-related command sets `NODE_ENV` as expected for the command.
21*1a3a1db5SEvan Bacon  'run:ios': () => import('../src/run/ios/index.js').then((i) => i.expoRunIos),
22*1a3a1db5SEvan Bacon  'run:android': () => import('../src/run/android/index.js').then((i) => i.expoRunAndroid),
23*1a3a1db5SEvan Bacon  start: () => import('../src/start/index.js').then((i) => i.expoStart),
24*1a3a1db5SEvan Bacon  prebuild: () => import('../src/prebuild/index.js').then((i) => i.expoPrebuild),
25*1a3a1db5SEvan Bacon  config: () => import('../src/config/index.js').then((i) => i.expoConfig),
26*1a3a1db5SEvan Bacon  export: () => import('../src/export/index.js').then((i) => i.expoExport),
27*1a3a1db5SEvan Bacon  'export:web': () => import('../src/export/web/index.js').then((i) => i.expoExportWeb),
28*1a3a1db5SEvan Bacon  'export:embed': () => import('../src/export/embed/index.js').then((i) => i.expoExportEmbed),
2909bb6093SEvan Bacon
3009bb6093SEvan Bacon  // Auxiliary commands
31*1a3a1db5SEvan Bacon  install: () => import('../src/install/index.js').then((i) => i.expoInstall),
32*1a3a1db5SEvan Bacon  add: () => import('../src/install/index.js').then((i) => i.expoInstall),
33*1a3a1db5SEvan Bacon  customize: () => import('../src/customize/index.js').then((i) => i.expoCustomize),
3409bb6093SEvan Bacon
358d307f52SEvan Bacon  // Auth
36*1a3a1db5SEvan Bacon  login: () => import('../src/login/index.js').then((i) => i.expoLogin),
37*1a3a1db5SEvan Bacon  logout: () => import('../src/logout/index.js').then((i) => i.expoLogout),
38*1a3a1db5SEvan Bacon  register: () => import('../src/register/index.js').then((i) => i.expoRegister),
39*1a3a1db5SEvan Bacon  whoami: () => import('../src/whoami/index.js').then((i) => i.expoWhoami),
408d307f52SEvan Bacon};
418d307f52SEvan Bacon
428d307f52SEvan Baconconst args = arg(
438d307f52SEvan Bacon  {
448d307f52SEvan Bacon    // Types
458d307f52SEvan Bacon    '--version': Boolean,
468d307f52SEvan Bacon    '--help': Boolean,
47120fe602SEvan Bacon    // NOTE(EvanBacon): This is here to silence warnings from processes that
48120fe602SEvan Bacon    // expect the global expo-cli.
49120fe602SEvan Bacon    '--non-interactive': Boolean,
508d307f52SEvan Bacon
518d307f52SEvan Bacon    // Aliases
528d307f52SEvan Bacon    '-v': '--version',
538d307f52SEvan Bacon    '-h': '--help',
548d307f52SEvan Bacon  },
558d307f52SEvan Bacon  {
568d307f52SEvan Bacon    permissive: true,
578d307f52SEvan Bacon  }
588d307f52SEvan Bacon);
598d307f52SEvan Bacon
608d307f52SEvan Baconif (args['--version']) {
618d307f52SEvan Bacon  // Version is added in the build script.
628d307f52SEvan Bacon  console.log(process.env.__EXPO_VERSION);
638d307f52SEvan Bacon  process.exit(0);
648d307f52SEvan Bacon}
658d307f52SEvan Bacon
66120fe602SEvan Baconif (args['--non-interactive']) {
67120fe602SEvan Bacon  console.warn(chalk.yellow`  {bold --non-interactive} is not supported, use {bold $CI=1} instead`);
68120fe602SEvan Bacon}
69120fe602SEvan Bacon
708d307f52SEvan Bacon// Check if we are running `npx expo <subcommand>` or `npx expo`
718d307f52SEvan Baconconst isSubcommand = Boolean(commands[args._[0]]);
728d307f52SEvan Bacon
738d307f52SEvan Bacon// Handle `--help` flag
748d307f52SEvan Baconif (!isSubcommand && args['--help']) {
7583d464dcSEvan Bacon  const {
7683d464dcSEvan Bacon    login,
7783d464dcSEvan Bacon    logout,
7883d464dcSEvan Bacon    whoami,
7983d464dcSEvan Bacon    register,
8083d464dcSEvan Bacon    start,
8183d464dcSEvan Bacon    install,
821971907aSEvan Bacon    add,
8383d464dcSEvan Bacon    export: _export,
8483d464dcSEvan Bacon    config,
85dfe12d45SEvan Bacon    customize,
863d6e487dSEvan Bacon    prebuild,
87c4ef02aeSEvan Bacon    'run:ios': runIos,
883d6e487dSEvan Bacon    'run:android': runAndroid,
89b6b91c50SEvan Bacon    // NOTE(EvanBacon): Don't document this command as it's a temporary
90b6b91c50SEvan Bacon    // workaround until we can use `expo export` for all production bundling.
91b6b91c50SEvan Bacon    // https://github.com/expo/expo/pull/21396/files#r1121025873
92b6b91c50SEvan Bacon    'export:embed': exportEmbed_unused,
9383d464dcSEvan Bacon    ...others
9483d464dcSEvan Bacon  } = commands;
9583d464dcSEvan Bacon
968d307f52SEvan Bacon  console.log(chalk`
978d307f52SEvan Bacon  {bold Usage}
9883d464dcSEvan Bacon    {dim $} npx expo <command>
998d307f52SEvan Bacon
10083d464dcSEvan Bacon  {bold Commands}
101dfe12d45SEvan Bacon    ${Object.keys({ start, export: _export, ...others }).join(', ')}
102c4ef02aeSEvan Bacon    ${Object.keys({ 'run:ios': runIos, 'run:android': runAndroid, prebuild }).join(', ')}
103dfe12d45SEvan Bacon    ${Object.keys({ install, customize, config }).join(', ')}
10483d464dcSEvan Bacon    {dim ${Object.keys({ login, logout, whoami, register }).join(', ')}}
1058d307f52SEvan Bacon
1068d307f52SEvan Bacon  {bold Options}
1078d307f52SEvan Bacon    --version, -v   Version number
10883d464dcSEvan Bacon    --help, -h      Usage info
1098d307f52SEvan Bacon
11083d464dcSEvan Bacon  For more info run a command with the {bold --help} flag
11183d464dcSEvan Bacon    {dim $} npx expo start --help
1128d307f52SEvan Bacon`);
113ea99eec9SEvan Bacon
1148d307f52SEvan Bacon  process.exit(0);
1158d307f52SEvan Bacon}
1168d307f52SEvan Bacon
1174b86b89eSEvan Bacon// NOTE(EvanBacon): Squat some directory names to help with migration,
1184b86b89eSEvan Bacon// users can still use folders named "send" or "eject" by using the fully qualified `npx expo start ./send`.
1194b86b89eSEvan Baconif (!isSubcommand) {
1204b86b89eSEvan Bacon  const migrationMap: Record<string, string> = {
1214b86b89eSEvan Bacon    init: 'npx create-expo-app',
1224b86b89eSEvan Bacon    eject: 'npx expo prebuild',
123120fe602SEvan Bacon    web: 'npx expo start --web',
1244b86b89eSEvan Bacon    'start:web': 'npx expo start --web',
1254b86b89eSEvan Bacon    'build:ios': 'eas build -p ios',
1264b86b89eSEvan Bacon    'build:android': 'eas build -p android',
1274b86b89eSEvan Bacon    'client:install:ios': 'npx expo start --ios',
1284b86b89eSEvan Bacon    'client:install:android': 'npx expo start --android',
129c3d7da5bSKeith Kurak    doctor: 'npx expo-doctor',
13032ffbf34SEvan Bacon    upgrade: 'expo-cli upgrade',
1314b86b89eSEvan Bacon    'customize:web': 'npx expo customize',
1324b86b89eSEvan Bacon
1334b86b89eSEvan Bacon    publish: 'eas update',
1344b86b89eSEvan Bacon    'publish:set': 'eas update',
1354b86b89eSEvan Bacon    'publish:rollback': 'eas update',
1364b86b89eSEvan Bacon    'publish:history': 'eas update',
1374b86b89eSEvan Bacon    'publish:details': 'eas update',
1384b86b89eSEvan Bacon
13932ffbf34SEvan Bacon    'build:web': 'npx expo export:web',
1404b86b89eSEvan Bacon
1414b86b89eSEvan Bacon    'credentials:manager': `eas credentials`,
1424b86b89eSEvan Bacon    'fetch:ios:certs': `eas credentials`,
1434b86b89eSEvan Bacon    'fetch:android:keystore': `eas credentials`,
1444b86b89eSEvan Bacon    'fetch:android:hashes': `eas credentials`,
1454b86b89eSEvan Bacon    'fetch:android:upload-cert': `eas credentials`,
1464b86b89eSEvan Bacon    'push:android:upload': `eas credentials`,
1474b86b89eSEvan Bacon    'push:android:show': `eas credentials`,
1484b86b89eSEvan Bacon    'push:android:clear': `eas credentials`,
1494b86b89eSEvan Bacon    url: `eas build:list`,
1504b86b89eSEvan Bacon    'url:ipa': `eas build:list`,
1514b86b89eSEvan Bacon    'url:apk': `eas build:list`,
1524b86b89eSEvan Bacon    webhooks: `eas webhook`,
1534b86b89eSEvan Bacon    'webhooks:add': `eas webhook:create`,
1544b86b89eSEvan Bacon    'webhooks:remove': `eas webhook:delete`,
1554b86b89eSEvan Bacon    'webhooks:update': `eas webhook:update`,
1564b86b89eSEvan Bacon
1574b86b89eSEvan Bacon    'build:status': `eas build:list`,
1584b86b89eSEvan Bacon    'upload:android': `eas submit -p android`,
1594b86b89eSEvan Bacon    'upload:ios': `eas submit -p ios`,
1604b86b89eSEvan Bacon  };
1614b86b89eSEvan Bacon
162ea99eec9SEvan Bacon  // TODO: Log telemetry about invalid command used.
1634b86b89eSEvan Bacon  const subcommand = args._[0];
1644b86b89eSEvan Bacon  if (subcommand in migrationMap) {
1654b86b89eSEvan Bacon    const replacement = migrationMap[subcommand];
1664b86b89eSEvan Bacon    console.log();
1674b86b89eSEvan Bacon    console.log(
1684b86b89eSEvan Bacon      chalk.yellow`  {gray $} {bold expo ${subcommand}} is not supported in the local CLI, please use {bold ${replacement}} instead`
1694b86b89eSEvan Bacon    );
1704b86b89eSEvan Bacon    console.log();
1714b86b89eSEvan Bacon    process.exit(1);
1724b86b89eSEvan Bacon  }
1734b86b89eSEvan Bacon  const deprecated = ['send', 'client:ios'];
1744b86b89eSEvan Bacon  if (deprecated.includes(subcommand)) {
1754b86b89eSEvan Bacon    console.log();
1764b86b89eSEvan Bacon    console.log(chalk.yellow`  {gray $} {bold expo ${subcommand}} is deprecated`);
1774b86b89eSEvan Bacon    console.log();
1784b86b89eSEvan Bacon    process.exit(1);
1794b86b89eSEvan Bacon  }
1804b86b89eSEvan Bacon}
1814b86b89eSEvan Bacon
1828d307f52SEvan Baconconst command = isSubcommand ? args._[0] : defaultCmd;
1838d307f52SEvan Baconconst commandArgs = isSubcommand ? args._.slice(1) : args._;
1848d307f52SEvan Bacon
1858d307f52SEvan Bacon// Push the help flag to the subcommand args.
1868d307f52SEvan Baconif (args['--help']) {
1878d307f52SEvan Bacon  commandArgs.push('--help');
1888d307f52SEvan Bacon}
1898d307f52SEvan Bacon
1908d307f52SEvan Bacon// Install exit hooks
1918d307f52SEvan Baconprocess.on('SIGINT', () => process.exit(0));
1928d307f52SEvan Baconprocess.on('SIGTERM', () => process.exit(0));
1938d307f52SEvan Bacon
194ea99eec9SEvan Baconcommands[command]().then((exec) => {
195ea99eec9SEvan Bacon  exec(commandArgs);
196ea99eec9SEvan Bacon
197ea99eec9SEvan Bacon  // NOTE(EvanBacon): Track some basic telemetry events indicating the command
198ea99eec9SEvan Bacon  // that was run. This can be disabled with the $EXPO_NO_TELEMETRY environment variable.
199ea99eec9SEvan Bacon  // We do this to determine how well deprecations are going before removing a command.
200ea99eec9SEvan Bacon  const { logEventAsync } =
201ea99eec9SEvan Bacon    require('../src/utils/analytics/rudderstackClient') as typeof import('../src/utils/analytics/rudderstackClient');
202ea99eec9SEvan Bacon  logEventAsync('action', {
203ea99eec9SEvan Bacon    action: `expo ${command}`,
204ea99eec9SEvan Bacon    source: 'expo/cli',
205ea99eec9SEvan Bacon    source_version: process.env.__EXPO_VERSION,
206ea99eec9SEvan Bacon  });
207ea99eec9SEvan Bacon});
208