1"use strict";
2var __importDefault = (this && this.__importDefault) || function (mod) {
3    return (mod && mod.__esModule) ? mod : { "default": mod };
4};
5Object.defineProperty(exports, "__esModule", { value: true });
6exports.getImprovedPodInstallError = exports.getPodRepoUpdateMessage = exports.getPodUpdateMessage = exports.CocoaPodsPackageManager = exports.extractMissingDependencyError = exports.CocoaPodsError = void 0;
7const spawn_async_1 = __importDefault(require("@expo/spawn-async"));
8const chalk_1 = __importDefault(require("chalk"));
9const fs_1 = require("fs");
10const os_1 = __importDefault(require("os"));
11const path_1 = __importDefault(require("path"));
12const spawn_1 = require("../utils/spawn");
13class CocoaPodsError extends Error {
14    code;
15    cause;
16    name = 'CocoaPodsError';
17    isPackageManagerError = true;
18    constructor(message, code, cause) {
19        super(cause ? `${message}\n└─ Cause: ${cause.message}` : message);
20        this.code = code;
21        this.cause = cause;
22    }
23}
24exports.CocoaPodsError = CocoaPodsError;
25function extractMissingDependencyError(errorOutput) {
26    // [!] Unable to find a specification for `expo-dev-menu-interface` depended upon by `expo-dev-launcher`
27    const results = errorOutput.match(/Unable to find a specification for ['"`]([\w-_\d\s]+)['"`] depended upon by ['"`]([\w-_\d\s]+)['"`]/);
28    if (results) {
29        return [results[1], results[2]];
30    }
31    return null;
32}
33exports.extractMissingDependencyError = extractMissingDependencyError;
34class CocoaPodsPackageManager {
35    options;
36    silent;
37    static getPodProjectRoot(projectRoot) {
38        if (CocoaPodsPackageManager.isUsingPods(projectRoot))
39            return projectRoot;
40        const iosProject = path_1.default.join(projectRoot, 'ios');
41        if (CocoaPodsPackageManager.isUsingPods(iosProject))
42            return iosProject;
43        const macOsProject = path_1.default.join(projectRoot, 'macos');
44        if (CocoaPodsPackageManager.isUsingPods(macOsProject))
45            return macOsProject;
46        return null;
47    }
48    static isUsingPods(projectRoot) {
49        return (0, fs_1.existsSync)(path_1.default.join(projectRoot, 'Podfile'));
50    }
51    static async gemInstallCLIAsync(nonInteractive = false, spawnOptions = { stdio: 'inherit' }) {
52        const options = ['install', 'cocoapods', '--no-document'];
53        try {
54            // In case the user has run sudo before running the command we can properly install CocoaPods without prompting for an interaction.
55            await (0, spawn_async_1.default)('gem', options, spawnOptions);
56        }
57        catch (error) {
58            if (nonInteractive) {
59                throw new CocoaPodsError('Failed to install CocoaPods CLI with gem (recommended)', 'COMMAND_FAILED', error);
60            }
61            // If the user doesn't have permission then we can prompt them to use sudo.
62            await (0, spawn_1.spawnSudoAsync)(['gem', ...options], spawnOptions);
63        }
64    }
65    static async brewLinkCLIAsync(spawnOptions = { stdio: 'inherit' }) {
66        await (0, spawn_async_1.default)('brew', ['link', 'cocoapods'], spawnOptions);
67    }
68    static async brewInstallCLIAsync(spawnOptions = { stdio: 'inherit' }) {
69        await (0, spawn_async_1.default)('brew', ['install', 'cocoapods'], spawnOptions);
70    }
71    static async installCLIAsync({ nonInteractive = false, spawnOptions = { stdio: 'inherit' }, }) {
72        if (!spawnOptions) {
73            spawnOptions = { stdio: 'inherit' };
74        }
75        const silent = !!spawnOptions.ignoreStdio;
76        try {
77            !silent && console.log(`\u203A Attempting to install CocoaPods CLI with Gem`);
78            await CocoaPodsPackageManager.gemInstallCLIAsync(nonInteractive, spawnOptions);
79            !silent && console.log(`\u203A Successfully installed CocoaPods CLI with Gem`);
80            return true;
81        }
82        catch (error) {
83            if (!silent) {
84                console.log(chalk_1.default.yellow(`\u203A Failed to install CocoaPods CLI with Gem`));
85                console.log(chalk_1.default.red(error.stderr ?? error.message));
86                console.log(`\u203A Attempting to install CocoaPods CLI with Homebrew`);
87            }
88            try {
89                await CocoaPodsPackageManager.brewInstallCLIAsync(spawnOptions);
90                if (!(await CocoaPodsPackageManager.isCLIInstalledAsync(spawnOptions))) {
91                    try {
92                        await CocoaPodsPackageManager.brewLinkCLIAsync(spawnOptions);
93                        // Still not available after linking? Bail out
94                        if (!(await CocoaPodsPackageManager.isCLIInstalledAsync(spawnOptions))) {
95                            throw new CocoaPodsError('CLI could not be installed automatically with gem or Homebrew, please install CocoaPods manually and try again', 'NO_CLI', error);
96                        }
97                    }
98                    catch (error) {
99                        throw new CocoaPodsError('Homebrew installation appeared to succeed but CocoaPods CLI not found in PATH and unable to link.', 'NO_CLI', error);
100                    }
101                }
102                !silent && console.log(`\u203A Successfully installed CocoaPods CLI with Homebrew`);
103                return true;
104            }
105            catch (error) {
106                !silent &&
107                    console.warn(chalk_1.default.yellow(`\u203A Failed to install CocoaPods with Homebrew. Please install CocoaPods CLI manually and try again.`));
108                throw new CocoaPodsError(`Failed to install CocoaPods with Homebrew. Please install CocoaPods CLI manually and try again.`, 'NO_CLI', error);
109            }
110        }
111    }
112    static isAvailable(projectRoot, silent) {
113        if (process.platform !== 'darwin') {
114            !silent && console.log(chalk_1.default.red('CocoaPods is only supported on macOS machines'));
115            return false;
116        }
117        if (!CocoaPodsPackageManager.isUsingPods(projectRoot)) {
118            !silent && console.log(chalk_1.default.yellow('CocoaPods is not supported in this project'));
119            return false;
120        }
121        return true;
122    }
123    static async isCLIInstalledAsync(spawnOptions = { stdio: 'inherit' }) {
124        try {
125            await (0, spawn_async_1.default)('pod', ['--version'], spawnOptions);
126            return true;
127        }
128        catch {
129            return false;
130        }
131    }
132    constructor({ cwd, silent }) {
133        this.silent = !!silent;
134        this.options = {
135            cwd,
136            // We use pipe by default instead of inherit so that we can capture stderr/stdout and process it for errors.
137            // Later we'll also pipe the stdout/stderr to the terminal when silent is false.
138            stdio: 'pipe',
139        };
140    }
141    get name() {
142        return 'CocoaPods';
143    }
144    /** Runs `pod install` and attempts to automatically run known troubleshooting steps automatically. */
145    async installAsync({ spinner } = {}) {
146        await this._installAsync({ spinner });
147    }
148    isCLIInstalledAsync() {
149        return CocoaPodsPackageManager.isCLIInstalledAsync(this.options);
150    }
151    installCLIAsync() {
152        return CocoaPodsPackageManager.installCLIAsync({
153            nonInteractive: true,
154            spawnOptions: this.options,
155        });
156    }
157    async handleInstallErrorAsync({ error, shouldUpdate = true, updatedPackages = [], spinner, }) {
158        // Unknown errors are rethrown.
159        if (!error.output) {
160            throw error;
161        }
162        // To emulate a `pod install --repo-update` error, enter your `ios/Podfile.lock` and change one of `PODS` version numbers to some lower value.
163        // const isPodRepoUpdateError = shouldPodRepoUpdate(output);
164        if (!shouldUpdate) {
165            // If we can't automatically fix the error, we'll just rethrow it with some known troubleshooting info.
166            throw getImprovedPodInstallError(error, {
167                cwd: this.options.cwd,
168            });
169        }
170        // Collect all of the spawn info.
171        const errorOutput = error.output.join(os_1.default.EOL).trim();
172        // Extract useful information from the error message and push it to the spinner.
173        const { updatePackage, shouldUpdateRepo } = getPodUpdateMessage(errorOutput);
174        if (!updatePackage || updatedPackages.includes(updatePackage)) {
175            // `pod install --repo-update`...
176            // Attempt to install again but this time with install --repo-update enabled.
177            return await this._installAsync({
178                spinner,
179                shouldRepoUpdate: true,
180                // Include a boolean to ensure pod install --repo-update isn't invoked in the unlikely case where the pods fail to update.
181                shouldUpdate: false,
182                updatedPackages,
183            });
184        }
185        // Store the package we should update to prevent a loop.
186        updatedPackages.push(updatePackage);
187        // If a single package is broken, we'll try to update it.
188        // You can manually test this by changing a version number in your `Podfile.lock`.
189        // Attempt `pod update <package> <--no-repo-update>` and then try again.
190        return await this.runInstallTypeCommandAsync(['update', updatePackage, shouldUpdateRepo ? '' : '--no-repo-update'].filter(Boolean), {
191            formatWarning() {
192                const updateMessage = `Failed to update ${chalk_1.default.bold(updatePackage)}. Attempting to update the repo instead.`;
193                return updateMessage;
194            },
195            spinner,
196            updatedPackages,
197        });
198        // // If update succeeds, we'll try to install again (skipping `pod install --repo-update`).
199        // return await this._installAsync({
200        //   spinner,
201        //   shouldUpdate: false,
202        //   updatedPackages,
203        // });
204    }
205    async _installAsync({ shouldRepoUpdate, ...props } = {}) {
206        return await this.runInstallTypeCommandAsync(['install', shouldRepoUpdate ? '--repo-update' : ''].filter(Boolean), {
207            formatWarning(error) {
208                // Extract useful information from the error message and push it to the spinner.
209                return getPodRepoUpdateMessage(error.output.join(os_1.default.EOL).trim()).message;
210            },
211            ...props,
212        });
213    }
214    async runInstallTypeCommandAsync(command, { formatWarning, ...props } = {}) {
215        try {
216            return await this._runAsync(command);
217        }
218        catch (error) {
219            if (formatWarning) {
220                const warning = formatWarning(error);
221                if (props.spinner) {
222                    props.spinner.text = chalk_1.default.bold(warning);
223                }
224                if (!this.silent) {
225                    console.warn(chalk_1.default.yellow(warning));
226                }
227            }
228            return await this.handleInstallErrorAsync({ error, ...props });
229        }
230    }
231    async addWithParametersAsync(names, parameters) {
232        throw new Error('Unimplemented');
233    }
234    addAsync(names = []) {
235        throw new Error('Unimplemented');
236    }
237    addDevAsync(names = []) {
238        throw new Error('Unimplemented');
239    }
240    addGlobalAsync(names = []) {
241        throw new Error('Unimplemented');
242    }
243    removeAsync(names = []) {
244        throw new Error('Unimplemented');
245    }
246    removeDevAsync(names = []) {
247        throw new Error('Unimplemented');
248    }
249    removeGlobalAsync(names = []) {
250        throw new Error('Unimplemented');
251    }
252    async versionAsync() {
253        const { stdout } = await (0, spawn_async_1.default)('pod', ['--version'], this.options);
254        return stdout.trim();
255    }
256    async configAsync(key) {
257        throw new Error('Unimplemented');
258    }
259    async removeLockfileAsync() {
260        throw new Error('Unimplemented');
261    }
262    async uninstallAsync() {
263        throw new Error('Unimplemented');
264    }
265    // Private
266    async podRepoUpdateAsync() {
267        try {
268            await this._runAsync(['repo', 'update']);
269        }
270        catch (error) {
271            error.message = error.message || (error.stderr ?? error.stdout);
272            throw new CocoaPodsError('The command `pod install --repo-update` failed', 'COMMAND_FAILED', error);
273        }
274    }
275    // Exposed for testing
276    async _runAsync(args) {
277        if (!this.silent) {
278            console.log(`> pod ${args.join(' ')}`);
279        }
280        const promise = (0, spawn_async_1.default)('pod', [
281            ...args,
282            // Enables colors while collecting output.
283            '--ansi',
284        ], {
285            // Add the cwd and other options to the spawn options.
286            ...this.options,
287            // We use pipe by default instead of inherit so that we can capture stderr/stdout and process it for errors.
288            // This is particularly required for the `pod install --repo-update` error.
289            // Later we'll also pipe the stdout/stderr to the terminal when silent is false,
290            // currently this means we lose out on the ansi colors unless passing the `--ansi` flag to every command.
291            stdio: 'pipe',
292        });
293        if (!this.silent) {
294            // If not silent, pipe the stdout/stderr to the terminal.
295            // We only do this when the `stdio` is set to `pipe` (collect the results for parsing), `inherit` won't contain `promise.child`.
296            if (promise.child.stdout) {
297                promise.child.stdout.pipe(process.stdout);
298            }
299        }
300        return await promise;
301    }
302}
303exports.CocoaPodsPackageManager = CocoaPodsPackageManager;
304/** When pods are outdated, they'll throw an error informing you to run "pod install --repo-update" */
305function shouldPodRepoUpdate(errorOutput) {
306    const output = errorOutput;
307    const isPodRepoUpdateError = output.includes('pod repo update') || output.includes('--no-repo-update');
308    return isPodRepoUpdateError;
309}
310function getPodUpdateMessage(output) {
311    const props = output.match(/run ['"`]pod update ([\w-_\d/]+)( --no-repo-update)?['"`] to apply changes/);
312    return {
313        updatePackage: props?.[1] ?? null,
314        shouldUpdateRepo: !props?.[2],
315    };
316}
317exports.getPodUpdateMessage = getPodUpdateMessage;
318function getPodRepoUpdateMessage(errorOutput) {
319    const warningInfo = extractMissingDependencyError(errorOutput);
320    const brokenPackage = getPodUpdateMessage(errorOutput);
321    let message;
322    if (warningInfo) {
323        message = `Couldn't install: ${warningInfo[1]} » ${chalk_1.default.underline(warningInfo[0])}.`;
324    }
325    else if (brokenPackage?.updatePackage) {
326        message = `Couldn't install: ${brokenPackage?.updatePackage}.`;
327    }
328    else {
329        message = `Couldn't install Pods.`;
330    }
331    message += ` Updating the Pods project and trying again...`;
332    return { message, ...brokenPackage };
333}
334exports.getPodRepoUpdateMessage = getPodRepoUpdateMessage;
335/**
336 * Format the CocoaPods CLI install error.
337 *
338 * @param error Error from CocoaPods CLI `pod install` command.
339 * @returns
340 */
341function getImprovedPodInstallError(error, { cwd = process.cwd() }) {
342    // Collect all of the spawn info.
343    const errorOutput = error.output.join(os_1.default.EOL).trim();
344    if (error.stdout.match(/No [`'"]Podfile[`'"] found in the project directory/)) {
345        // Ran pod install but no Podfile was found.
346        error.message = `No Podfile found in directory: ${cwd}. Ensure CocoaPods is setup any try again.`;
347    }
348    else if (shouldPodRepoUpdate(errorOutput)) {
349        // Ran pod install but the install --repo-update step failed.
350        const warningInfo = extractMissingDependencyError(errorOutput);
351        let reason;
352        if (warningInfo) {
353            reason = `Couldn't install: ${warningInfo[1]} » ${chalk_1.default.underline(warningInfo[0])}`;
354        }
355        else {
356            reason = `This is often due to native package versions mismatching`;
357        }
358        // Attempt to provide a helpful message about the missing NPM dependency (containing a CocoaPod) since React Native
359        // developers will almost always be using autolinking and not interacting with CocoaPods directly.
360        let solution;
361        if (warningInfo?.[0]) {
362            // If the missing package is named `expo-dev-menu`, `react-native`, etc. then it might not be installed in the project.
363            if (warningInfo[0].match(/^(?:@?expo|@?react)(-|\/)/)) {
364                solution = `Ensure the node module "${warningInfo[0]}" is installed in your project, then run 'npx pod-install' to try again.`;
365            }
366            else {
367                solution = `Ensure the CocoaPod "${warningInfo[0]}" is installed in your project, then run 'npx pod-install' to try again.`;
368            }
369        }
370        else {
371            // Brute force
372            solution = `Try deleting the 'ios/Pods' folder or the 'ios/Podfile.lock' file and running 'npx pod-install' to resolve.`;
373        }
374        error.message = `${reason}. ${solution}`;
375        // Attempt to provide the troubleshooting info from CocoaPods CLI at the bottom of the error message.
376        if (error.stdout) {
377            const cocoapodsDebugInfo = error.stdout.split(os_1.default.EOL);
378            // The troubleshooting info starts with `[!]`, capture everything after that.
379            const firstWarning = cocoapodsDebugInfo.findIndex((v) => v.startsWith('[!]'));
380            if (firstWarning !== -1) {
381                const warning = cocoapodsDebugInfo.slice(firstWarning).join(os_1.default.EOL);
382                error.message += `\n\n${chalk_1.default.gray(warning)}`;
383            }
384        }
385        return new CocoaPodsError('Command `pod install --repo-update` failed.', 'COMMAND_FAILED', error);
386    }
387    else {
388        let stderr = error.stderr.trim();
389        // CocoaPods CLI prints the useful error to stdout...
390        const usefulError = error.stdout.match(/\[!\]\s((?:.|\n)*)/)?.[1];
391        // If there is a useful error message then prune the less useful info.
392        if (usefulError) {
393            // Delete unhelpful CocoaPods CLI error message.
394            if (error.message?.match(/pod exited with non-zero code: 1/)) {
395                error.message = '';
396            }
397            stderr = null;
398        }
399        error.message = [usefulError, error.message, stderr].filter(Boolean).join('\n');
400    }
401    return new CocoaPodsError('Command `pod install` failed.', 'COMMAND_FAILED', error);
402}
403exports.getImprovedPodInstallError = getImprovedPodInstallError;
404//# sourceMappingURL=CocoaPodsPackageManager.js.map