"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
Object.defineProperty(exports, "__esModule", { value: true });
const lifecycle_1 = require("@pnpm/lifecycle");
const link_bins_1 = require("@pnpm/link-bins");
const logger_1 = require("@pnpm/logger");
const modules_yaml_1 = require("@pnpm/modules-yaml");
const package_requester_1 = require("@pnpm/package-requester");
const pkgid_to_filename_1 = require("@pnpm/pkgid-to-filename");
const dp = require("dependency-path");
const pLimit = require("p-limit");
const path = require("path");
const pnpm_shrinkwrap_1 = require("pnpm-shrinkwrap");
const R = require("ramda");
const readPkgCB = require("read-package-json");
const removeOrphanPkgs_1 = require("supi/lib/api/removeOrphanPkgs");
const realNodeModulesDir_1 = require("supi/lib/fs/realNodeModulesDir");
const loggers_1 = require("supi/lib/loggers");
const symlinkDir = require("symlink-dir");
const promisify = require("util.promisify");
const constants_1 = require("./constants");
const runDependenciesScripts_1 = require("./runDependenciesScripts");
const readPkg = promisify(readPkgCB);
exports.default = (opts) => __awaiter(this, void 0, void 0, function* () {
    const reporter = opts.reporter;
    if (reporter) {
        logger_1.streamParser.on('data', reporter);
    }
    if (typeof opts.prefix !== 'string') {
        throw new TypeError('opts.prefix should be a string');
    }
    const wantedShrinkwrap = opts.wantedShrinkwrap || (yield pnpm_shrinkwrap_1.readWanted(opts.prefix, { ignoreIncompatible: false }));
    if (!wantedShrinkwrap) {
        throw new Error('Headless installation requires a shrinkwrap.yaml file');
    }
    const currentShrinkwrap = opts.currentShrinkwrap || (yield pnpm_shrinkwrap_1.readCurrent(opts.prefix, { ignoreIncompatible: false }));
    const modules = yield modules_yaml_1.read(path.join(opts.prefix, 'node_modules'));
    const pkg = opts.packageJson || (yield readPkg(path.join(opts.prefix, 'package.json')));
    if (!pnpm_shrinkwrap_1.satisfiesPackageJson(wantedShrinkwrap, pkg)) {
        throw new Error('Cannot run headless installation because shrinkwrap.yaml is not up-to-date with package.json');
    }
    loggers_1.packageJsonLogger.debug({ initial: pkg });
    const scripts = !opts.ignoreScripts && pkg.scripts || {};
    const nodeModules = yield realNodeModulesDir_1.default(opts.prefix);
    const bin = path.join(nodeModules, '.bin');
    const scriptsOpts = {
        pkgId: opts.prefix,
        pkgRoot: opts.prefix,
        rawNpmConfig: opts.rawNpmConfig,
        rootNodeModulesDir: nodeModules,
        stdio: 'inherit',
        unsafePerm: opts.unsafePerm || false,
    };
    if (scripts.preinstall) {
        yield lifecycle_1.default('preinstall', pkg, scriptsOpts);
    }
    if (currentShrinkwrap) {
        yield removeOrphanPkgs_1.default({
            bin,
            dryRun: false,
            hoistedAliases: modules && modules.hoistedAliases || {},
            newShrinkwrap: wantedShrinkwrap,
            oldShrinkwrap: currentShrinkwrap,
            prefix: opts.prefix,
            shamefullyFlatten: false,
            storeController: opts.storeController,
        });
    }
    else {
        loggers_1.statsLogger.debug({ removed: 0 });
    }
    const filterOpts = {
        noDev: !opts.development,
        noOptional: !opts.optional,
        noProd: !opts.production,
    };
    const filteredShrinkwrap = filterShrinkwrap(wantedShrinkwrap, filterOpts);
    loggers_1.stageLogger.debug('importing_started');
    const depGraph = yield shrinkwrapToDepGraph(filteredShrinkwrap, currentShrinkwrap, opts);
    loggers_1.statsLogger.debug({ added: Object.keys(depGraph).length });
    yield Promise.all([
        linkAllModules(depGraph, { optional: opts.optional }),
        linkAllPkgs(opts.storeController, R.values(depGraph).filter((depNode) => !depNode.skip), opts),
    ]);
    loggers_1.stageLogger.debug('importing_done');
    yield linkAllBins(depGraph, { optional: opts.optional });
    yield linkRootPackages(filteredShrinkwrap, depGraph, nodeModules);
    yield link_bins_1.default(nodeModules, bin);
    yield pnpm_shrinkwrap_1.writeCurrentOnly(opts.prefix, filteredShrinkwrap);
    yield modules_yaml_1.write(path.join(opts.prefix, 'node_modules'), {
        hoistedAliases: {},
        independentLeaves: !!opts.independentLeaves,
        layoutVersion: constants_1.LAYOUT_VERSION,
        packageManager: `${opts.packageManager.name}@${opts.packageManager.version}`,
        pendingBuilds: [],
        shamefullyFlatten: false,
        skipped: [],
        store: opts.store,
    });
    if (!opts.ignoreScripts) {
        yield runDependenciesScripts_1.default(depGraph, opts);
    }
    // waiting till package requests are finished
    yield Promise.all(R.values(depGraph).map((depNode) => depNode.finishing));
    loggers_1.summaryLogger.info(undefined);
    yield opts.storeController.close();
    if (scripts.install) {
        yield lifecycle_1.default('install', pkg, scriptsOpts);
    }
    if (scripts.postinstall) {
        yield lifecycle_1.default('postinstall', pkg, scriptsOpts);
    }
    if (scripts.prepublish) {
        yield lifecycle_1.default('prepublish', pkg, scriptsOpts);
    }
    if (scripts.prepare) {
        yield lifecycle_1.default('prepare', pkg, scriptsOpts);
    }
    if (reporter) {
        logger_1.streamParser.removeListener('data', reporter);
    }
});
function linkRootPackages(shr, depGraph, baseNodeModules) {
    return __awaiter(this, void 0, void 0, function* () {
        const allDeps = Object.assign({}, shr.devDependencies, shr.dependencies, shr.optionalDependencies);
        return Promise.all(R.keys(allDeps)
            .map((alias) => __awaiter(this, void 0, void 0, function* () {
            const depPath = dp.refToAbsolute(allDeps[alias], alias, shr.registry);
            const depNode = depGraph[depPath];
            if (!depNode || depNode.skip) {
                return;
            }
            yield symlinkDependencyTo(alias, depNode, baseNodeModules);
            const isDev = shr.devDependencies && shr.devDependencies[alias];
            const isOptional = shr.optionalDependencies && shr.optionalDependencies[alias];
            const relDepPath = dp.refToRelative(allDeps[alias], alias);
            const pkgSnapshot = shr.packages && shr.packages[relDepPath];
            if (!pkgSnapshot)
                return; // this won't ever happen. Just making typescript happy
            const pkgId = pkgSnapshot.id || depPath;
            const pkgInfo = pnpm_shrinkwrap_1.nameVerFromPkgSnapshot(relDepPath, pkgSnapshot);
            loggers_1.rootLogger.info({
                added: {
                    dependencyType: isDev && 'dev' || isOptional && 'optional' || 'prod',
                    id: pkgId,
                    // latest: opts.outdatedPkgs[pkg.id],
                    name: alias,
                    realName: pkgInfo.name,
                    version: pkgInfo.version,
                },
            });
        })));
    });
}
function shrinkwrapToDepGraph(shr, currentShrinkwrap, opts) {
    return __awaiter(this, void 0, void 0, function* () {
        const nodeModules = path.join(opts.prefix, 'node_modules');
        const currentPackages = currentShrinkwrap && currentShrinkwrap.packages || {};
        const graph = {};
        if (shr.packages) {
            for (const relDepPath of R.keys(shr.packages)) {
                const skip = (currentPackages[relDepPath] && R.equals(currentPackages[relDepPath].dependencies, shr.packages[relDepPath].dependencies) &&
                    R.equals(currentPackages[relDepPath].optionalDependencies, shr.packages[relDepPath].optionalDependencies));
                const depPath = dp.resolve(shr.registry, relDepPath);
                const pkgSnapshot = shr.packages[relDepPath];
                const independent = opts.independentLeaves && pkgSnapshot.dependencies === undefined && pkgSnapshot.optionalDependencies === undefined;
                const resolution = pnpm_shrinkwrap_1.pkgSnapshotToResolution(relDepPath, pkgSnapshot, shr.registry);
                // TODO: optimize. This info can be already returned by pkgSnapshotToResolution()
                const pkgName = pkgSnapshot.name || dp.parse(relDepPath)['name']; // tslint:disable-line
                const pkgId = pkgSnapshot.id || depPath;
                const fetchResponse = opts.storeController.fetchPackage({
                    force: false,
                    pkgId,
                    prefix: opts.prefix,
                    resolution,
                    verifyStoreIntegrity: opts.verifyStoreIntegrity,
                });
                const cacheByEngine = opts.force ? new Map() : yield package_requester_1.getCacheByEngine(opts.store, pkgId);
                const cache = cacheByEngine[constants_1.ENGINE_NAME];
                const centralLocation = cache || path.join(fetchResponse.inStoreLocation, 'node_modules', pkgName);
                // NOTE: This code will not convert the depPath with peer deps correctly
                // Unfortunately, there is currently no way to tell if the last dir in the path is originally there or added to separate
                // the diferent peer dependency sets
                const modules = path.join(nodeModules, `.${pkgid_to_filename_1.default(depPath)}`, 'node_modules');
                const peripheralLocation = !independent
                    ? path.join(modules, pkgName)
                    : centralLocation;
                graph[depPath] = {
                    centralLocation,
                    // TODO: This could be just a map of alias to the child's peripheralLocation.
                    // In that case the skipping field could be avoided
                    children: getChildren(pkgSnapshot, shr.registry),
                    fetchingFiles: fetchResponse.fetchingFiles,
                    finishing: fetchResponse.finishing,
                    hasBundledDependencies: !!pkgSnapshot.bundledDependencies,
                    independent,
                    isBuilt: !!cache,
                    modules,
                    optional: !!pkgSnapshot.optional,
                    optionalDependencies: new Set(R.keys(pkgSnapshot.optionalDependencies)),
                    peripheralLocation,
                    pkgId,
                    skip,
                };
            }
        }
        return graph;
    });
}
function getChildren(pkgSnapshot, registry) {
    const allDeps = Object.assign({}, pkgSnapshot.dependencies, pkgSnapshot.optionalDependencies);
    return R.keys(allDeps)
        .reduce((acc, alias) => {
        acc[alias] = dp.refToAbsolute(allDeps[alias], alias, registry);
        return acc;
    }, {});
}
const limitLinking = pLimit(16);
function linkAllPkgs(storeController, depNodes, opts) {
    return __awaiter(this, void 0, void 0, function* () {
        return Promise.all(depNodes.map((depNode) => __awaiter(this, void 0, void 0, function* () {
            const filesResponse = yield depNode.fetchingFiles;
            if (depNode.independent)
                return;
            return storeController.importPackage(depNode.centralLocation, depNode.peripheralLocation, {
                filesResponse,
                force: opts.force,
            });
        })));
    });
}
function linkAllBins(depGraph, opts) {
    return __awaiter(this, void 0, void 0, function* () {
        return Promise.all(R.values(depGraph)
            .filter((depNode) => !depNode.skip)
            .map((depNode) => limitLinking(() => __awaiter(this, void 0, void 0, function* () {
            const binPath = path.join(depNode.peripheralLocation, 'node_modules', '.bin');
            const childrenToLink = opts.optional
                ? depNode.children
                : R.keys(depNode.children)
                    .reduce((nonOptionalChildren, childAlias) => {
                    if (!depNode.optionalDependencies.has(childAlias)) {
                        nonOptionalChildren[childAlias] = depNode.children[childAlias];
                    }
                    return nonOptionalChildren;
                }, {});
            yield Promise.all(R.keys(childrenToLink)
                .map((alias) => path.join(depNode.modules, alias))
                .map((target) => link_bins_1.linkPackageBins(target, binPath)));
            // link also the bundled dependencies` bins
            if (depNode.hasBundledDependencies) {
                const bundledModules = path.join(depNode.peripheralLocation, 'node_modules');
                yield link_bins_1.default(bundledModules, binPath);
            }
        }))));
    });
}
function linkAllModules(depGraph, opts) {
    return __awaiter(this, void 0, void 0, function* () {
        return Promise.all(R.values(depGraph)
            .filter((depNode) => !depNode.independent && !depNode.skip)
            .map((depNode) => limitLinking(() => __awaiter(this, void 0, void 0, function* () {
            const childrenToLink = opts.optional
                ? depNode.children
                : R.keys(depNode.children)
                    .reduce((nonOptionalChildren, childAlias) => {
                    if (!depNode.optionalDependencies.has(childAlias)) {
                        nonOptionalChildren[childAlias] = depNode.children[childAlias];
                    }
                    return nonOptionalChildren;
                }, {});
            yield Promise.all(R.keys(childrenToLink)
                .map((alias) => __awaiter(this, void 0, void 0, function* () {
                const pkg = depGraph[childrenToLink[alias]];
                // if (!pkg.installable) return
                yield symlinkDependencyTo(alias, pkg, depNode.modules);
            })));
        }))));
    });
}
function symlinkDependencyTo(alias, depNode, dest) {
    dest = path.join(dest, alias);
    return symlinkDir(depNode.peripheralLocation, dest);
}
// TODO: move this to separate package
// the version of the function which is in supi also accepts `opts.skip`
// headless will never skip anything
function filterShrinkwrap(shr, opts) {
    let pairs = R.toPairs(shr.packages || {});
    if (opts.noProd) {
        pairs = pairs.filter((pair) => pair[1].dev !== false || pair[1].optional);
    }
    if (opts.noDev) {
        pairs = pairs.filter((pair) => pair[1].dev !== true);
    }
    if (opts.noOptional) {
        pairs = pairs.filter((pair) => !pair[1].optional);
    }
    return {
        dependencies: opts.noProd ? {} : shr.dependencies || {},
        devDependencies: opts.noDev ? {} : shr.devDependencies || {},
        optionalDependencies: opts.noOptional ? {} : shr.optionalDependencies || {},
        packages: R.fromPairs(pairs),
        registry: shr.registry,
        shrinkwrapVersion: shr.shrinkwrapVersion,
        specifiers: shr.specifiers,
    };
}
//# sourceMappingURL=index.js.map