"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());
    });
};
const debug_1 = require('../debug');
const debug = debug_1.default('pnpm:install');
const npa = require('npm-package-arg');
const fs = require('mz/fs');
const logger = require('@zkochan/logger');
const path = require('path');
const resolve_1 = require('../resolve');
const mkdirp_1 = require('../fs/mkdirp');
const requireJson_1 = require('../fs/requireJson');
const relSymlink_1 = require('../fs/relSymlink');
const exists = require('exists-file');
const linkBundledDeps_1 = require('./linkBundledDeps');
const isAvailable_1 = require('./isAvailable');
const installMultiple_1 = require('../installMultiple');
const symlinkToModules_1 = require('./symlinkToModules');
/**
 * Installs a package.
 *
 * What it does:
 *
 * - resolve() - resolve from registry.npmjs.org
 * - fetch() - download tarball into node_modules/.store/{name}@{version}
 * - recurse into its dependencies
 * - symlink node_modules/{name}
 *
 * @param {Object} ctx - the context.
 * @param {Object} pkgMeta - meta info about the package to install.
 *
 * @example
 *     install(ctx, 'rimraf@2', './node_modules')
 */
function install(ctx, pkgMeta, modules, options) {
    return __awaiter(this, void 0, void 0, function* () {
        debug('installing ' + pkgMeta.rawSpec);
        if (!ctx.builds)
            ctx.builds = {};
        if (!ctx.fetches)
            ctx.fetches = {};
        // Preliminary spec data
        // => { raw, name, scope, type, spec, rawSpec }
        const spec = npa(pkgMeta.rawSpec);
        const optional = pkgMeta.optional || options.optional === true;
        // Dependency path to the current package. Not actually needed anmyore
        // outside getting its length
        // => ['babel-core@6.4.5', 'babylon@6.4.5', 'babel-runtime@5.8.35']
        const keypath = (options && options.keypath || []);
        const log = logger.fork(spec).log.bind(null, 'progress', pkgMeta.rawSpec);
        let installedPkg;
        try {
            // it might be a bundleDependency, in which case, don't bother
            const available = !options.force && (yield isAvailable_1.default(spec, modules));
            if (available) {
                installedPkg = yield saveCachedResolution();
                log('package.json', installedPkg.pkg);
            }
            else {
                const res = yield resolve_1.default(spec, {
                    log,
                    got: ctx.got,
                    root: options.parentRoot || options.root,
                    linkLocal: options.linkLocal,
                    tag: options.tag
                });
                const freshPkg = saveResolution(res);
                log('resolved', freshPkg);
                yield mkdirp_1.default(modules);
                const target = path.join(options.storePath, res.id);
                let installedPkgs = yield buildToStoreCached(ctx, target, freshPkg, log);
                const pkg = requireJson_1.default(path.join(target, '_', 'package.json'));
                yield symlinkToModules_1.default(path.join(target, '_'), modules);
                installedPkg = {
                    pkg,
                    optional,
                    keypath,
                    id: freshPkg.id,
                    name: spec.name,
                    fromCache: false,
                    dependencies: installedPkgs,
                };
                log('package.json', pkg);
            }
            if (!ctx.installs[installedPkg.id]) {
                ctx.installs[installedPkg.id] = installedPkg;
            }
            else {
                ctx.installs[installedPkg.id].optional = ctx.installs[installedPkg.id].optional && installedPkg.optional;
            }
            log('done');
            return installedPkg;
        }
        catch (err) {
            log('error', err);
            throw err;
        }
        // set metadata as fetched from resolve()
        function saveResolution(res) {
            return {
                optional,
                keypath,
                linkLocal: options.linkLocal,
                // Full name of package as it should be put in the store. Aim to make
                // this as friendly as possible as this will appear in stack traces.
                // => 'lodash@4.0.0'
                // => '@rstacruz!tap-spec@4.1.1'
                // => 'rstacruz!pnpm@0a1b382da'
                // => 'foobar@9a3b283ac'
                id: res.id,
                root: res.root,
                fetch: res.fetch,
                installationRoot: options.root,
                storePath: options.storePath,
                force: options.force,
                depth: options.depth,
                tag: options.tag
            };
        }
        function saveCachedResolution() {
            return __awaiter(this, void 0, void 0, function* () {
                const target = path.join(modules, spec.name);
                const stat = yield fs.lstat(target);
                if (stat.isSymbolicLink()) {
                    const linkPath = yield fs.readlink(target);
                    return save(path.resolve(linkPath, target));
                }
                return save(target);
                function save(fullpath) {
                    const data = requireJson_1.default(path.join(fullpath, 'package.json'));
                    return {
                        pkg: data,
                        id: path.basename(fullpath),
                        optional,
                        keypath,
                        name: spec.name,
                        fromCache: true,
                        dependencies: [],
                    };
                }
            });
        }
    });
}
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = install;
/**
 * Builds to `.store/lodash@4.0.0` (paths.target)
 * If an ongoing build is already working, use it. Also, if that ongoing build
 * is part of the dependency chain (ie, it's a circular dependency), use its stub
 */
