"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const cafs_1 = require("@pnpm/cafs");
const core_loggers_1 = require("@pnpm/core-loggers");
const logger_1 = require("@pnpm/logger");
const pkgid_to_filename_1 = require("@pnpm/pkgid-to-filename");
const read_package_json_1 = require("@pnpm/read-package-json");
const loadJsonFile = require("load-json-file");
const fs = require("mz/fs");
const pDefer = require("p-defer");
const p_queue_1 = require("p-queue");
const path = require("path");
const pathTemp = require("path-temp");
const pShare = require("promise-share");
const R = require("ramda");
const renameOverwrite = require("rename-overwrite");
const ssri = require("ssri");
const safeDeferredPromise_1 = require("./safeDeferredPromise");
const TARBALL_INTEGRITY_FILENAME = 'tarball-integrity';
const packageRequestLogger = logger_1.default('package-requester');
const pickBundledManifest = R.pick([
    'bin',
    'bundledDependencies',
    'bundleDependencies',
    'dependencies',
    'directories',
    'engines',
    'name',
    'optionalDependencies',
    'os',
    'peerDependencies',
    'peerDependenciesMeta',
    'scripts',
    'version',
]);
function default_1(resolve, fetchers, opts) {
    opts = opts || {};
    const networkConcurrency = opts.networkConcurrency || 16;
    const requestsQueue = new p_queue_1.default({
        concurrency: networkConcurrency,
    });
    requestsQueue['counter'] = 0; // tslint:disable-line
    requestsQueue['concurrency'] = networkConcurrency; // tslint:disable-line
    const cafsDir = path.join(opts.storeDir, 'files');
    const cafs = cafs_1.default(cafsDir, opts.ignoreFile);
    const getFilePathInCafs = cafs_1.getFilePathInCafs.bind(null, cafsDir);
    const fetch = fetcher.bind(null, fetchers, cafs);
    const fetchPackageToStore = fetchToStore.bind(null, {
        checkFilesIntegrity: cafs_1.checkFilesIntegrity.bind(null, cafsDir),
        fetch,
        fetchingLocker: new Map(),
        getFilePathByModeInCafs: cafs_1.getFilePathByModeInCafs.bind(null, cafsDir),
        getFilePathInCafs,
        requestsQueue,
        storeDir: opts.storeDir,
        verifyStoreIntegrity: opts.verifyStoreIntegrity,
    });
    const requestPackage = resolveAndFetch.bind(null, {
        fetchPackageToStore,
        requestsQueue,
        resolve,
        storeDir: opts.storeDir,
        verifyStoreIntegrity: opts.verifyStoreIntegrity,
    });
    return Object.assign(requestPackage, { cafs, fetchPackageToStore, requestPackage });
}
exports.default = default_1;
async function resolveAndFetch(ctx, wantedDependency, options) {
    try {
        let latest;
        let manifest;
        let normalizedPref;
        let resolution = options.currentResolution;
        let pkgId = options.currentPackageId;
        const skipResolution = resolution && !options.update;
        let forceFetch = false;
        let updated = false;
        let resolvedVia;
        // When fetching is skipped, resolution cannot be skipped.
        // We need the package's manifest when doing `lockfile-only` installs.
        // When we don't fetch, the only way to get the package's manifest is via resolving it.
        //
        // The resolution step is never skipped for local dependencies.
        if (!skipResolution || options.skipFetch || (pkgId === null || pkgId === void 0 ? void 0 : pkgId.startsWith('file:'))) {
            const resolveResult = await ctx.requestsQueue.add(() => ctx.resolve(wantedDependency, {
                alwaysTryWorkspacePackages: options.alwaysTryWorkspacePackages,
                defaultTag: options.defaultTag,
                lockfileDir: options.lockfileDir,
                preferredVersions: options.preferredVersions,
                projectDir: options.projectDir,
                registry: options.registry,
                workspacePackages: options.workspacePackages,
            }), { priority: options.downloadPriority });
            manifest = resolveResult.manifest;
            latest = resolveResult.latest;
            resolvedVia = resolveResult.resolvedVia;
            // If the integrity of a local tarball dependency has changed,
            // the local tarball should be unpacked, so a fetch to the store should be forced
            forceFetch = Boolean(options.currentResolution && (pkgId === null || pkgId === void 0 ? void 0 : pkgId.startsWith('file:')) &&
                options.currentResolution['integrity'] !== resolveResult.resolution['integrity'] // tslint:disable-line:no-string-literal
            );
            if (!skipResolution || forceFetch) {
                updated = pkgId !== resolveResult.id || !resolution || forceFetch;
                // Keep the lockfile resolution when possible
                // to keep the original shasum.
                if (updated) {
                    resolution = resolveResult.resolution;
                }
                pkgId = resolveResult.id;
                normalizedPref = resolveResult.normalizedPref;
            }
        }
        const id = pkgId;
        if (resolution.type === 'directory') {
            if (!manifest) {
                throw new Error(`Couldn't read package.json of local dependency ${wantedDependency.alias ? wantedDependency.alias + '@' : ''}${wantedDependency.pref}`);
            }
            return {
                body: {
                    id,
                    isLocal: true,
                    manifest,
                    normalizedPref,
                    resolution: resolution,
                    resolvedVia,
                    updated,
                },
            };
        }
        // We can skip fetching the package only if the manifest
        // is present after resolution
        if (options.skipFetch && manifest) {
            return {
                body: {
                    id,
                    isLocal: false,
                    latest,
                    manifest,
                    normalizedPref,
                    resolution,
                    resolvedVia,
                    updated,
                },
            };
        }
        const fetchResult = ctx.fetchPackageToStore({
            fetchRawManifest: updated || !manifest,
            force: forceFetch,
            lockfileDir: options.lockfileDir,
            pkgId: id,
            resolution: resolution,
        });
        return {
            body: {
                id,
                isLocal: false,
                latest,
                manifest,
                normalizedPref,
                resolution,
                resolvedVia,
                updated,
            },
            bundledManifest: fetchResult.bundledManifest,
            files: fetchResult.files,
            filesIndexFile: fetchResult.filesIndexFile,
            finishing: fetchResult.finishing,
        };
    }
    catch (err) {
        throw err;
    }
}
function fetchToStore(ctx, opts) {
    const targetRelative = pkgid_to_filename_1.default(opts.pkgId, opts.lockfileDir);
    const target = path.join(ctx.storeDir, targetRelative);
    if (!ctx.fetchingLocker.has(opts.pkgId)) {
        const bundledManifest = pDefer();
        const files = pDefer();
        const finishing = pDefer();
        const filesIndexFile = opts.resolution['integrity']
            ? ctx.getFilePathInCafs(opts.resolution['integrity'], 'index')
            : path.join(target, 'integrity.json');
        doFetchToStore(filesIndexFile, bundledManifest, files, finishing); // tslint:disable-line
        if (opts.fetchRawManifest) {
            ctx.fetchingLocker.set(opts.pkgId, {
                bundledManifest: removeKeyOnFail(bundledManifest.promise),
                files: removeKeyOnFail(files.promise),
                filesIndexFile,
                finishing: removeKeyOnFail(finishing.promise),
            });
        }
        else {
            ctx.fetchingLocker.set(opts.pkgId, {
                files: removeKeyOnFail(files.promise),
                filesIndexFile,
                finishing: removeKeyOnFail(finishing.promise),
            });
        }
        // When files resolves, the cached result has to set fromStore to true, without
        // affecting previous invocations: so we need to replace the cache.
        //
        // Changing the value of fromStore is needed for correct reporting of `pnpm server`.
        // Otherwise, if a package was not in store when the server started, it will be always
        // reported as "downloaded" instead of "reused".
        files.promise.then((cache) => {
            // If it's already in the store, we don't need to update the cache
            if (cache.fromStore) {
                return;
            }
            const tmp = ctx.fetchingLocker.get(opts.pkgId);
            // If fetching failed then it was removed from the cache.
            // It is OK. In that case there is no need to update it.
            if (!tmp)
                return;
            ctx.fetchingLocker.set(opts.pkgId, {
                ...tmp,
                files: Promise.resolve({
                    ...cache,
                    fromStore: true,
                }),
            });
        })
            .catch(() => {
            ctx.fetchingLocker.delete(opts.pkgId);
        });
    }
    const result = ctx.fetchingLocker.get(opts.pkgId);
    if (opts.fetchRawManifest && !result.bundledManifest) {
        result.bundledManifest = removeKeyOnFail(result.files.then(({ filesIndex }) => {
            const { integrity, mode } = filesIndex['package.json'];
            const manifestPath = ctx.getFilePathByModeInCafs(integrity, mode);
            return readBundledManifest(manifestPath);
        }));
    }
    return {
        bundledManifest: result.bundledManifest ? pShare(result.bundledManifest) : undefined,
        files: pShare(result.files),
        filesIndexFile: result.filesIndexFile,
        finishing: pShare(result.finishing),
    };
    async function removeKeyOnFail(p) {
        try {
            return await p;
        }
        catch (err) {
            ctx.fetchingLocker.delete(opts.pkgId);
            throw err;
        }
    }
    async function doFetchToStore(filesIndexFile, bundledManifest, files, finishing) {
        try {
            const isLocalTarballDep = opts.pkgId.startsWith('file:');
            if (!opts.force &&
                (isLocalTarballDep === false ||
                    await tarballIsUpToDate(opts.resolution, target, opts.lockfileDir) // tslint:disable-line
                )) {
                let pkgFilesIndex;
                try {
                    pkgFilesIndex = await loadJsonFile(filesIndexFile);
                }
                catch (err) {
                    // ignoring. It is fine if the integrity file is not present. Just refetch the package
                }
                // if target exists and it wasn't modified, then no need to refetch it
                if (pkgFilesIndex && pkgFilesIndex.files) {
                    const manifest = opts.fetchRawManifest
                        ? safeDeferredPromise_1.default()
                        : undefined;
                    const verified = await ctx.checkFilesIntegrity(pkgFilesIndex.files, manifest);
                    if (verified) {
                        files.resolve({
                            filesIndex: pkgFilesIndex.files,
                            fromStore: true,
                            sideEffects: pkgFilesIndex.sideEffects,
                        });
                        if (manifest) {
                            manifest()
                                .then((manifest) => bundledManifest.resolve(pickBundledManifest(manifest)))
                                .catch(bundledManifest.reject);
                        }
                        finishing.resolve(undefined);
                        return;
                    }
                    packageRequestLogger.warn({
                        message: `Refetching ${target} to store. It was either modified or had no integrity checksums`,
                        prefix: opts.lockfileDir,
                    });
                }
            }
            // We fetch into targetStage directory first and then fs.rename() it to the
            // target directory.
            // Tarballs are requested first because they are bigger than metadata files.
            // However, when one line is left available, allow it to be picked up by a metadata request.
            // This is done in order to avoid situations when tarballs are downloaded in chunks
            // As much tarballs should be downloaded simultaneously as possible.
            const priority = (++ctx.requestsQueue['counter'] % ctx.requestsQueue['concurrency'] === 0 ? -1 : 1) * 1000; // tslint:disable-line
            const fetchManifest = opts.fetchRawManifest
                ? safeDeferredPromise_1.default()
                : undefined;
            if (fetchManifest) {
                fetchManifest()
                    .then((manifest) => bundledManifest.resolve(pickBundledManifest(manifest)))
                    .catch(bundledManifest.reject);
            }
            const fetchedPackage = await ctx.requestsQueue.add(() => ctx.fetch(opts.pkgId, opts.resolution, {
                lockfileDir: opts.lockfileDir,
                manifest: fetchManifest,
                onProgress: (downloaded) => {
                    core_loggers_1.fetchingProgressLogger.debug({
                        downloaded,
                        packageId: opts.pkgId,
                        status: 'in_progress',
                    });
                },
                onStart: (size, attempt) => {
                    core_loggers_1.fetchingProgressLogger.debug({
                        attempt,
                        packageId: opts.pkgId,
                        size,
                        status: 'started',
                    });
                },
            }), { priority });
            const filesIndex = fetchedPackage.filesIndex;
            // Ideally, files wouldn't care about when integrity is calculated.
            // However, we can only rename the temp folder once we know the package name.
            // And we cannot rename the temp folder till we're calculating integrities.
            const integrity = {};
            await Promise.all(Object.keys(filesIndex)
                .map(async (filename) => {
                const fileIntegrity = await filesIndex[filename].generatingIntegrity;
                integrity[filename] = {
                    integrity: fileIntegrity.toString(),
                    mode: filesIndex[filename].mode,
                    size: filesIndex[filename].size,
                };
            }));
            await writeJsonFile(filesIndexFile, { files: integrity });
            finishing.resolve(undefined);
            if (isLocalTarballDep && opts.resolution['integrity']) { // tslint:disable-line:no-string-literal
                await fs.mkdir(target, { recursive: true });
                await fs.writeFile(path.join(target, TARBALL_INTEGRITY_FILENAME), opts.resolution['integrity'], 'utf8'); // tslint:disable-line:no-string-literal
            }
            files.resolve({
                filesIndex: integrity,
                fromStore: false,
            });
        }
        catch (err) {
            files.reject(err);
            if (opts.fetchRawManifest) {
                bundledManifest.reject(err);
            }
        }
    }
}
async function writeJsonFile(filePath, data) {
    const targetDir = path.dirname(filePath);
    // TODO: use the API of @pnpm/cafs to write this file
    // There is actually no need to create the directory in 99% of cases.
    // So by using cafs API, we'll improve performance.
    await fs.mkdir(targetDir, { recursive: true });
    const temp = pathTemp(targetDir);
    await fs.writeFile(temp, JSON.stringify(data));
    await renameOverwrite(temp, filePath);
}
async function readBundledManifest(pkgJsonPath) {
    return pickBundledManifest(await read_package_json_1.default(pkgJsonPath));
}
async function tarballIsUpToDate(resolution, pkgInStoreLocation, lockfileDir) {
    let currentIntegrity;
    try {
        currentIntegrity = (await fs.readFile(path.join(pkgInStoreLocation, TARBALL_INTEGRITY_FILENAME), 'utf8'));
    }
    catch (err) {
        return false;
    }
    if (resolution.integrity && currentIntegrity !== resolution.integrity)
        return false;
    const tarball = path.join(lockfileDir, resolution.tarball.slice(5));
    const tarballStream = fs.createReadStream(tarball);
    try {
        return Boolean(await ssri.checkStream(tarballStream, currentIntegrity));
    }
    catch (err) {
        return false;
    }
}
async function fetcher(fetcherByHostingType, cafs, packageId, resolution, opts) {
    const fetch = fetcherByHostingType[resolution.type || 'tarball'];
    if (!fetch) {
        throw new Error(`Fetching for dependency type "${resolution.type}" is not supported`);
    }
    try {
        return await fetch(cafs, resolution, opts);
    }
    catch (err) {
        packageRequestLogger.warn({
            message: `Fetching ${packageId} failed!`,
            prefix: opts.lockfileDir,
        });
        throw err;
    }
}
//# sourceMappingURL=packageRequester.js.map