"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const build_modules_1 = require("@pnpm/build-modules");
const constants_1 = require("@pnpm/constants");
const core_loggers_1 = require("@pnpm/core-loggers");
const error_1 = require("@pnpm/error");
const filter_lockfile_1 = require("@pnpm/filter-lockfile");
const lifecycle_1 = require("@pnpm/lifecycle");
const link_bins_1 = require("@pnpm/link-bins");
const lockfile_file_1 = require("@pnpm/lockfile-file");
const lockfile_utils_1 = require("@pnpm/lockfile-utils");
const logger_1 = require("@pnpm/logger");
const modules_cleaner_1 = require("@pnpm/modules-cleaner");
const modules_yaml_1 = require("@pnpm/modules-yaml");
const pkgid_to_filename_1 = require("@pnpm/pkgid-to-filename");
const read_importer_manifest_1 = require("@pnpm/read-importer-manifest");
const read_package_json_1 = require("@pnpm/read-package-json");
const shamefully_flatten_1 = require("@pnpm/shamefully-flatten");
const symlink_dependency_1 = require("@pnpm/symlink-dependency");
const utils_1 = require("@pnpm/utils");
const dp = require("dependency-path");
const fs = require("mz/fs");
const p_limit_1 = require("p-limit");
const path = require("path");
const R = require("ramda");
const brokenNodeModulesLogger = logger_1.default('_broken_node_modules');
exports.default = async (opts) => {
    const reporter = opts.reporter;
    if (reporter) {
        logger_1.streamParser.on('data', reporter);
    }
    const lockfileDirectory = opts.lockfileDirectory;
    const wantedLockfile = opts.wantedLockfile || await lockfile_file_1.readWantedLockfile(lockfileDirectory, { ignoreIncompatible: false });
    if (!wantedLockfile) {
        throw new Error(`Headless installation requires a ${constants_1.WANTED_LOCKFILE} file`);
    }
    const currentLockfile = opts.currentLockfile || await lockfile_file_1.readCurrentLockfile(lockfileDirectory, { ignoreIncompatible: false });
    const virtualStoreDir = await utils_1.realNodeModulesDir(lockfileDirectory);
    for (const { id, manifest, prefix } of opts.importers) {
        if (!lockfile_utils_1.satisfiesPackageJson(wantedLockfile, manifest, id)) {
            throw new error_1.default('OUTDATED_LOCKFILE', `Cannot install with "frozen-lockfile" because ${constants_1.WANTED_LOCKFILE} is not up-to-date with ` +
                path.relative(opts.lockfileDirectory, path.join(prefix, 'package.json')));
        }
    }
    const scriptsOpts = {
        optional: false,
        rawNpmConfig: opts.rawNpmConfig,
        stdio: opts.ownLifecycleHooksStdio || 'inherit',
        unsafePerm: opts.unsafePerm || false,
    };
    if (!opts.ignoreScripts) {
        await lifecycle_1.runLifecycleHooksConcurrently(['preinstall'], opts.importers, opts.childConcurrency || 5, scriptsOpts);
    }
    const skipped = opts.skipped || new Set();
    if (currentLockfile) {
        await modules_cleaner_1.prune(opts.importers, {
            currentLockfile,
            dryRun: false,
            include: opts.include,
            lockfileDirectory,
            pruneStore: opts.pruneStore,
            registries: opts.registries,
            skipped,
            storeController: opts.storeController,
            virtualStoreDir,
            wantedLockfile,
        });
    }
    else {
        core_loggers_1.statsLogger.debug({
            prefix: lockfileDirectory,
            removed: 0,
        });
    }
    core_loggers_1.stageLogger.debug({
        prefix: opts.lockfileDirectory,
        stage: 'importing_started',
    });
    const filterOpts = {
        include: opts.include,
        registries: opts.registries,
        skipped,
    };
    const filteredLockfile = filter_lockfile_1.filterLockfileByImportersAndEngine(wantedLockfile, opts.importers.map(({ id }) => id), Object.assign({}, filterOpts, { currentEngine: opts.currentEngine, engineStrict: opts.engineStrict, failOnMissingDependencies: true, includeIncompatiblePackages: opts.force === true, prefix: lockfileDirectory }));
    const { directDependenciesByImporterId, graph } = await lockfileToDepGraph(filteredLockfile, opts.force ? null : currentLockfile, Object.assign({}, opts, { importerIds: opts.importers.map(({ id }) => id), prefix: lockfileDirectory, skipped,
        virtualStoreDir }));
    const depNodes = R.values(graph);
    core_loggers_1.statsLogger.debug({
        added: depNodes.length,
        prefix: lockfileDirectory,
    });
    await Promise.all([
        linkAllModules(depNodes, {
            lockfileDirectory: opts.lockfileDirectory,
            optional: opts.include.optionalDependencies,
        }),
        linkAllPkgs(opts.storeController, depNodes, opts),
    ]);
    core_loggers_1.stageLogger.debug({
        prefix: opts.lockfileDirectory,
        stage: 'importing_done',
    });
    function warn(message) {
        logger_1.default.warn({
            message,
            prefix: lockfileDirectory,
        });
    }
    const rootImporterWithFlatModules = opts.importers.find((importer) => importer.id === '.' && importer.shamefullyFlatten);
    if (rootImporterWithFlatModules) {
        rootImporterWithFlatModules.hoistedAliases = await shamefully_flatten_1.default({
            getIndependentPackageLocation: opts.independentLeaves
                ? async (packageId, packageName) => {
                    const { directory } = await opts.storeController.getPackageLocation(packageId, packageName, {
                        lockfileDirectory: opts.lockfileDirectory,
                        targetEngine: opts.sideEffectsCacheRead && constants_1.ENGINE_NAME || undefined,
                    });
                    return directory;
                }
                : undefined,
            lockfile: filteredLockfile,
            lockfileDirectory: opts.lockfileDirectory,
            modulesDir: rootImporterWithFlatModules.modulesDir,
            registries: opts.registries,
            virtualStoreDir,
        });
    }
    await Promise.all(opts.importers.map(async ({ id, manifest, modulesDir, prefix }) => {
        await linkRootPackages(filteredLockfile, {
            importerId: id,
            importerModulesDir: modulesDir,
            importers: opts.importers,
            lockfileDirectory: opts.lockfileDirectory,
            prefix,
            registries: opts.registries,
            rootDependencies: directDependenciesByImporterId[id],
        });
        // Even though headless installation will never update the package.json
        // this needs to be logged because otherwise install summary won't be printed
        core_loggers_1.packageJsonLogger.debug({
            prefix,
            updated: manifest,
        });
    }));
    if (opts.ignoreScripts) {
        for (const { id, manifest } of opts.importers) {
            if (opts.ignoreScripts && manifest && manifest.scripts &&
                (manifest.scripts.preinstall || manifest.scripts.prepublish ||
                    manifest.scripts.install ||
                    manifest.scripts.postinstall ||
                    manifest.scripts.prepare)) {
                opts.pendingBuilds.push(id);
            }
        }
        // we can use concat here because we always only append new packages, which are guaranteed to not be there by definition
        opts.pendingBuilds = opts.pendingBuilds
            .concat(depNodes
            .filter(({ requiresBuild }) => requiresBuild)
            .map(({ relDepPath }) => relDepPath));
    }
    else {
        const directNodes = new Set();
        for (const { id } of opts.importers) {
            R
                .values(directDependenciesByImporterId[id])
                .filter((loc) => graph[loc])
                .forEach((loc) => {
                directNodes.add(loc);
            });
        }
        await build_modules_1.default(graph, Array.from(directNodes), {
            childConcurrency: opts.childConcurrency,
            extraBinPaths: opts.extraBinPaths,
            optional: opts.include.optionalDependencies,
            prefix: opts.lockfileDirectory,
            rawNpmConfig: opts.rawNpmConfig,
            rootNodeModulesDir: virtualStoreDir,
            sideEffectsCacheWrite: opts.sideEffectsCacheWrite,
            storeController: opts.storeController,
            unsafePerm: opts.unsafePerm,
            userAgent: opts.userAgent,
        });
    }
    await linkAllBins(graph, { optional: opts.include.optionalDependencies, warn });
    await Promise.all(opts.importers.map(linkBinsOfImporter));
    if (currentLockfile && !R.equals(opts.importers.map(({ id }) => id).sort(), Object.keys(filteredLockfile.importers).sort())) {
        Object.assign(filteredLockfile.packages, currentLockfile.packages);
    }
    await lockfile_file_1.writeCurrentLockfile(lockfileDirectory, filteredLockfile);
    await modules_yaml_1.write(virtualStoreDir, {
        importers: opts.importers.reduce((acc, importer) => {
            acc[importer.id] = {
                hoistedAliases: importer.hoistedAliases,
                shamefullyFlatten: importer.shamefullyFlatten,
            };
            return acc;
        }, {}),
        included: opts.include,
        independentLeaves: !!opts.independentLeaves,
        layoutVersion: constants_1.LAYOUT_VERSION,
        packageManager: `${opts.packageManager.name}@${opts.packageManager.version}`,
        pendingBuilds: opts.pendingBuilds,
        registries: opts.registries,
        skipped: Array.from(skipped),
        store: opts.store,
    });
    // waiting till package requests are finished
    await Promise.all(depNodes.map(({ finishing }) => finishing));
    core_loggers_1.summaryLogger.debug({ prefix: opts.lockfileDirectory });
    await opts.storeController.close();
    if (!opts.ignoreScripts) {
        await lifecycle_1.runLifecycleHooksConcurrently(['install', 'postinstall', 'prepublish', 'prepare'], opts.importers, opts.childConcurrency || 5, scriptsOpts);
    }
    if (reporter) {
        logger_1.streamParser.removeListener('data', reporter);
    }
};
function linkBinsOfImporter({ modulesDir, bin, prefix }) {
    const warn = (message) => logger_1.default.warn({ message, prefix });
    return link_bins_1.default(modulesDir, bin, {
        allowExoticManifests: true,
        warn,
    });
}
async function linkRootPackages(lockfile, opts) {
    const importerManifestsByImporterId = {};
    for (const { id, manifest } of opts.importers) {
        importerManifestsByImporterId[id] = manifest;
    }
    const lockfileImporter = lockfile.importers[opts.importerId];
    const allDeps = Object.assign({}, lockfileImporter.devDependencies, lockfileImporter.dependencies, lockfileImporter.optionalDependencies);
    return Promise.all(R.keys(allDeps)
        .map(async (alias) => {
        if (allDeps[alias].startsWith('link:')) {
            const isDev = lockfileImporter.devDependencies && lockfileImporter.devDependencies[alias];
            const isOptional = lockfileImporter.optionalDependencies && lockfileImporter.optionalDependencies[alias];
            const packageDir = path.join(opts.prefix, allDeps[alias].substr(5));
            const linkedPackage = await (async () => {
                const importerId = lockfile_file_1.getLockfileImporterId(opts.lockfileDirectory, packageDir);
                if (importerManifestsByImporterId[importerId]) {
                    return importerManifestsByImporterId[importerId];
                }
                // TODO: cover this case with a test
                return await read_importer_manifest_1.readImporterManifestOnly(packageDir);
            })();
            await symlink_dependency_1.symlinkDirectRootDependency(packageDir, opts.importerModulesDir, alias, {
                fromDependenciesField: isDev && 'devDependencies' ||
                    isOptional && 'optionalDependencies' ||
                    'dependencies',
                linkedPackage,
                prefix: opts.prefix,
            });
            return;
        }
        const depPath = dp.refToAbsolute(allDeps[alias], alias, opts.registries);
        const peripheralLocation = opts.rootDependencies[alias];
        // Skipping linked packages
        if (!peripheralLocation) {
            return;
        }
        if ((await symlink_dependency_1.default(peripheralLocation, opts.importerModulesDir, alias)).reused) {
            return;
        }
        const isDev = lockfileImporter.devDependencies && lockfileImporter.devDependencies[alias];
        const isOptional = lockfileImporter.optionalDependencies && lockfileImporter.optionalDependencies[alias];
        const relDepPath = dp.refToRelative(allDeps[alias], alias);
        if (relDepPath === null)
            return;
        const pkgSnapshot = lockfile.packages && lockfile.packages[relDepPath];
        if (!pkgSnapshot)
            return; // this won't ever happen. Just making typescript happy
        const pkgId = pkgSnapshot.id || depPath || undefined;
        const pkgInfo = lockfile_utils_1.nameVerFromPkgSnapshot(relDepPath, pkgSnapshot);
        core_loggers_1.rootLogger.debug({
            added: {
                dependencyType: isDev && 'dev' || isOptional && 'optional' || 'prod',
                id: pkgId,
                // latest: opts.outdatedPkgs[pkg.id],
                name: alias,
                realName: pkgInfo.name,
                version: pkgInfo.version,
            },
            prefix: opts.prefix,
        });
    }));
}
async function lockfileToDepGraph(lockfile, currentLockfile, opts) {
    const currentPackages = currentLockfile && currentLockfile.packages || {};
    const graph = {};
    let directDependenciesByImporterId = {};
    if (lockfile.packages) {
        const pkgSnapshotByLocation = {};
        await Promise.all(R.keys(lockfile.packages).map(async (relDepPath) => {
            const depPath = dp.resolve(opts.registries, relDepPath);
            const pkgSnapshot = lockfile.packages[relDepPath];
            // TODO: optimize. This info can be already returned by pkgSnapshotToResolution()
            const pkgName = lockfile_utils_1.nameVerFromPkgSnapshot(relDepPath, pkgSnapshot).name;
            const modules = path.join(opts.virtualStoreDir, `.${pkgid_to_filename_1.default(depPath, opts.lockfileDirectory)}`, 'node_modules');
            const packageId = lockfile_utils_1.packageIdFromSnapshot(relDepPath, pkgSnapshot, opts.registries);
            const pkgLocation = await opts.storeController.getPackageLocation(packageId, pkgName, {
                lockfileDirectory: opts.lockfileDirectory,
                targetEngine: opts.sideEffectsCacheRead && !opts.force && constants_1.ENGINE_NAME || undefined,
            });
            const independent = opts.independentLeaves && lockfile_utils_1.packageIsIndependent(pkgSnapshot);
            const peripheralLocation = !independent
                ? path.join(modules, pkgName)
                : pkgLocation.directory;
            if (currentPackages[relDepPath] && R.equals(currentPackages[relDepPath].dependencies, lockfile.packages[relDepPath].dependencies) &&
                R.equals(currentPackages[relDepPath].optionalDependencies, lockfile.packages[relDepPath].optionalDependencies)) {
                if (await fs.exists(peripheralLocation)) {
                    return;
                }
                brokenNodeModulesLogger.debug({
                    missing: peripheralLocation,
                });
            }
            const resolution = lockfile_utils_1.pkgSnapshotToResolution(relDepPath, pkgSnapshot, opts.registries);
            core_loggers_1.progressLogger.debug({
                packageId,
                requester: opts.lockfileDirectory,
                status: 'resolved',
            });
            let fetchResponse = opts.storeController.fetchPackage({
                force: false,
                pkgId: packageId,
                prefix: opts.prefix,
                resolution,
            });
            if (fetchResponse instanceof Promise)
                fetchResponse = await fetchResponse;
            fetchResponse.fetchingFiles // tslint:disable-line
                .then((fetchResult) => {
                core_loggers_1.progressLogger.debug({
                    packageId,
                    requester: opts.lockfileDirectory,
                    status: fetchResult.fromStore
                        ? 'found_in_store' : 'fetched',
                });
            });
            graph[peripheralLocation] = {
                centralLocation: pkgLocation.directory,
                children: {},
                fetchingFiles: fetchResponse.fetchingFiles,
                finishing: fetchResponse.finishing,
                hasBin: pkgSnapshot.hasBin === true,
                hasBundledDependencies: !!pkgSnapshot.bundledDependencies,
                independent,
                isBuilt: pkgLocation.isBuilt,
                modules,
                name: pkgName,
                optional: !!pkgSnapshot.optional,
                optionalDependencies: new Set(R.keys(pkgSnapshot.optionalDependencies)),
                packageId,
                peripheralLocation,
                prepare: pkgSnapshot.prepare === true,
                relDepPath,
                requiresBuild: pkgSnapshot.requiresBuild === true,
            };
            pkgSnapshotByLocation[peripheralLocation] = pkgSnapshot;
        }));
        const ctx = {
            force: opts.force,
            graph,
            independentLeaves: opts.independentLeaves,
            lockfileDirectory: opts.lockfileDirectory,
            pkgSnapshotsByRelDepPaths: lockfile.packages,
            prefix: opts.prefix,
            registries: opts.registries,
            sideEffectsCacheRead: opts.sideEffectsCacheRead,
            skipped: opts.skipped,
            store: opts.store,
            storeController: opts.storeController,
            virtualStoreDir: opts.virtualStoreDir,
        };
        for (const peripheralLocation of R.keys(graph)) {
            const pkgSnapshot = pkgSnapshotByLocation[peripheralLocation];
            const allDeps = Object.assign({}, pkgSnapshot.dependencies, pkgSnapshot.optionalDependencies);
            graph[peripheralLocation].children = await getChildrenPaths(ctx, allDeps);
        }
        for (const importerId of opts.importerIds) {
            const lockfileImporter = lockfile.importers[importerId];
            const rootDeps = Object.assign({}, lockfileImporter.devDependencies, lockfileImporter.dependencies, lockfileImporter.optionalDependencies);
            directDependenciesByImporterId[importerId] = await getChildrenPaths(ctx, rootDeps);
        }
    }
    return { graph, directDependenciesByImporterId };
}
async function getChildrenPaths(ctx, allDeps) {
    const children = {};
    for (const alias of R.keys(allDeps)) {
        const childDepPath = dp.refToAbsolute(allDeps[alias], alias, ctx.registries);
        if (childDepPath === null) {
            children[alias] = path.resolve(ctx.prefix, allDeps[alias].substr(5));
            continue;
        }
        const childRelDepPath = dp.refToRelative(allDeps[alias], alias);
        const childPkgSnapshot = ctx.pkgSnapshotsByRelDepPaths[childRelDepPath];
        if (ctx.graph[childDepPath]) {
            children[alias] = ctx.graph[childDepPath].peripheralLocation;
        }
        else if (ctx.independentLeaves && lockfile_utils_1.packageIsIndependent(childPkgSnapshot)) {
            const pkgId = childPkgSnapshot.id || childDepPath;
            const pkgName = lockfile_utils_1.nameVerFromPkgSnapshot(childRelDepPath, childPkgSnapshot).name;
            const pkgLocation = await ctx.storeController.getPackageLocation(pkgId, pkgName, {
                lockfileDirectory: ctx.lockfileDirectory,
                targetEngine: ctx.sideEffectsCacheRead && !ctx.force && constants_1.ENGINE_NAME || undefined,
            });
            children[alias] = pkgLocation.directory;
        }
        else if (childPkgSnapshot) {
            const pkgName = lockfile_utils_1.nameVerFromPkgSnapshot(childRelDepPath, childPkgSnapshot).name;
            children[alias] = path.join(ctx.virtualStoreDir, `.${pkgid_to_filename_1.default(childDepPath, ctx.lockfileDirectory)}`, 'node_modules', pkgName);
        }
        else if (allDeps[alias].indexOf('file:') === 0) {
            children[alias] = path.resolve(ctx.prefix, allDeps[alias].substr(5));
        }
        else if (!ctx.skipped.has(childRelDepPath)) {
            throw new Error(`${childRelDepPath} not found in ${constants_1.WANTED_LOCKFILE}`);
        }
    }
    return children;
}
const limitLinking = p_limit_1.default(16);
async function linkAllPkgs(storeController, depNodes, opts) {
    return Promise.all(depNodes.map(async (depNode) => {
        const filesResponse = await depNode.fetchingFiles;
        if (depNode.independent)
            return;
        return storeController.importPackage(depNode.centralLocation, depNode.peripheralLocation, {
            filesResponse,
            force: opts.force,
        });
    }));
}
async function linkAllBins(depGraph, opts) {
    return Promise.all(R.values(depGraph)
        .map((depNode) => limitLinking(async () => {
        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.includes(undefined)) { // tslint:disable-line
            await link_bins_1.default(depNode.modules, binPath, { warn: opts.warn });
        }
        else {
            const pkgs = await Promise.all(pkgSnapshots
                .filter(({ hasBin }) => hasBin)
                .map(async ({ peripheralLocation }) => ({
                location: peripheralLocation,
                manifest: await read_package_json_1.fromDir(peripheralLocation),
            })));
            await link_bins_1.linkBinsOfPackages(pkgs, binPath, { warn: opts.warn });
        }
        // link also the bundled dependencies` bins
        if (depNode.hasBundledDependencies) {
            const bundledModules = path.join(depNode.peripheralLocation, 'node_modules');
            await link_bins_1.default(bundledModules, binPath, { warn: opts.warn });
        }
    })));
}
async function linkAllModules(depNodes, opts) {
    return Promise.all(depNodes
        .filter(({ independent }) => !independent)
        .map(async (depNode) => {
        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;
            }, {});
        await Promise.all(R.keys(childrenToLink)
            .map(async (alias) => {
            // if (!pkg.installable && pkg.optional) return
            if (alias === depNode.name) {
                logger_1.default.warn({
                    message: `Cannot link dependency with name ${alias} to ${depNode.modules}. Dependency's name should differ from the parent's name.`,
                    prefix: opts.lockfileDirectory,
                });
                return;
            }
            await limitLinking(() => symlink_dependency_1.default(childrenToLink[alias], depNode.modules, alias));
        }));
    }));
}