function buildToStoreCached(ctx, target, buildInfo, log) {
    return __awaiter(this, void 0, void 0, function* () {
        // If a package is requested for a second time (usually when many packages depend
        // on the same thing), only resolve until it's fetched (not built).
        if (ctx.fetches[buildInfo.id]) {
            yield ctx.fetches[buildInfo.id];
            return [];
        }
        if (!(yield exists(target)) || buildInfo.force) {
            return memoize(ctx.builds, buildInfo.id, function () {
                return __awaiter(this, void 0, void 0, function* () {
                    yield memoize(ctx.fetches, buildInfo.id, () => fetchToStore(ctx, target, buildInfo, log));
                    return buildInStore(ctx, target, buildInfo, log);
                });
            });
        }
        if (buildInfo.keypath.length <= buildInfo.depth) {
            const pkg = requireJson_1.default(path.resolve(path.join(target, '_', 'package.json')));
            return installSubDeps(ctx, target, buildInfo, pkg, log);
        }
        return [];
    });
}
/**
 * Builds to `.store/lodash@4.0.0` (paths.target)
 * Fetches from npm, recurses to dependencies, runs lifecycle scripts, etc
 */
function fetchToStore(ctx, target, buildInfo, log) {
    return __awaiter(this, void 0, void 0, function* () {
        // download and untar
        log('download-queued');
        return buildInfo.fetch(path.join(target, '_'), { log, got: ctx.got });
        // TODO: this is the point it becomes partially useable.
        // ie, it can now be symlinked into .store/foo@1.0.0.
        // it is only here that it should be available for ciruclar dependencies.
    });
}
function buildInStore(ctx, target, buildInfo, log) {
    return __awaiter(this, void 0, void 0, function* () {
        const pkg = requireJson_1.default(path.resolve(path.join(target, '_', 'package.json')));
        log('package.json', pkg);
        yield linkBundledDeps_1.default(path.join(target, '_'));
        const installedPkgs = yield installSubDeps(ctx, target, buildInfo, pkg, log);
        // symlink itself; . -> node_modules/lodash@4.0.0
        // this way it can require itself
        yield symlinkSelf(target, pkg, buildInfo.keypath.length);
        ctx.piq = ctx.piq || [];
        ctx.piq.push({
            path: target,
            pkgId: buildInfo.id
        });
        return installedPkgs;
    });
}
function installSubDeps(ctx, target, buildInfo, pkg, log) {
    return __awaiter(this, void 0, void 0, function* () {
        // recurse down to dependencies
        log('dependencies');
        return installMultiple_1.default(ctx, pkg.dependencies || {}, pkg.optionalDependencies || {}, path.join(target, '_', 'node_modules'), {
            keypath: buildInfo.keypath.concat([buildInfo.id]),
            dependent: buildInfo.id,
            parentRoot: buildInfo.root,
            optional: buildInfo.optional,
            linkLocal: buildInfo.linkLocal,
            root: buildInfo.installationRoot,
            storePath: buildInfo.storePath,
            force: buildInfo.force,
            depth: buildInfo.depth,
            tag: buildInfo.tag
        });
    });
}
/**
 * Symlink a package into its own node_modules. this way, babel-runtime@5 can
 * require('babel-runtime') within itself.
 */
function symlinkSelf(target, pkg, depth) {
    return __awaiter(this, void 0, void 0, function* () {
        debug(`symlinkSelf ${pkg.name}`);
        if (depth === 0) {
            return;
        }
        yield mkdirp_1.default(path.join(target, 'node_modules'));
        const src = isScoped(pkg.name)
            ? path.join('..', '..', '_')
            : path.join('..', '_');
        yield relSymlink_1.default(src, path.join(target, 'node_modules', pkg.name));
    });
}
function isScoped(pkgName) {
    return pkgName.indexOf('/') !== -1;
}
/**
 * Save promises for later
 */
function memoize(locks, key, fn) {
    if (locks && locks[key])
        return locks[key];
    locks[key] = fn();
    return locks[key];
}
//# sourceMappingURL=index.js.map