18d3f3824SCedric van Puttenimport spawnAsync, { SpawnOptions, SpawnResult } from '@expo/spawn-async';
28d3f3824SCedric van Puttenimport chalk from 'chalk';
38d3f3824SCedric van Puttenimport { existsSync } from 'fs';
48d3f3824SCedric van Puttenimport { Ora } from 'ora';
58d3f3824SCedric van Puttenimport os from 'os';
68d3f3824SCedric van Puttenimport path from 'path';
78d3f3824SCedric van Putten
88d3f3824SCedric van Puttenimport { spawnSudoAsync } from '../utils/spawn';
98d3f3824SCedric van Putten
108d3f3824SCedric van Puttenexport type CocoaPodsErrorCode = 'NON_INTERACTIVE' | 'NO_CLI' | 'COMMAND_FAILED';
118d3f3824SCedric van Putten
128d3f3824SCedric van Puttenexport class CocoaPodsError extends Error {
138d3f3824SCedric van Putten  readonly name = 'CocoaPodsError';
148d3f3824SCedric van Putten  readonly isPackageManagerError = true;
158d3f3824SCedric van Putten
16*8a424bebSJames Ide  constructor(
17*8a424bebSJames Ide    message: string,
18*8a424bebSJames Ide    public code: CocoaPodsErrorCode,
19*8a424bebSJames Ide    public cause?: Error
20*8a424bebSJames Ide  ) {
218d3f3824SCedric van Putten    super(cause ? `${message}\n└─ Cause: ${cause.message}` : message);
228d3f3824SCedric van Putten  }
238d3f3824SCedric van Putten}
248d3f3824SCedric van Putten
258d3f3824SCedric van Puttenexport function extractMissingDependencyError(errorOutput: string): [string, string] | null {
268d3f3824SCedric van Putten  // [!] Unable to find a specification for `expo-dev-menu-interface` depended upon by `expo-dev-launcher`
278d3f3824SCedric van Putten  const results = errorOutput.match(
288d3f3824SCedric van Putten    /Unable to find a specification for ['"`]([\w-_\d\s]+)['"`] depended upon by ['"`]([\w-_\d\s]+)['"`]/
298d3f3824SCedric van Putten  );
308d3f3824SCedric van Putten  if (results) {
318d3f3824SCedric van Putten    return [results[1], results[2]];
328d3f3824SCedric van Putten  }
338d3f3824SCedric van Putten  return null;
348d3f3824SCedric van Putten}
358d3f3824SCedric van Putten
368d3f3824SCedric van Puttenexport class CocoaPodsPackageManager {
378d3f3824SCedric van Putten  options: SpawnOptions;
388d3f3824SCedric van Putten
398d3f3824SCedric van Putten  private silent: boolean;
408d3f3824SCedric van Putten
418d3f3824SCedric van Putten  static getPodProjectRoot(projectRoot: string): string | null {
428d3f3824SCedric van Putten    if (CocoaPodsPackageManager.isUsingPods(projectRoot)) return projectRoot;
438d3f3824SCedric van Putten    const iosProject = path.join(projectRoot, 'ios');
448d3f3824SCedric van Putten    if (CocoaPodsPackageManager.isUsingPods(iosProject)) return iosProject;
458d3f3824SCedric van Putten    const macOsProject = path.join(projectRoot, 'macos');
468d3f3824SCedric van Putten    if (CocoaPodsPackageManager.isUsingPods(macOsProject)) return macOsProject;
478d3f3824SCedric van Putten    return null;
488d3f3824SCedric van Putten  }
498d3f3824SCedric van Putten
508d3f3824SCedric van Putten  static isUsingPods(projectRoot: string): boolean {
518d3f3824SCedric van Putten    return existsSync(path.join(projectRoot, 'Podfile'));
528d3f3824SCedric van Putten  }
538d3f3824SCedric van Putten
548d3f3824SCedric van Putten  static async gemInstallCLIAsync(
558d3f3824SCedric van Putten    nonInteractive: boolean = false,
568d3f3824SCedric van Putten    spawnOptions: SpawnOptions = { stdio: 'inherit' }
578d3f3824SCedric van Putten  ): Promise<void> {
588d3f3824SCedric van Putten    const options = ['install', 'cocoapods', '--no-document'];
598d3f3824SCedric van Putten
608d3f3824SCedric van Putten    try {
618d3f3824SCedric van Putten      // In case the user has run sudo before running the command we can properly install CocoaPods without prompting for an interaction.
628d3f3824SCedric van Putten      await spawnAsync('gem', options, spawnOptions);
638d3f3824SCedric van Putten    } catch (error: any) {
648d3f3824SCedric van Putten      if (nonInteractive) {
658d3f3824SCedric van Putten        throw new CocoaPodsError(
668d3f3824SCedric van Putten          'Failed to install CocoaPods CLI with gem (recommended)',
678d3f3824SCedric van Putten          'COMMAND_FAILED',
688d3f3824SCedric van Putten          error
698d3f3824SCedric van Putten        );
708d3f3824SCedric van Putten      }
718d3f3824SCedric van Putten      // If the user doesn't have permission then we can prompt them to use sudo.
728d3f3824SCedric van Putten      await spawnSudoAsync(['gem', ...options], spawnOptions);
738d3f3824SCedric van Putten    }
748d3f3824SCedric van Putten  }
758d3f3824SCedric van Putten
768d3f3824SCedric van Putten  static async brewLinkCLIAsync(spawnOptions: SpawnOptions = { stdio: 'inherit' }): Promise<void> {
778d3f3824SCedric van Putten    await spawnAsync('brew', ['link', 'cocoapods'], spawnOptions);
788d3f3824SCedric van Putten  }
798d3f3824SCedric van Putten
808d3f3824SCedric van Putten  static async brewInstallCLIAsync(
818d3f3824SCedric van Putten    spawnOptions: SpawnOptions = { stdio: 'inherit' }
828d3f3824SCedric van Putten  ): Promise<void> {
838d3f3824SCedric van Putten    await spawnAsync('brew', ['install', 'cocoapods'], spawnOptions);
848d3f3824SCedric van Putten  }
858d3f3824SCedric van Putten
868d3f3824SCedric van Putten  static async installCLIAsync({
878d3f3824SCedric van Putten    nonInteractive = false,
888d3f3824SCedric van Putten    spawnOptions = { stdio: 'inherit' },
898d3f3824SCedric van Putten  }: {
908d3f3824SCedric van Putten    nonInteractive?: boolean;
918d3f3824SCedric van Putten    spawnOptions?: SpawnOptions;
928d3f3824SCedric van Putten  }): Promise<boolean> {
938d3f3824SCedric van Putten    if (!spawnOptions) {
948d3f3824SCedric van Putten      spawnOptions = { stdio: 'inherit' };
958d3f3824SCedric van Putten    }
968d3f3824SCedric van Putten    const silent = !!spawnOptions.ignoreStdio;
978d3f3824SCedric van Putten
988d3f3824SCedric van Putten    try {
998d3f3824SCedric van Putten      !silent && console.log(`\u203A Attempting to install CocoaPods CLI with Gem`);
1008d3f3824SCedric van Putten      await CocoaPodsPackageManager.gemInstallCLIAsync(nonInteractive, spawnOptions);
1018d3f3824SCedric van Putten      !silent && console.log(`\u203A Successfully installed CocoaPods CLI with Gem`);
1028d3f3824SCedric van Putten      return true;
1038d3f3824SCedric van Putten    } catch (error: any) {
1048d3f3824SCedric van Putten      if (!silent) {
1058d3f3824SCedric van Putten        console.log(chalk.yellow(`\u203A Failed to install CocoaPods CLI with Gem`));
1068d3f3824SCedric van Putten        console.log(chalk.red(error.stderr ?? error.message));
1078d3f3824SCedric van Putten        console.log(`\u203A Attempting to install CocoaPods CLI with Homebrew`);
1088d3f3824SCedric van Putten      }
1098d3f3824SCedric van Putten      try {
1108d3f3824SCedric van Putten        await CocoaPodsPackageManager.brewInstallCLIAsync(spawnOptions);
1118d3f3824SCedric van Putten        if (!(await CocoaPodsPackageManager.isCLIInstalledAsync(spawnOptions))) {
1128d3f3824SCedric van Putten          try {
1138d3f3824SCedric van Putten            await CocoaPodsPackageManager.brewLinkCLIAsync(spawnOptions);
1148d3f3824SCedric van Putten            // Still not available after linking? Bail out
1158d3f3824SCedric van Putten            if (!(await CocoaPodsPackageManager.isCLIInstalledAsync(spawnOptions))) {
1168d3f3824SCedric van Putten              throw new CocoaPodsError(
1178d3f3824SCedric van Putten                'CLI could not be installed automatically with gem or Homebrew, please install CocoaPods manually and try again',
1188d3f3824SCedric van Putten                'NO_CLI',
1198d3f3824SCedric van Putten                error
1208d3f3824SCedric van Putten              );
1218d3f3824SCedric van Putten            }
1228d3f3824SCedric van Putten          } catch (error: any) {
1238d3f3824SCedric van Putten            throw new CocoaPodsError(
1248d3f3824SCedric van Putten              'Homebrew installation appeared to succeed but CocoaPods CLI not found in PATH and unable to link.',
1258d3f3824SCedric van Putten              'NO_CLI',
1268d3f3824SCedric van Putten              error
1278d3f3824SCedric van Putten            );
1288d3f3824SCedric van Putten          }
1298d3f3824SCedric van Putten        }
1308d3f3824SCedric van Putten
1318d3f3824SCedric van Putten        !silent && console.log(`\u203A Successfully installed CocoaPods CLI with Homebrew`);
1328d3f3824SCedric van Putten        return true;
1338d3f3824SCedric van Putten      } catch (error: any) {
1348d3f3824SCedric van Putten        !silent &&
1358d3f3824SCedric van Putten          console.warn(
1368d3f3824SCedric van Putten            chalk.yellow(
1378d3f3824SCedric van Putten              `\u203A Failed to install CocoaPods with Homebrew. Please install CocoaPods CLI manually and try again.`
1388d3f3824SCedric van Putten            )
1398d3f3824SCedric van Putten          );
1408d3f3824SCedric van Putten        throw new CocoaPodsError(
1418d3f3824SCedric van Putten          `Failed to install CocoaPods with Homebrew. Please install CocoaPods CLI manually and try again.`,
1428d3f3824SCedric van Putten          'NO_CLI',
1438d3f3824SCedric van Putten          error
1448d3f3824SCedric van Putten        );
1458d3f3824SCedric van Putten      }
1468d3f3824SCedric van Putten    }
1478d3f3824SCedric van Putten  }
1488d3f3824SCedric van Putten
1498d3f3824SCedric van Putten  static isAvailable(projectRoot: string, silent: boolean): boolean {
1508d3f3824SCedric van Putten    if (process.platform !== 'darwin') {
1518d3f3824SCedric van Putten      !silent && console.log(chalk.red('CocoaPods is only supported on macOS machines'));
1528d3f3824SCedric van Putten      return false;
1538d3f3824SCedric van Putten    }
1548d3f3824SCedric van Putten    if (!CocoaPodsPackageManager.isUsingPods(projectRoot)) {
1558d3f3824SCedric van Putten      !silent && console.log(chalk.yellow('CocoaPods is not supported in this project'));
1568d3f3824SCedric van Putten      return false;
1578d3f3824SCedric van Putten    }
1588d3f3824SCedric van Putten    return true;
1598d3f3824SCedric van Putten  }
1608d3f3824SCedric van Putten
1618d3f3824SCedric van Putten  static async isCLIInstalledAsync(
1628d3f3824SCedric van Putten    spawnOptions: SpawnOptions = { stdio: 'inherit' }
1638d3f3824SCedric van Putten  ): Promise<boolean> {
1648d3f3824SCedric van Putten    try {
1658d3f3824SCedric van Putten      await spawnAsync('pod', ['--version'], spawnOptions);
1668d3f3824SCedric van Putten      return true;
1678d3f3824SCedric van Putten    } catch {
1688d3f3824SCedric van Putten      return false;
1698d3f3824SCedric van Putten    }
1708d3f3824SCedric van Putten  }
1718d3f3824SCedric van Putten
1728d3f3824SCedric van Putten  constructor({ cwd, silent }: { cwd: string; silent?: boolean }) {
1738d3f3824SCedric van Putten    this.silent = !!silent;
1748d3f3824SCedric van Putten    this.options = {
1758d3f3824SCedric van Putten      cwd,
1768d3f3824SCedric van Putten      // We use pipe by default instead of inherit so that we can capture stderr/stdout and process it for errors.
1778d3f3824SCedric van Putten      // Later we'll also pipe the stdout/stderr to the terminal when silent is false.
1788d3f3824SCedric van Putten      stdio: 'pipe',
1798d3f3824SCedric van Putten    };
1808d3f3824SCedric van Putten  }
1818d3f3824SCedric van Putten
1828d3f3824SCedric van Putten  get name() {
1838d3f3824SCedric van Putten    return 'CocoaPods';
1848d3f3824SCedric van Putten  }
1858d3f3824SCedric van Putten
1868d3f3824SCedric van Putten  /** Runs `pod install` and attempts to automatically run known troubleshooting steps automatically. */
1878d3f3824SCedric van Putten  async installAsync({ spinner }: { spinner?: Ora } = {}) {
1888d3f3824SCedric van Putten    await this._installAsync({ spinner });
1898d3f3824SCedric van Putten  }
1908d3f3824SCedric van Putten
1918d3f3824SCedric van Putten  public isCLIInstalledAsync() {
1928d3f3824SCedric van Putten    return CocoaPodsPackageManager.isCLIInstalledAsync(this.options);
1938d3f3824SCedric van Putten  }
1948d3f3824SCedric van Putten
1958d3f3824SCedric van Putten  public installCLIAsync() {
1968d3f3824SCedric van Putten    return CocoaPodsPackageManager.installCLIAsync({
1978d3f3824SCedric van Putten      nonInteractive: true,
1988d3f3824SCedric van Putten      spawnOptions: this.options,
1998d3f3824SCedric van Putten    });
2008d3f3824SCedric van Putten  }
2018d3f3824SCedric van Putten
2028d3f3824SCedric van Putten  async handleInstallErrorAsync({
2038d3f3824SCedric van Putten    error,
2048d3f3824SCedric van Putten    shouldUpdate = true,
2058d3f3824SCedric van Putten    updatedPackages = [],
2068d3f3824SCedric van Putten    spinner,
2078d3f3824SCedric van Putten  }: {
2088d3f3824SCedric van Putten    error: any;
2098d3f3824SCedric van Putten    spinner?: Ora;
2108d3f3824SCedric van Putten    shouldUpdate?: boolean;
2118d3f3824SCedric van Putten    updatedPackages?: string[];
2128d3f3824SCedric van Putten  }) {
2138d3f3824SCedric van Putten    // Unknown errors are rethrown.
2148d3f3824SCedric van Putten    if (!error.output) {
2158d3f3824SCedric van Putten      throw error;
2168d3f3824SCedric van Putten    }
2178d3f3824SCedric van Putten
2188d3f3824SCedric van Putten    // To emulate a `pod install --repo-update` error, enter your `ios/Podfile.lock` and change one of `PODS` version numbers to some lower value.
2198d3f3824SCedric van Putten    // const isPodRepoUpdateError = shouldPodRepoUpdate(output);
2208d3f3824SCedric van Putten    if (!shouldUpdate) {
2218d3f3824SCedric van Putten      // If we can't automatically fix the error, we'll just rethrow it with some known troubleshooting info.
2228d3f3824SCedric van Putten      throw getImprovedPodInstallError(error, {
2238d3f3824SCedric van Putten        cwd: this.options.cwd,
2248d3f3824SCedric van Putten      });
2258d3f3824SCedric van Putten    }
2268d3f3824SCedric van Putten
2278d3f3824SCedric van Putten    // Collect all of the spawn info.
2288d3f3824SCedric van Putten    const errorOutput = error.output.join(os.EOL).trim();
2298d3f3824SCedric van Putten
2308d3f3824SCedric van Putten    // Extract useful information from the error message and push it to the spinner.
2318d3f3824SCedric van Putten    const { updatePackage, shouldUpdateRepo } = getPodUpdateMessage(errorOutput);
2328d3f3824SCedric van Putten
2338d3f3824SCedric van Putten    if (!updatePackage || updatedPackages.includes(updatePackage)) {
2348d3f3824SCedric van Putten      // `pod install --repo-update`...
2358d3f3824SCedric van Putten      // Attempt to install again but this time with install --repo-update enabled.
2368d3f3824SCedric van Putten      return await this._installAsync({
2378d3f3824SCedric van Putten        spinner,
2388d3f3824SCedric van Putten        shouldRepoUpdate: true,
2398d3f3824SCedric van Putten        // Include a boolean to ensure pod install --repo-update isn't invoked in the unlikely case where the pods fail to update.
2408d3f3824SCedric van Putten        shouldUpdate: false,
2418d3f3824SCedric van Putten        updatedPackages,
2428d3f3824SCedric van Putten      });
2438d3f3824SCedric van Putten    }
2448d3f3824SCedric van Putten    // Store the package we should update to prevent a loop.
2458d3f3824SCedric van Putten    updatedPackages.push(updatePackage);
2468d3f3824SCedric van Putten
2478d3f3824SCedric van Putten    // If a single package is broken, we'll try to update it.
2488d3f3824SCedric van Putten    // You can manually test this by changing a version number in your `Podfile.lock`.
2498d3f3824SCedric van Putten
2508d3f3824SCedric van Putten    // Attempt `pod update <package> <--no-repo-update>` and then try again.
2518d3f3824SCedric van Putten    return await this.runInstallTypeCommandAsync(
2528d3f3824SCedric van Putten      ['update', updatePackage, shouldUpdateRepo ? '' : '--no-repo-update'].filter(Boolean),
2538d3f3824SCedric van Putten      {
2548d3f3824SCedric van Putten        formatWarning() {
2558d3f3824SCedric van Putten          const updateMessage = `Failed to update ${chalk.bold(
2568d3f3824SCedric van Putten            updatePackage
2578d3f3824SCedric van Putten          )}. Attempting to update the repo instead.`;
2588d3f3824SCedric van Putten          return updateMessage;
2598d3f3824SCedric van Putten        },
2608d3f3824SCedric van Putten        spinner,
2618d3f3824SCedric van Putten        updatedPackages,
2628d3f3824SCedric van Putten      }
2638d3f3824SCedric van Putten    );
2648d3f3824SCedric van Putten    // // If update succeeds, we'll try to install again (skipping `pod install --repo-update`).
2658d3f3824SCedric van Putten    // return await this._installAsync({
2668d3f3824SCedric van Putten    //   spinner,
2678d3f3824SCedric van Putten    //   shouldUpdate: false,
2688d3f3824SCedric van Putten    //   updatedPackages,
2698d3f3824SCedric van Putten    // });
2708d3f3824SCedric van Putten  }
2718d3f3824SCedric van Putten
2728d3f3824SCedric van Putten  private async _installAsync({
2738d3f3824SCedric van Putten    shouldRepoUpdate,
2748d3f3824SCedric van Putten    ...props
2758d3f3824SCedric van Putten  }: {
2768d3f3824SCedric van Putten    spinner?: Ora;
2778d3f3824SCedric van Putten    shouldUpdate?: boolean;
2788d3f3824SCedric van Putten    updatedPackages?: string[];
2798d3f3824SCedric van Putten    shouldRepoUpdate?: boolean;
2808d3f3824SCedric van Putten  } = {}): Promise<SpawnResult> {
2818d3f3824SCedric van Putten    return await this.runInstallTypeCommandAsync(
2828d3f3824SCedric van Putten      ['install', shouldRepoUpdate ? '--repo-update' : ''].filter(Boolean),
2838d3f3824SCedric van Putten      {
2848d3f3824SCedric van Putten        formatWarning(error: any) {
2858d3f3824SCedric van Putten          // Extract useful information from the error message and push it to the spinner.
2868d3f3824SCedric van Putten          return getPodRepoUpdateMessage(error.output.join(os.EOL).trim()).message;
2878d3f3824SCedric van Putten        },
2888d3f3824SCedric van Putten        ...props,
2898d3f3824SCedric van Putten      }
2908d3f3824SCedric van Putten    );
2918d3f3824SCedric van Putten  }
2928d3f3824SCedric van Putten
2938d3f3824SCedric van Putten  private async runInstallTypeCommandAsync(
2948d3f3824SCedric van Putten    command: string[],
2958d3f3824SCedric van Putten    {
2968d3f3824SCedric van Putten      formatWarning,
2978d3f3824SCedric van Putten      ...props
2988d3f3824SCedric van Putten    }: {
2998d3f3824SCedric van Putten      formatWarning?: (error: Error) => string;
3008d3f3824SCedric van Putten      spinner?: Ora;
3018d3f3824SCedric van Putten      shouldUpdate?: boolean;
3028d3f3824SCedric van Putten      updatedPackages?: string[];
3038d3f3824SCedric van Putten    } = {}
3048d3f3824SCedric van Putten  ): Promise<SpawnResult> {
3058d3f3824SCedric van Putten    try {
3068d3f3824SCedric van Putten      return await this._runAsync(command);
3078d3f3824SCedric van Putten    } catch (error: any) {
3088d3f3824SCedric van Putten      if (formatWarning) {
3098d3f3824SCedric van Putten        const warning = formatWarning(error);
3108d3f3824SCedric van Putten        if (props.spinner) {
3118d3f3824SCedric van Putten          props.spinner.text = chalk.bold(warning);
3128d3f3824SCedric van Putten        }
3138d3f3824SCedric van Putten        if (!this.silent) {
3148d3f3824SCedric van Putten          console.warn(chalk.yellow(warning));
3158d3f3824SCedric van Putten        }
3168d3f3824SCedric van Putten      }
3178d3f3824SCedric van Putten
3188d3f3824SCedric van Putten      return await this.handleInstallErrorAsync({ error, ...props });
3198d3f3824SCedric van Putten    }
3208d3f3824SCedric van Putten  }
3218d3f3824SCedric van Putten
3228d3f3824SCedric van Putten  async addWithParametersAsync(names: string[], parameters: string[]) {
3238d3f3824SCedric van Putten    throw new Error('Unimplemented');
3248d3f3824SCedric van Putten  }
3258d3f3824SCedric van Putten
3268d3f3824SCedric van Putten  addAsync(names: string[] = []) {
3278d3f3824SCedric van Putten    throw new Error('Unimplemented');
3288d3f3824SCedric van Putten  }
3298d3f3824SCedric van Putten
3308d3f3824SCedric van Putten  addDevAsync(names: string[] = []) {
3318d3f3824SCedric van Putten    throw new Error('Unimplemented');
3328d3f3824SCedric van Putten  }
3338d3f3824SCedric van Putten
3348d3f3824SCedric van Putten  addGlobalAsync(names: string[] = []) {
3358d3f3824SCedric van Putten    throw new Error('Unimplemented');
3368d3f3824SCedric van Putten  }
3378d3f3824SCedric van Putten
3388d3f3824SCedric van Putten  removeAsync(names: string[] = []) {
3398d3f3824SCedric van Putten    throw new Error('Unimplemented');
3408d3f3824SCedric van Putten  }
3418d3f3824SCedric van Putten
3428d3f3824SCedric van Putten  removeDevAsync(names: string[] = []) {
3438d3f3824SCedric van Putten    throw new Error('Unimplemented');
3448d3f3824SCedric van Putten  }
3458d3f3824SCedric van Putten
3468d3f3824SCedric van Putten  removeGlobalAsync(names: string[] = []) {
3478d3f3824SCedric van Putten    throw new Error('Unimplemented');
3488d3f3824SCedric van Putten  }
3498d3f3824SCedric van Putten
3508d3f3824SCedric van Putten  async versionAsync() {
3518d3f3824SCedric van Putten    const { stdout } = await spawnAsync('pod', ['--version'], this.options);
3528d3f3824SCedric van Putten    return stdout.trim();
3538d3f3824SCedric van Putten  }
3548d3f3824SCedric van Putten
3558d3f3824SCedric van Putten  async configAsync(key: string): Promise<string> {
3568d3f3824SCedric van Putten    throw new Error('Unimplemented');
3578d3f3824SCedric van Putten  }
3588d3f3824SCedric van Putten
3598d3f3824SCedric van Putten  async removeLockfileAsync() {
3608d3f3824SCedric van Putten    throw new Error('Unimplemented');
3618d3f3824SCedric van Putten  }
3628d3f3824SCedric van Putten
3638d3f3824SCedric van Putten  async uninstallAsync() {
3648d3f3824SCedric van Putten    throw new Error('Unimplemented');
3658d3f3824SCedric van Putten  }
3668d3f3824SCedric van Putten
3678d3f3824SCedric van Putten  // Private
3688d3f3824SCedric van Putten  private async podRepoUpdateAsync(): Promise<void> {
3698d3f3824SCedric van Putten    try {
3708d3f3824SCedric van Putten      await this._runAsync(['repo', 'update']);
3718d3f3824SCedric van Putten    } catch (error: any) {
3728d3f3824SCedric van Putten      error.message = error.message || (error.stderr ?? error.stdout);
3738d3f3824SCedric van Putten
3748d3f3824SCedric van Putten      throw new CocoaPodsError(
3758d3f3824SCedric van Putten        'The command `pod install --repo-update` failed',
3768d3f3824SCedric van Putten        'COMMAND_FAILED',
3778d3f3824SCedric van Putten        error
3788d3f3824SCedric van Putten      );
3798d3f3824SCedric van Putten    }
3808d3f3824SCedric van Putten  }
3818d3f3824SCedric van Putten
3828d3f3824SCedric van Putten  // Exposed for testing
3838d3f3824SCedric van Putten  async _runAsync(args: string[]): Promise<SpawnResult> {
3848d3f3824SCedric van Putten    if (!this.silent) {
3858d3f3824SCedric van Putten      console.log(`> pod ${args.join(' ')}`);
3868d3f3824SCedric van Putten    }
3878d3f3824SCedric van Putten    const promise = spawnAsync(
3888d3f3824SCedric van Putten      'pod',
3898d3f3824SCedric van Putten      [
3908d3f3824SCedric van Putten        ...args,
3918d3f3824SCedric van Putten        // Enables colors while collecting output.
3928d3f3824SCedric van Putten        '--ansi',
3938d3f3824SCedric van Putten      ],
3948d3f3824SCedric van Putten      {
3958d3f3824SCedric van Putten        // Add the cwd and other options to the spawn options.
3968d3f3824SCedric van Putten        ...this.options,
3978d3f3824SCedric van Putten        // We use pipe by default instead of inherit so that we can capture stderr/stdout and process it for errors.
3988d3f3824SCedric van Putten        // This is particularly required for the `pod install --repo-update` error.
3998d3f3824SCedric van Putten
4008d3f3824SCedric van Putten        // Later we'll also pipe the stdout/stderr to the terminal when silent is false,
4018d3f3824SCedric van Putten        // currently this means we lose out on the ansi colors unless passing the `--ansi` flag to every command.
4028d3f3824SCedric van Putten        stdio: 'pipe',
4038d3f3824SCedric van Putten      }
4048d3f3824SCedric van Putten    );
4058d3f3824SCedric van Putten
4068d3f3824SCedric van Putten    if (!this.silent) {
4078d3f3824SCedric van Putten      // If not silent, pipe the stdout/stderr to the terminal.
4088d3f3824SCedric van Putten      // We only do this when the `stdio` is set to `pipe` (collect the results for parsing), `inherit` won't contain `promise.child`.
4098d3f3824SCedric van Putten      if (promise.child.stdout) {
4108d3f3824SCedric van Putten        promise.child.stdout.pipe(process.stdout);
4118d3f3824SCedric van Putten      }
4128d3f3824SCedric van Putten    }
4138d3f3824SCedric van Putten
4148d3f3824SCedric van Putten    return await promise;
4158d3f3824SCedric van Putten  }
4168d3f3824SCedric van Putten}
4178d3f3824SCedric van Putten
4188d3f3824SCedric van Putten/** When pods are outdated, they'll throw an error informing you to run "pod install --repo-update" */
4198d3f3824SCedric van Puttenfunction shouldPodRepoUpdate(errorOutput: string) {
4208d3f3824SCedric van Putten  const output = errorOutput;
4218d3f3824SCedric van Putten  const isPodRepoUpdateError =
4228d3f3824SCedric van Putten    output.includes('pod repo update') || output.includes('--no-repo-update');
4238d3f3824SCedric van Putten  return isPodRepoUpdateError;
4248d3f3824SCedric van Putten}
4258d3f3824SCedric van Putten
4268d3f3824SCedric van Puttenexport function getPodUpdateMessage(output: string) {
4278d3f3824SCedric van Putten  const props = output.match(
4288d3f3824SCedric van Putten    /run ['"`]pod update ([\w-_\d/]+)( --no-repo-update)?['"`] to apply changes/
4298d3f3824SCedric van Putten  );
4308d3f3824SCedric van Putten
4318d3f3824SCedric van Putten  return {
4328d3f3824SCedric van Putten    updatePackage: props?.[1] ?? null,
4338d3f3824SCedric van Putten    shouldUpdateRepo: !props?.[2],
4348d3f3824SCedric van Putten  };
4358d3f3824SCedric van Putten}
4368d3f3824SCedric van Putten
4378d3f3824SCedric van Puttenexport function getPodRepoUpdateMessage(errorOutput: string) {
4388d3f3824SCedric van Putten  const warningInfo = extractMissingDependencyError(errorOutput);
4398d3f3824SCedric van Putten  const brokenPackage = getPodUpdateMessage(errorOutput);
4408d3f3824SCedric van Putten
4418d3f3824SCedric van Putten  let message: string;
4428d3f3824SCedric van Putten  if (warningInfo) {
4438d3f3824SCedric van Putten    message = `Couldn't install: ${warningInfo[1]} » ${chalk.underline(warningInfo[0])}.`;
4448d3f3824SCedric van Putten  } else if (brokenPackage?.updatePackage) {
4458d3f3824SCedric van Putten    message = `Couldn't install: ${brokenPackage?.updatePackage}.`;
4468d3f3824SCedric van Putten  } else {
4478d3f3824SCedric van Putten    message = `Couldn't install Pods.`;
4488d3f3824SCedric van Putten  }
4498d3f3824SCedric van Putten  message += ` Updating the Pods project and trying again...`;
4508d3f3824SCedric van Putten  return { message, ...brokenPackage };
4518d3f3824SCedric van Putten}
4528d3f3824SCedric van Putten
4538d3f3824SCedric van Putten/**
4548d3f3824SCedric van Putten * Format the CocoaPods CLI install error.
4558d3f3824SCedric van Putten *
4568d3f3824SCedric van Putten * @param error Error from CocoaPods CLI `pod install` command.
4578d3f3824SCedric van Putten * @returns
4588d3f3824SCedric van Putten */
4598d3f3824SCedric van Puttenexport function getImprovedPodInstallError(
4608d3f3824SCedric van Putten  error: SpawnResult & Error,
4618d3f3824SCedric van Putten  { cwd = process.cwd() }: Pick<SpawnOptions, 'cwd'>
4628d3f3824SCedric van Putten): Error {
4638d3f3824SCedric van Putten  // Collect all of the spawn info.
4648d3f3824SCedric van Putten  const errorOutput = error.output.join(os.EOL).trim();
4658d3f3824SCedric van Putten
4668d3f3824SCedric van Putten  if (error.stdout.match(/No [`'"]Podfile[`'"] found in the project directory/)) {
4678d3f3824SCedric van Putten    // Ran pod install but no Podfile was found.
4688d3f3824SCedric van Putten    error.message = `No Podfile found in directory: ${cwd}. Ensure CocoaPods is setup any try again.`;
4698d3f3824SCedric van Putten  } else if (shouldPodRepoUpdate(errorOutput)) {
4708d3f3824SCedric van Putten    // Ran pod install but the install --repo-update step failed.
4718d3f3824SCedric van Putten    const warningInfo = extractMissingDependencyError(errorOutput);
4728d3f3824SCedric van Putten    let reason: string;
4738d3f3824SCedric van Putten    if (warningInfo) {
4748d3f3824SCedric van Putten      reason = `Couldn't install: ${warningInfo[1]} » ${chalk.underline(warningInfo[0])}`;
4758d3f3824SCedric van Putten    } else {
4768d3f3824SCedric van Putten      reason = `This is often due to native package versions mismatching`;
4778d3f3824SCedric van Putten    }
4788d3f3824SCedric van Putten
4798d3f3824SCedric van Putten    // Attempt to provide a helpful message about the missing NPM dependency (containing a CocoaPod) since React Native
4808d3f3824SCedric van Putten    // developers will almost always be using autolinking and not interacting with CocoaPods directly.
4818d3f3824SCedric van Putten    let solution: string;
4828d3f3824SCedric van Putten    if (warningInfo?.[0]) {
4838d3f3824SCedric van Putten      // If the missing package is named `expo-dev-menu`, `react-native`, etc. then it might not be installed in the project.
4848d3f3824SCedric van Putten      if (warningInfo[0].match(/^(?:@?expo|@?react)(-|\/)/)) {
4858d3f3824SCedric van Putten        solution = `Ensure the node module "${warningInfo[0]}" is installed in your project, then run 'npx pod-install' to try again.`;
4868d3f3824SCedric van Putten      } else {
4878d3f3824SCedric van Putten        solution = `Ensure the CocoaPod "${warningInfo[0]}" is installed in your project, then run 'npx pod-install' to try again.`;
4888d3f3824SCedric van Putten      }
4898d3f3824SCedric van Putten    } else {
4908d3f3824SCedric van Putten      // Brute force
4918d3f3824SCedric van Putten      solution = `Try deleting the 'ios/Pods' folder or the 'ios/Podfile.lock' file and running 'npx pod-install' to resolve.`;
4928d3f3824SCedric van Putten    }
4938d3f3824SCedric van Putten    error.message = `${reason}. ${solution}`;
4948d3f3824SCedric van Putten
4958d3f3824SCedric van Putten    // Attempt to provide the troubleshooting info from CocoaPods CLI at the bottom of the error message.
4968d3f3824SCedric van Putten    if (error.stdout) {
4978d3f3824SCedric van Putten      const cocoapodsDebugInfo = error.stdout.split(os.EOL);
4988d3f3824SCedric van Putten      // The troubleshooting info starts with `[!]`, capture everything after that.
4998d3f3824SCedric van Putten      const firstWarning = cocoapodsDebugInfo.findIndex((v) => v.startsWith('[!]'));
5008d3f3824SCedric van Putten      if (firstWarning !== -1) {
5018d3f3824SCedric van Putten        const warning = cocoapodsDebugInfo.slice(firstWarning).join(os.EOL);
5028d3f3824SCedric van Putten        error.message += `\n\n${chalk.gray(warning)}`;
5038d3f3824SCedric van Putten      }
5048d3f3824SCedric van Putten    }
5058d3f3824SCedric van Putten    return new CocoaPodsError(
5068d3f3824SCedric van Putten      'Command `pod install --repo-update` failed.',
5078d3f3824SCedric van Putten      'COMMAND_FAILED',
5088d3f3824SCedric van Putten      error
5098d3f3824SCedric van Putten    );
5108d3f3824SCedric van Putten  } else {
5118d3f3824SCedric van Putten    let stderr: string | null = error.stderr.trim();
5128d3f3824SCedric van Putten
5138d3f3824SCedric van Putten    // CocoaPods CLI prints the useful error to stdout...
5148d3f3824SCedric van Putten    const usefulError = error.stdout.match(/\[!\]\s((?:.|\n)*)/)?.[1];
5158d3f3824SCedric van Putten
5168d3f3824SCedric van Putten    // If there is a useful error message then prune the less useful info.
5178d3f3824SCedric van Putten    if (usefulError) {
5188d3f3824SCedric van Putten      // Delete unhelpful CocoaPods CLI error message.
5198d3f3824SCedric van Putten      if (error.message?.match(/pod exited with non-zero code: 1/)) {
5208d3f3824SCedric van Putten        error.message = '';
5218d3f3824SCedric van Putten      }
5228d3f3824SCedric van Putten      stderr = null;
5238d3f3824SCedric van Putten    }
5248d3f3824SCedric van Putten
5258d3f3824SCedric van Putten    error.message = [usefulError, error.message, stderr].filter(Boolean).join('\n');
5268d3f3824SCedric van Putten  }
5278d3f3824SCedric van Putten
5288d3f3824SCedric van Putten  return new CocoaPodsError('Command `pod install` failed.', 'COMMAND_FAILED', error);
5298d3f3824SCedric van Putten}
530