"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 read_package_json_1 = require("@pnpm/read-package-json");
const utils_1 = require("@pnpm/utils");
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 symlinkDir = require("symlink-dir");
const constants_1 = require("./constants");
const runDependenciesScripts_1 = require("./runDependenciesScripts");
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 nodeModulesDir = yield utils_1.realNodeModulesDir(opts.prefix);
    const modules = (yield modules_yaml_1.read(nodeModulesDir)) || { pendingBuilds: [], hoistedAliases: {} };
    const pkg = opts.packageJson || (yield read_package_json_1.fromDir(opts.prefix));
    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');
    }
    utils_1.packageJsonLogger.debug({ initial: pkg });
    const scripts = !opts.ignoreScripts && pkg.scripts || {};
    const bin = path.join(nodeModulesDir, '.bin');
    const scriptsOpts = {
        depPath: opts.prefix,
        pkgRoot: opts.prefix,
        rawNpmConfig: opts.rawNpmConfig,
        rootNodeModulesDir: nodeModulesDir,
        stdio: opts.ownLifecycleHooksStdio || 'inherit',
        unsafePerm: opts.unsafePerm || false,
    };
    if (scripts.preinstall) {
        yield lifecycle_1.default('preinstall', pkg, scriptsOpts);
    }
    if (currentShrinkwrap) {
        yield utils_1.removeOrphanPackages({
            bin,
            dryRun: false,
            hoistedAliases: modules && modules.hoistedAliases || {},
            newShrinkwrap: wantedShrinkwrap,
            oldShrinkwrap: currentShrinkwrap,
            prefix: opts.prefix,
            shamefullyFlatten: false,
            storeController: opts.storeController,
        });
    }
    else {
        utils_1.statsLogger.debug({
            prefix: opts.prefix,
            removed: 0,
        });
    }
    const filterOpts = {
        noDev: !opts.development,
        noOptional: !opts.optional,
        noProd: !opts.production,
    };
    const filteredShrinkwrap = filterShrinkwrap(wantedShrinkwrap, filterOpts);
    utils_1.stageLogger.debug('importing_started');
    const res = yield shrinkwrapToDepGraph(filteredShrinkwrap, opts.force ? null : currentShrinkwrap, Object.assign({}, opts, { nodeModulesDir }));
    const depGraph = res.graph;
    utils_1.statsLogger.debug({
        added: Object.keys(depGraph).length,
        prefix: opts.prefix,
    });
    yield Promise.all([
        linkAllModules(depGraph, { optional: opts.optional }),
        linkAllPkgs(opts.storeController, R.values(depGraph), opts),
    ]);
    utils_1.stageLogger.debug('importing_done');
    yield linkAllBins(depGraph, { optional: opts.optional });
    yield linkRootPackages(filteredShrinkwrap, depGraph, res.rootDependencies, nodeModulesDir);
    yield link_bins_1.default(nodeModulesDir, bin);
    yield pnpm_shrinkwrap_1.writeCurrentOnly(opts.prefix, filteredShrinkwrap);
    if (opts.ignoreScripts) {
        // we can use concat here because we always only append new packages, which are guaranteed to not be there by definition
        modules.pendingBuilds = modules.pendingBuilds
            .concat(R.values(depGraph)
            .filter((node) => node.requiresBuild)
            .map((node) => node.relDepPath));
    }
    yield modules_yaml_1.write(nodeModulesDir, {
        hoistedAliases: {},
        independentLeaves: !!opts.independentLeaves,
        layoutVersion: constants_1.LAYOUT_VERSION,
        packageManager: `${opts.packageManager.name}@${opts.packageManager.version}`,
        pendingBuilds: modules.pendingBuilds,
        shamefullyFlatten: false,
        skipped: [],
        store: opts.store,
    });
    if (!opts.ignoreScripts) {
        yield runDependenciesScripts_1.default(depGraph, R.values(res.rootDependencies).filter((loc) => depGraph[loc]), opts);
    }
    // waiting till package requests are finished
    yield Promise.all(R.values(depGraph).map((depNode) => depNode.finishing));
    utils_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, rootDependencies, 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 peripheralLocation = rootDependencies[alias];
            // Skipping linked packages
            if (!peripheralLocation) {
                return;
            }
            if ((yield symlinkDependencyTo(alias, peripheralLocation, baseNodeModules)).reused) {
                return;
            }
            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);
            utils_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 currentPackages = currentShrinkwrap && currentShrinkwrap.packages || {};
        const graph = {};
        let rootDependencies = {};
        if (shr.packages) {
            const pkgSnapshotByLocation = {};
            for (const relDepPath of R.keys(shr.packages)) {
                if (currentPackages[relDepPath] && R.equals(currentPackages[relDepPath].dependencies, shr.packages[relDepPath].dependencies) &&
                    R.equals(currentPackages[relDepPath].optionalDependencies, shr.packages[relDepPath].optionalDependencies)) {
                    continue;
                }
                const depPath = dp.resolve(shr.registry, relDepPath);
                const pkgSnapshot = shr.packages[relDepPath];
                const independent = opts.independentLeaves && pkgIsIndependent(pkgSnapshot);
                const resolution = pnpm_shrinkwrap_1.pkgSnapshotToResolution(relDepPath, pkgSnapshot, shr.registry);
                // TODO: optimize. This info can be already returned by pkgSnapshotToResolution()
                const pkgName = pnpm_shrinkwrap_1.nameVerFromPkgSnapshot(relDepPath, pkgSnapshot).name;
                const pkgId = pkgSnapshot.id || depPath;
                utils_1.progressLogger.debug({
                    pkgId,
                    status: 'resolving_content',
                });
                let fetchResponse = opts.storeController.fetchPackage({
                    force: false,
                    pkgId,
                    prefix: opts.prefix,
                    resolution,
                    verifyStoreIntegrity: opts.verifyStoreIntegrity,
                });
                if (fetchResponse instanceof Promise)
                    fetchResponse = yield fetchResponse;
                fetchResponse.fetchingFiles
                    .then((fetchResult) => {
                    utils_1.progressLogger.debug({
                        pkgId,
                        status: fetchResult.fromStore
                            ? 'found_in_store' : 'fetched',
                    });
                });
                const cache = !opts.force && (yield getCache(opts.store, pkgId));
                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(opts.nodeModulesDir, `.${pkgid_to_filename_1.default(depPath)}`, 'node_modules');
                const peripheralLocation = !independent
                    ? path.join(modules, pkgName)
                    : centralLocation;
                graph[peripheralLocation] = {
                    centralLocation,
                    children: {},
                    fetchingFiles: fetchResponse.fetchingFiles,
                    finishing: fetchResponse.finishing,
                    hasBin: pkgSnapshot.hasBin === true,
                    hasBundledDependencies: !!pkgSnapshot.bundledDependencies,
                    independent,
                    isBuilt: !!cache,
                    modules,
                    optional: !!pkgSnapshot.optional,
                    optionalDependencies: new Set(R.keys(pkgSnapshot.optionalDependencies)),
                    peripheralLocation,
                    pkgId,
                    prepare: pkgSnapshot.prepare === true,
                    relDepPath: depPath,
                    requiresBuild: pkgSnapshot.requiresBuild === true,
                };
                pkgSnapshotByLocation[peripheralLocation] = pkgSnapshot;
            }
            const ctx = {
                force: opts.force,
                graph,
                independentLeaves: opts.independentLeaves,
                nodeModules: opts.nodeModulesDir,
                pkgSnapshotsByRelDepPaths: shr.packages,
                prefix: opts.prefix,
                registry: shr.registry,
                store: opts.store,
            };
            for (const peripheralLocation of R.keys(graph)) {
                const pkgSnapshot = pkgSnapshotByLocation[peripheralLocation];
                const allDeps = Object.assign({}, pkgSnapshot.dependencies, pkgSnapshot.optionalDependencies);
                graph[peripheralLocation].children = yield getChildrenPaths(ctx, allDeps);
            }
            const rootDeps = Object.assign({}, shr.devDependencies, shr.dependencies, shr.optionalDependencies);
            rootDependencies = yield getChildrenPaths(ctx, rootDeps);
        }
        return { graph, rootDependencies };
    });
}
function getChildrenPaths(ctx, allDeps) {
    return __awaiter(this, void 0, void 0, function* () {
        const children = {};
        for (const alias of R.keys(allDeps)) {
            const childDepPath = dp.refToAbsolute(allDeps[alias], alias, ctx.registry);
            const childRelDepPath = dp.relative(ctx.registry, childDepPath);
            const childPkgSnapshot = ctx.pkgSnapshotsByRelDepPaths[childRelDepPath];
            if (ctx.graph[childDepPath]) {
                children[alias] = ctx.graph[childDepPath].peripheralLocation;
            }
            else if (ctx.independentLeaves && pkgIsIndependent(childPkgSnapshot)) {
                const pkgId = childPkgSnapshot.id || childDepPath;
                const cache = !ctx.force && (yield getCache(ctx.store, pkgId));
                const pkgName = pnpm_shrinkwrap_1.nameVerFromPkgSnapshot(childRelDepPath, childPkgSnapshot).name;
                const inStoreLocation = pkgid_to_filename_1.default(pkgId);
                children[alias] = cache || path.join(inStoreLocation, 'node_modules', pkgName);
            }
            else if (childPkgSnapshot) {
                const relDepPath = dp.relative(ctx.registry, childDepPath);
                const pkgName = pnpm_shrinkwrap_1.nameVerFromPkgSnapshot(relDepPath, childPkgSnapshot).name;
                children[alias] = path.join(ctx.nodeModules, `.${pkgid_to_filename_1.default(childDepPath)}`, 'node_modules', pkgName);
            }
            else if (allDeps[alias].indexOf('link:') === 0 || allDeps[alias].indexOf('file:') === 0) {
                children[alias] = path.resolve(ctx.prefix, allDeps[alias].substr(5));
            }
            else {
                throw new Error(`${childRelDepPath} not found in shrinkwrap.yaml`);
            }
        }
        return children;
    });
}
function getCache(storePath, pkgId) {
    return __awaiter(this, void 0, void 0, function* () {
        return (yield package_requester_1.getCacheByEngine(storePath, pkgId))[constants_1.ENGINE_NAME];
    });
}
function pkgIsIndependent(pkgSnapshot) {
    return pkgSnapshot.dependencies === undefined && pkgSnapshot.optionalDependencies === undefined;
}
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)
            .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;
                }, {});
            const binPath = path.join(depNode.peripheralLocation, 'node_modules', '.bin');
            const pkgSnapshots = R.props(R.values(childrenToLink), depGraph);
            if (pkgSnapshots.indexOf(undefined) !== -1) { // tslint:disable-line
                yield link_bins_1.default(depNode.modules, binPath);
            }
            else {
                const pkgs = yield Promise.all(pkgSnapshots
                    .filter((dep) => dep.hasBin)
                    .map((dep) => __awaiter(this, void 0, void 0, function* () {
                    return ({
                        location: dep.peripheralLocation,
                        manifest: yield read_package_json_1.fromDir(dep.peripheralLocation),
                    });
                })));
                yield link_bins_1.linkBinsOfPackages(pkgs, 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)
            .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* () {
                // if (!pkg.installable) return
                yield symlinkDependencyTo(alias, childrenToLink[alias], depNode.modules);
            })));
        }))));
    });
}
function symlinkDependencyTo(alias, peripheralLocation, dest) {
    dest = path.join(dest, alias);
    return symlinkDir(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