"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 hoist_1 = require("@pnpm/hoist");
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_package_json_1 = require("@pnpm/read-package-json");
const read_project_manifest_1 = require("@pnpm/read-project-manifest");
const symlink_dependency_1 = require("@pnpm/symlink-dependency");
const dp = require("dependency-path");
const fs = require("mz/fs");
const pLimit = require("p-limit");
const path = require("path");
const pathAbsolute = require("path-absolute");
const R = require("ramda");
const realpathMissing = require("realpath-missing");
const brokenModulesLogger = logger_1.default('_broken_node_modules');
exports.default = async (opts) => {
    var _a, _b, _c, _d;
    const reporter = opts.reporter;
    if (reporter) {
        logger_1.streamParser.on('data', reporter);
    }
    const lockfileDir = opts.lockfileDir;
    const wantedLockfile = opts.wantedLockfile || await lockfile_file_1.readWantedLockfile(lockfileDir, { ignoreIncompatible: false });
    if (!wantedLockfile) {
        throw new Error(`Headless installation requires a ${constants_1.WANTED_LOCKFILE} file`);
    }
    const relativeModulesDir = (_a = opts.modulesDir) !== null && _a !== void 0 ? _a : 'node_modules';
    const rootModulesDir = await realpathMissing(path.join(lockfileDir, relativeModulesDir));
    const virtualStoreDir = pathAbsolute((_b = opts.virtualStoreDir) !== null && _b !== void 0 ? _b : path.join(relativeModulesDir, '.pnpm'), lockfileDir);
    const currentLockfile = opts.currentLockfile || await lockfile_file_1.readCurrentLockfile(virtualStoreDir, { ignoreIncompatible: false });
    const hoistedModulesDir = path.join(virtualStoreDir, 'node_modules');
    const publicHoistedModulesDir = rootModulesDir;
    for (const { id, manifest, rootDir } of opts.projects) {
        if (!lockfile_utils_1.satisfiesPackageManifest(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(lockfileDir, path.join(rootDir, 'package.json')));
        }
    }
    const scriptsOpts = {
        optional: false,
        rawConfig: opts.rawConfig,
        stdio: opts.ownLifecycleHooksStdio || 'inherit',
        unsafePerm: opts.unsafePerm || false,
    };
    if (!opts.ignoreScripts) {
        await lifecycle_1.runLifecycleHooksConcurrently(['preinstall'], opts.projects, opts.childConcurrency || 5, scriptsOpts);
    }
    const skipped = opts.skipped || new Set();
    if (currentLockfile) {
        await modules_cleaner_1.prune(opts.projects, {
            currentLockfile,
            dryRun: false,
            hoistedDependencies: opts.hoistedDependencies,
            hoistedModulesDir: opts.hoistPattern && hoistedModulesDir || undefined,
            include: opts.include,
            lockfileDir,
            pruneStore: opts.pruneStore,
            publicHoistedModulesDir: opts.publicHoistPattern && publicHoistedModulesDir || undefined,
            registries: opts.registries,
            skipped,
            storeController: opts.storeController,
            virtualStoreDir,
            wantedLockfile,
        });
    }
    else {
        core_loggers_1.statsLogger.debug({
            prefix: lockfileDir,
            removed: 0,
        });
    }
    core_loggers_1.stageLogger.debug({
        prefix: lockfileDir,
        stage: 'importing_started',
    });
    const filterOpts = {
        include: opts.include,
        registries: opts.registries,
        skipped,
    };
    const filteredLockfile = filter_lockfile_1.filterLockfileByImportersAndEngine(wantedLockfile, opts.projects.map(({ id }) => id), {
        ...filterOpts,
        currentEngine: opts.currentEngine,
        engineStrict: opts.engineStrict,
        failOnMissingDependencies: true,
        includeIncompatiblePackages: opts.force === true,
        lockfileDir,
    });
    const { directDependenciesByImporterId, graph } = await lockfileToDepGraph(filteredLockfile, opts.force ? null : currentLockfile, {
        ...opts,
        importerIds: opts.projects.map(({ id }) => id),
        lockfileDir,
        skipped,
        virtualStoreDir,
    });
    const depNodes = R.values(graph);
    core_loggers_1.statsLogger.debug({
        added: depNodes.length,
        prefix: lockfileDir,
    });
    await Promise.all([
        linkAllModules(depNodes, {
            lockfileDir,
            optional: opts.include.optionalDependencies,
        }),
        linkAllPkgs(opts.storeController, depNodes, {
            force: opts.force,
            targetEngine: opts.sideEffectsCacheRead && constants_1.ENGINE_NAME || undefined,
        }),
    ]);
    core_loggers_1.stageLogger.debug({
        prefix: lockfileDir,
        stage: 'importing_done',
    });
    function warn(message) {
        logger_1.default.warn({
            message,
            prefix: lockfileDir,
        });
    }
    const rootImporterWithFlatModules = (opts.hoistPattern || opts.publicHoistPattern) && opts.projects.find(({ id }) => id === '.');
    let newHoistedDependencies;
    if (rootImporterWithFlatModules) {
        newHoistedDependencies = await hoist_1.default({
            lockfile: filteredLockfile,
            lockfileDir,
            privateHoistedModulesDir: hoistedModulesDir,
            privateHoistPattern: (_c = opts.hoistPattern) !== null && _c !== void 0 ? _c : [],
            publicHoistedModulesDir,
            publicHoistPattern: (_d = opts.publicHoistPattern) !== null && _d !== void 0 ? _d : [],
            virtualStoreDir,
        });
    }
    else {
        newHoistedDependencies = {};
    }
    await Promise.all(opts.projects.map(async ({ rootDir, id, manifest, modulesDir }) => {
        await linkRootPackages(filteredLockfile, {
            importerId: id,
            importerModulesDir: modulesDir,
            lockfileDir,
            projectDir: rootDir,
            projects: opts.projects,
            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.packageManifestLogger.debug({
            prefix: rootDir,
            updated: manifest,
        });
    }));
    if (opts.ignoreScripts) {
        for (const { id, manifest } of opts.projects) {
            if (opts.ignoreScripts && (manifest === null || manifest === void 0 ? void 0 : 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(({ depPath }) => depPath));
    }
    else {
        const directNodes = new Set();
        for (const { id } of opts.projects) {
            R
                .values(directDependenciesByImporterId[id])
                .filter((loc) => graph[loc])
                .forEach((loc) => {
                directNodes.add(loc);
            });
        }
        const extraBinPaths = [...opts.extraBinPaths || []];
        if (opts.hoistPattern) {
            extraBinPaths.unshift(path.join(virtualStoreDir, 'node_modules/.bin'));
        }
        await build_modules_1.default(graph, Array.from(directNodes), {
            childConcurrency: opts.childConcurrency,
            extraBinPaths,
            lockfileDir,
            optional: opts.include.optionalDependencies,
            rawConfig: opts.rawConfig,
            rootModulesDir: 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.projects.map(linkBinsOfImporter));
    if (currentLockfile && !R.equals(opts.projects.map(({ id }) => id).sort(), Object.keys(filteredLockfile.importers).sort())) {
        Object.assign(filteredLockfile.packages, currentLockfile.packages);
    }
    await lockfile_file_1.writeCurrentLockfile(virtualStoreDir, filteredLockfile);
    await modules_yaml_1.write(rootModulesDir, {
        hoistedDependencies: newHoistedDependencies,
        hoistPattern: opts.hoistPattern,
        included: opts.include,
        layoutVersion: constants_1.LAYOUT_VERSION,
        packageManager: `${opts.packageManager.name}@${opts.packageManager.version}`,
        pendingBuilds: opts.pendingBuilds,
        publicHoistPattern: opts.publicHoistPattern,
        registries: opts.registries,
        skipped: Array.from(skipped),
        storeDir: opts.storeDir,
        virtualStoreDir,
    });
    // waiting till package requests are finished
    await Promise.all(depNodes.map(({ finishing }) => finishing));
    core_loggers_1.summaryLogger.debug({ prefix: lockfileDir });
    await opts.storeController.close();
    if (!opts.ignoreScripts) {
        await lifecycle_1.runLifecycleHooksConcurrently(['install', 'postinstall', 'prepublish', 'prepare'], opts.projects, opts.childConcurrency || 5, scriptsOpts);
    }
    if (reporter) {
        logger_1.streamParser.removeListener('data', reporter);
    }
};
function linkBinsOfImporter({ modulesDir, binsDir, rootDir }) {
    const warn = (message) => logger_1.default.warn({ message, prefix: rootDir });
    return link_bins_1.default(modulesDir, binsDir, {
        allowExoticManifests: true,
        warn,
    });
}
async function linkRootPackages(lockfile, opts) {
    const importerManifestsByImporterId = {};
    for (const { id, manifest } of opts.projects) {
        importerManifestsByImporterId[id] = manifest;
    }
    const projectSnapshot = lockfile.importers[opts.importerId];
    const allDeps = {
        ...projectSnapshot.devDependencies,
        ...projectSnapshot.dependencies,
        ...projectSnapshot.optionalDependencies,
    };
    return Promise.all(Object.keys(allDeps)
        .map(async (alias) => {
        var _a, _b, _c, _d, _e;
        if (allDeps[alias].startsWith('link:')) {
            const isDev = (_a = projectSnapshot.devDependencies) === null || _a === void 0 ? void 0 : _a[alias];
            const isOptional = (_b = projectSnapshot.optionalDependencies) === null || _b === void 0 ? void 0 : _b[alias];
            const packageDir = path.join(opts.projectDir, allDeps[alias].substr(5));
            const linkedPackage = await (async () => {
                const importerId = lockfile_file_1.getLockfileImporterId(opts.lockfileDir, packageDir);
                if (importerManifestsByImporterId[importerId]) {
                    return importerManifestsByImporterId[importerId];
                }
                // TODO: cover this case with a test
                return await read_project_manifest_1.readProjectManifestOnly(packageDir);
            })();
            await symlink_dependency_1.symlinkDirectRootDependency(packageDir, opts.importerModulesDir, alias, {
                fromDependenciesField: isDev && 'devDependencies' ||
                    isOptional && 'optionalDependencies' ||
                    'dependencies',
                linkedPackage,
                prefix: opts.projectDir,
            });
            return;
        }
        const dir = opts.rootDependencies[alias];
        // Skipping linked packages
        if (!dir) {
            return;
        }
        if ((await symlink_dependency_1.default(dir, opts.importerModulesDir, alias)).reused) {
            return;
        }
        const isDev = (_c = projectSnapshot.devDependencies) === null || _c === void 0 ? void 0 : _c[alias];
        const isOptional = (_d = projectSnapshot.optionalDependencies) === null || _d === void 0 ? void 0 : _d[alias];
        const depPath = dp.refToRelative(allDeps[alias], alias);
        if (depPath === null)
            return;
        const pkgSnapshot = (_e = lockfile.packages) === null || _e === void 0 ? void 0 : _e[depPath];
        if (!pkgSnapshot)
            return; // this won't ever happen. Just making typescript happy
        const pkgId = pkgSnapshot.id || dp.refToAbsolute(allDeps[alias], alias, opts.registries) || undefined;
        const pkgInfo = lockfile_utils_1.nameVerFromPkgSnapshot(depPath, 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.projectDir,
        });
    }));
}
async function lockfileToDepGraph(lockfile, currentLockfile, opts) {
    var _a;
    const currentPackages = (_a = currentLockfile === null || currentLockfile === void 0 ? void 0 : currentLockfile.packages) !== null && _a !== void 0 ? _a : {};
    const graph = {};
    let directDependenciesByImporterId = {};
    if (lockfile.packages) {
        const pkgSnapshotByLocation = {};
        await Promise.all(Object.keys(lockfile.packages).map(async (depPath) => {
            if (opts.skipped.has(depPath))
                return;
            const pkgSnapshot = lockfile.packages[depPath];
            // TODO: optimize. This info can be already returned by pkgSnapshotToResolution()
            const pkgName = lockfile_utils_1.nameVerFromPkgSnapshot(depPath, pkgSnapshot).name;
            const modules = path.join(opts.virtualStoreDir, pkgid_to_filename_1.default(depPath, opts.lockfileDir), 'node_modules');
            const packageId = lockfile_utils_1.packageIdFromSnapshot(depPath, pkgSnapshot, opts.registries);
            const dir = path.join(modules, pkgName);
            if (currentPackages[depPath] && R.equals(currentPackages[depPath].dependencies, lockfile.packages[depPath].dependencies) &&
                R.equals(currentPackages[depPath].optionalDependencies, lockfile.packages[depPath].optionalDependencies)) {
                if (await fs.exists(dir)) {
                    return;
                }
                brokenModulesLogger.debug({
                    missing: dir,
                });
            }
            const resolution = lockfile_utils_1.pkgSnapshotToResolution(depPath, pkgSnapshot, opts.registries);
            core_loggers_1.progressLogger.debug({
                packageId,
                requester: opts.lockfileDir,
                status: 'resolved',
            });
            let fetchResponse = opts.storeController.fetchPackage({
                force: false,
                lockfileDir: opts.lockfileDir,
                pkgId: packageId,
                resolution,
            });
            if (fetchResponse instanceof Promise)
                fetchResponse = await fetchResponse;
            fetchResponse.files() // tslint:disable-line
                .then(({ fromStore }) => {
                core_loggers_1.progressLogger.debug({
                    packageId,
                    requester: opts.lockfileDir,
                    status: fromStore
                        ? 'found_in_store' : 'fetched',
                });
            })
                .catch(() => {
                // ignore
            });
            graph[dir] = {
                children: {},
                depPath,
                dir,
                fetchingFiles: fetchResponse.files,
                filesIndexFile: fetchResponse.filesIndexFile,
                finishing: fetchResponse.finishing,
                hasBin: pkgSnapshot.hasBin === true,
                hasBundledDependencies: !!pkgSnapshot.bundledDependencies,
                modules,
                name: pkgName,
                optional: !!pkgSnapshot.optional,
                optionalDependencies: new Set(R.keys(pkgSnapshot.optionalDependencies)),
                prepare: pkgSnapshot.prepare === true,
                requiresBuild: pkgSnapshot.requiresBuild === true,
            };
            pkgSnapshotByLocation[dir] = pkgSnapshot;
        }));
        const ctx = {
            force: opts.force,
            graph,
            lockfileDir: opts.lockfileDir,
            pkgSnapshotsByDepPaths: lockfile.packages,
            registries: opts.registries,
            sideEffectsCacheRead: opts.sideEffectsCacheRead,
            skipped: opts.skipped,
            storeController: opts.storeController,
            storeDir: opts.storeDir,
            virtualStoreDir: opts.virtualStoreDir,
        };
        for (const dir of R.keys(graph)) {
            const pkgSnapshot = pkgSnapshotByLocation[dir];
            const allDeps = {
                ...pkgSnapshot.dependencies,
                ...(opts.include.optionalDependencies ? pkgSnapshot.optionalDependencies : {}),
            };
            graph[dir].children = await getChildrenPaths(ctx, allDeps);
        }
        for (const importerId of opts.importerIds) {
            const projectSnapshot = lockfile.importers[importerId];
            const rootDeps = {
                ...(opts.include.devDependencies ? projectSnapshot.devDependencies : {}),
                ...(opts.include.dependencies ? projectSnapshot.dependencies : {}),
                ...(opts.include.optionalDependencies ? projectSnapshot.optionalDependencies : {}),
            };
            directDependenciesByImporterId[importerId] = await getChildrenPaths(ctx, rootDeps);
        }
    }
    return { graph, directDependenciesByImporterId };
}
async function getChildrenPaths(ctx, allDeps) {
    const children = {};
    for (const alias of Object.keys(allDeps)) {
        const childDepPath = dp.refToAbsolute(allDeps[alias], alias, ctx.registries);
        if (childDepPath === null) {
            children[alias] = path.resolve(ctx.lockfileDir, allDeps[alias].substr(5));
            continue;
        }
        const childRelDepPath = dp.refToRelative(allDeps[alias], alias);
        const childPkgSnapshot = ctx.pkgSnapshotsByDepPaths[childRelDepPath];
        if (ctx.graph[childRelDepPath]) {
            children[alias] = ctx.graph[childRelDepPath].dir;
        }
        else if (childPkgSnapshot) {
            const pkgName = lockfile_utils_1.nameVerFromPkgSnapshot(childRelDepPath, childPkgSnapshot).name;
            children[alias] = path.join(ctx.virtualStoreDir, pkgid_to_filename_1.default(childRelDepPath, ctx.lockfileDir), 'node_modules', pkgName);
        }
        else if (allDeps[alias].indexOf('file:') === 0) {
            children[alias] = path.resolve(ctx.lockfileDir, 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 = pLimit(16);
async function linkAllPkgs(storeController, depNodes, opts) {
    return Promise.all(depNodes.map(async (depNode) => {
        const filesResponse = await depNode.fetchingFiles();
        const { isBuilt } = await storeController.importPackage(depNode.dir, {
            filesResponse,
            force: opts.force,
            targetEngine: opts.targetEngine,
        });
        depNode.isBuilt = isBuilt;
    }));
}
async function linkAllBins(depGraph, opts) {
    return Promise.all(R.values(depGraph)
        .map((depNode) => limitLinking(async () => {
        const childrenToLink = opts.optional
            ? depNode.children
            : Object.keys(depNode.children)
                .reduce((nonOptionalChildren, childAlias) => {
                if (!depNode.optionalDependencies.has(childAlias)) {
                    nonOptionalChildren[childAlias] = depNode.children[childAlias];
                }
                return nonOptionalChildren;
            }, {});
        const binPath = path.join(depNode.dir, '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 ({ dir }) => ({
                location: dir,
                manifest: await read_package_json_1.fromDir(dir),
            })));
            await link_bins_1.linkBinsOfPackages(pkgs, binPath, { warn: opts.warn });
        }
        // link also the bundled dependencies` bins
        if (depNode.hasBundledDependencies) {
            const bundledModules = path.join(depNode.dir, 'node_modules');
            await link_bins_1.default(bundledModules, binPath, { warn: opts.warn });
        }
    })));
}
async function linkAllModules(depNodes, opts) {
    return Promise.all(depNodes
        .map(async (depNode) => {
        const childrenToLink = opts.optional
            ? depNode.children
            : Object.keys(depNode.children)
                .reduce((nonOptionalChildren, childAlias) => {
                if (!depNode.optionalDependencies.has(childAlias)) {
                    nonOptionalChildren[childAlias] = depNode.children[childAlias];
                }
                return nonOptionalChildren;
            }, {});
        await Promise.all(Object.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.lockfileDir,
                });
                return;
            }
            await limitLinking(() => symlink_dependency_1.default(childrenToLink[alias], depNode.modules, alias));
        }));
    }));
}
//# sourceMappingURL=index.js.map