"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.NpmPackageManager = void 0;
const json_file_1 = __importDefault(require("@expo/json-file"));
const spawn_async_1 = __importDefault(require("@expo/spawn-async"));
const npm_package_arg_1 = __importDefault(require("npm-package-arg"));
const path_1 = __importDefault(require("path"));
const BasePackageManager_1 = require("./BasePackageManager");
const nodeManagers_1 = require("../utils/nodeManagers");
const spawn_1 = require("../utils/spawn");
class NpmPackageManager extends BasePackageManager_1.BasePackageManager {
    name = 'npm';
    bin = 'npm';
    lockFile = nodeManagers_1.NPM_LOCK_FILE;
    workspaceRoot() {
        const root = (0, nodeManagers_1.resolveWorkspaceRoot)(this.ensureCwdDefined('workspaceRoot'));
        if (root) {
            return new NpmPackageManager({
                ...this.options,
                silent: this.silent,
                log: this.log,
                cwd: root,
            });
        }
        return null;
    }
    addAsync(namesOrFlags = []) {
        if (!namesOrFlags.length) {
            return this.installAsync();
        }
        const { flags, versioned, unversioned } = this.parsePackageSpecs(namesOrFlags);
        return (0, spawn_1.createPendingSpawnAsync)(() => this.updatePackageFileAsync(versioned, 'dependencies'), () => !unversioned.length
            ? this.runAsync(['install', ...flags])
            : this.runAsync(['install', '--save', ...flags, ...unversioned.map((spec) => spec.raw)]));
    }
    addDevAsync(namesOrFlags = []) {
        if (!namesOrFlags.length) {
            return this.installAsync();
        }
        const { flags, versioned, unversioned } = this.parsePackageSpecs(namesOrFlags);
        return (0, spawn_1.createPendingSpawnAsync)(() => this.updatePackageFileAsync(versioned, 'devDependencies'), () => !unversioned.length
            ? this.runAsync(['install', ...flags])
            : this.runAsync([
                'install',
                '--save-dev',
                ...flags,
                ...unversioned.map((spec) => spec.raw),
            ]));
    }
    addGlobalAsync(namesOrFlags = []) {
        if (!namesOrFlags.length) {
            return this.installAsync();
        }
        return this.runAsync(['install', '--global', ...namesOrFlags]);
    }
    removeAsync(namesOrFlags) {
        return this.runAsync(['uninstall', ...namesOrFlags]);
    }
    removeDevAsync(namesOrFlags) {
        return this.runAsync(['uninstall', '--save-dev', ...namesOrFlags]);
    }
    removeGlobalAsync(namesOrFlags) {
        return this.runAsync(['uninstall', '--global', ...namesOrFlags]);
    }
    runBinAsync(command, options = {}) {
        this.log?.(`> npx ${command.join(' ')}`);
        return (0, spawn_async_1.default)('npx', command, { ...this.options, ...options });
    }
    /**
     * Parse all package specifications from the names or flag list.
     * The result from this method can be used for `.updatePackageFileAsync`.
     */
    parsePackageSpecs(namesOrFlags) {
        const result = { flags: [], versioned: [], unversioned: [] };
        namesOrFlags
            .map((name) => {
            if (name.trim().startsWith('-')) {
                result.flags.push(name);
                return null;
            }
            return (0, npm_package_arg_1.default)(name);
        })
            .forEach((spec) => {
            // When using a dist-tag version of a library, we need to consider it as "unversioned".
            // Doing so will install that version with `npm install --save(-dev)`, and resolve the dist-tag properly.
            const hasExactSpec = !!spec && spec.rawSpec !== '' && spec.rawSpec !== '*';
            if (spec && hasExactSpec && spec.type !== 'tag') {
                result.versioned.push(spec);
            }
            else if (spec) {
                result.unversioned.push(spec);
            }
        });
        return result;
    }
    /** Sort dependencies by keys (case-insensitive, stable). Sorting algorithm is taken from https://github.com/npm/package-json/blob/f5db81bdfbba5e9d3bfc0732f8bfe511825a20aa/lib/update-dependencies.js#L9 */
    orderDependencies(deps) {
        if (!deps) {
            return {};
        }
        const sorted = Object.keys(deps)
            .sort((a, b) => a.localeCompare(b, 'en'))
            .reduce((res, key) => {
            res[key] = deps[key];
            return res;
        }, {});
        return sorted;
    }
    /**
     * Older npm versions have issues with mismatched nested dependencies when adding exact versions.
     * This propagates as issues like mismatched `@expo/config-pugins` versions.
     * As a workaround, we update the `package.json` directly and run `npm install`.
     */
    async updatePackageFileAsync(packageSpecs, packageType) {
        if (!packageSpecs.length) {
            return;
        }
        const pkgPath = path_1.default.join(this.options.cwd?.toString() || '.', 'package.json');
        const pkg = await json_file_1.default.readAsync(pkgPath);
        packageSpecs.forEach((spec) => {
            pkg[packageType] = pkg[packageType] || {};
            pkg[packageType][spec.name] = spec.rawSpec;
        });
        pkg[packageType] = this.orderDependencies(pkg[packageType]);
        await json_file_1.default.writeAsync(pkgPath, pkg, { json5: false });
    }
}
exports.NpmPackageManager = NpmPackageManager;
//# sourceMappingURL=NpmPackageManager.js.map