// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import { FileSystem, InternalError, Import } from '@rushstack/node-core-library';
import { BaseShrinkwrapFile } from '../base/BaseShrinkwrapFile';
import { RushConstants } from '../RushConstants';
import { PackageNameParsers } from '../../api/PackageNameParsers';
const lockfileModule = Import.lazy('@yarnpkg/lockfile', require);
/**
 * Support for consuming the "yarn.lock" file.
 *
 * Yarn refers to its shrinkwrap file as a "lock file", even though it has nothing to do
 * with file locking.  Apparently this was based on a convention of the Ruby bundler.
 * Since Rush has to work interchangeably with 3 different package managers, here we refer
 * generically to yarn.lock as a "shrinkwrap file".
 *
 * If Rush's Yarn support gains popularity, we will try to improve the wording of
 * logging messages to use terminology more consistent with Yarn's own documentation.
 */
export class YarnShrinkwrapFile extends BaseShrinkwrapFile {
    constructor(shrinkwrapJson) {
        super();
        this._shrinkwrapJson = shrinkwrapJson;
        this._tempProjectNames = [];
        const seenEntries = new Set();
        for (const key of Object.keys(this._shrinkwrapJson)) {
            // Example key:
            const packageNameAndSemVer = YarnShrinkwrapFile._decodePackageNameAndSemVer(key);
            // If it starts with @rush-temp, then include it:
            if (PackageNameParsers.permissive.getScope(packageNameAndSemVer.packageName) ===
                RushConstants.rushTempNpmScope) {
                if (!/^file:/i.test(packageNameAndSemVer.semVerRange)) {
                    // Sanity check to make sure this is a real package.
                    // (Nobody should ever have an actual dependency on an "@rush-temp/" package.
                    throw new Error('Unexpected package/semver expression found in the Yarn shrinkwrap file (yarn.lock): ' +
                        JSON.stringify(key));
                }
                if (!seenEntries.add(packageNameAndSemVer.packageName)) {
                    // Sanity check -- this should never happen
                    throw new Error('Duplicate @rush-temp package found in the Yarn shrinkwrap file (yarn.lock): ' +
                        JSON.stringify(key));
                }
                this._tempProjectNames.push(packageNameAndSemVer.packageName);
                const entry = this._shrinkwrapJson[key];
                // Yarn fails installation if the integrity hash does not match a "file://" reference to a tarball.
                // This is incorrect:  Normally a mismatched integrity hash does indicate a corrupted download,
                // since an NPM registry normally guarantees that a specific version number cannot be republished
                // with different content.  But this is NOT true for a "file://" reference, and there are valid
                // reasons why someone would update the file.  (PNPM handles this correctly, by simply reinstalling
                // the tarball if its hash has changed.)
                //
                // As a workaround, we can simply remove the hashes from the shrinkwrap file.  We will convert this:
                //   "file:./projects/my-project.tgz#80cefe05fd715e65219d1ed481209dc4023408aa"
                // ..to this:
                //   "file:./projects/my-project.tgz"
                const indexOfHash = entry.resolved.indexOf('#');
                if (indexOfHash >= 0) {
                    entry.resolved = entry.resolved.substring(0, indexOfHash);
                }
            }
        }
        this._tempProjectNames.sort(); // make the result deterministic
        // We don't support Yarn workspaces yet
        this.isWorkspaceCompatible = false;
    }
    static loadFromFile(shrinkwrapFilename) {
        try {
            const shrinkwrapContent = FileSystem.readFile(shrinkwrapFilename);
            return YarnShrinkwrapFile.loadFromString(shrinkwrapContent);
        }
        catch (error) {
            if (FileSystem.isNotExistError(error)) {
                return undefined; // file does not exist
            }
            throw new Error(`Error reading "${shrinkwrapFilename}":\n  ${error.message}`);
        }
    }
    static loadFromString(shrinkwrapContent) {
        const shrinkwrapJson = lockfileModule.parse(shrinkwrapContent);
        return new YarnShrinkwrapFile(shrinkwrapJson.object);
    }
    /**
     * The `@yarnpkg/lockfile` API only partially deserializes its data, and expects the caller
     * to parse the yarn.lock lookup keys (sometimes called a "pattern").
     *
     * Example input:  "js-tokens@^3.0.0 || ^4.0.0"
     * Example output: { packageName: "js-tokens", semVerRange: "^3.0.0 || ^4.0.0" }
     */
    static _decodePackageNameAndSemVer(packageNameAndSemVer) {
        const result = YarnShrinkwrapFile._packageNameAndSemVerRegExp.exec(packageNameAndSemVer);
        if (!result) {
            // Sanity check -- this should never happen
            throw new Error('Unable to parse package/semver expression in the Yarn shrinkwrap file (yarn.lock): ' +
                JSON.stringify(packageNameAndSemVer));
        }
        const packageName = result[1] || '';
        const parsedPackageName = PackageNameParsers.permissive.tryParse(packageName);
        if (parsedPackageName.error) {
            // Sanity check -- this should never happen
            throw new Error('Invalid package name the Yarn shrinkwrap file (yarn.lock): ' +
                JSON.stringify(packageNameAndSemVer) +
                '\n' +
                parsedPackageName.error);
        }
        return {
            packageName,
            semVerRange: result[2] || ''
        };
    }
    /**
     * This is the inverse of _decodePackageNameAndSemVer():
     * Given an IPackageNameAndSemVer object, recreate the yarn.lock lookup key
     * (sometimes called a "pattern").
     */
    static _encodePackageNameAndSemVer(packageNameAndSemVer) {
        return packageNameAndSemVer.packageName + '@' + packageNameAndSemVer.semVerRange;
    }
    /** @override */
    getTempProjectNames() {
        return this._tempProjectNames;
    }
    /** @override */
    hasCompatibleTopLevelDependency(dependencySpecifier) {
        // It seems like we should normalize the key somehow, but Yarn apparently does not
        // do any normalization.
        const key = YarnShrinkwrapFile._encodePackageNameAndSemVer({
            packageName: dependencySpecifier.packageName,
            semVerRange: dependencySpecifier.versionSpecifier
        });
        // Check whether this exact key appears in the shrinkwrap file
        return Object.hasOwnProperty.call(this._shrinkwrapJson, key);
    }
    /** @override */
    tryEnsureCompatibleDependency(dependencySpecifier, tempProjectName) {
        return this.hasCompatibleTopLevelDependency(dependencySpecifier);
    }
    /** @override */
    serialize() {
        return lockfileModule.stringify(this._shrinkwrapJson);
    }
    /** @override */
    getTopLevelDependencyVersion(dependencyName) {
        throw new InternalError('Not implemented');
    }
    /** @override */
    tryEnsureDependencyVersion(dependencySpecifier, tempProjectName) {
        throw new InternalError('Not implemented');
    }
    /** @override */
    getProjectShrinkwrap(project) {
        return undefined;
    }
    /** @override */
    async isWorkspaceProjectModifiedAsync(project, subspace) {
        throw new InternalError('Not implemented');
    }
}
// Example inputs:
// "js-tokens@^3.0.0 || ^4.0.0"
// "@rush-temp/api-extractor-test-03@file:./projects/api-extractor-test-03.tgz"
YarnShrinkwrapFile._packageNameAndSemVerRegExp = /^(@?[^@\s]+)(?:@(.*))?$/;
//# sourceMappingURL=YarnShrinkwrapFile.js.map