// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import * as crypto from 'node:crypto';
import { InternalError, JsonFile } from '@rushstack/node-core-library';
import { BaseProjectShrinkwrapFile } from '../base/BaseProjectShrinkwrapFile';
import { RushConstants } from '../RushConstants';
/**
 *
 */
export class PnpmProjectShrinkwrapFile extends BaseProjectShrinkwrapFile {
    /**
     * Generate and write the project shrinkwrap file to <project>/.rush/temp/shrinkwrap-deps.json.
     * @returns True if the project shrinkwrap was created or updated, false otherwise.
     */
    async updateProjectShrinkwrapAsync() {
        const projectShrinkwrapMap = this.generateProjectShrinkwrapMap();
        return projectShrinkwrapMap ? this.saveAsync(projectShrinkwrapMap) : this.deleteIfExistsAsync();
    }
    hasChanges(otherShrinkwrap) {
        if (!otherShrinkwrap.shrinkwrapFile.isWorkspaceCompatible &&
            !otherShrinkwrap.shrinkwrapFile.getTempProjectDependencyKey(this.project.tempProjectName)) {
            // The project is new to the shrinkwrap file.
            return true;
        }
        const otherMap = otherShrinkwrap.generateProjectShrinkwrapMap();
        const thisMap = this.generateProjectShrinkwrapMap();
        if (!thisMap || !otherMap) {
            // Handle one or both being undefined.
            return !!(thisMap || otherMap);
        }
        if (thisMap.size !== otherMap.size) {
            // Entries added or removed
            return true;
        }
        for (const [key, value] of thisMap) {
            if (otherMap.get(key) !== value) {
                // A dependency changed or was added/removed
                return true;
            }
        }
        return false;
    }
    /**
     * Generate the project shrinkwrap file content
     */
    generateProjectShrinkwrapMap() {
        const projectShrinkwrapMap = this.shrinkwrapFile.isWorkspaceCompatible
            ? this.generateWorkspaceProjectShrinkwrapMap()
            : this.generateLegacyProjectShrinkwrapMap();
        return projectShrinkwrapMap;
    }
    generateWorkspaceProjectShrinkwrapMap() {
        // Obtain the workspace importer from the shrinkwrap, which lists resolved dependencies
        const subspace = this.project.subspace;
        const importerKey = this.shrinkwrapFile.getImporterKeyByPath(subspace.getSubspaceTempFolderPath(), this.project.projectFolder);
        const projectShrinkwrapMap = this.shrinkwrapFile.getIntegrityForImporter(importerKey);
        return projectShrinkwrapMap;
    }
    generateLegacyProjectShrinkwrapMap() {
        const tempProjectDependencyKey = this.shrinkwrapFile.getTempProjectDependencyKey(this.project.tempProjectName);
        if (!tempProjectDependencyKey) {
            throw new Error(`Cannot get dependency key for temp project: ${this.project.tempProjectName}`);
        }
        const parentShrinkwrapEntry = this.shrinkwrapFile.getShrinkwrapEntryFromTempProjectDependencyKey(tempProjectDependencyKey);
        const allDependencies = [
            ...Object.entries(parentShrinkwrapEntry.dependencies || {}),
            ...Object.entries(parentShrinkwrapEntry.optionalDependencies || {})
        ];
        const projectShrinkwrapMap = new Map();
        for (const [name, version] of allDependencies) {
            if (name.indexOf(`${RushConstants.rushTempNpmScope}/`) < 0) {
                // Only select the shrinkwrap dependencies that are non-local since we already handle local
                // project changes
                this._addDependencyRecursive(projectShrinkwrapMap, name, version, parentShrinkwrapEntry);
            }
        }
        // Since peer dependencies within on external packages may be hoisted up to the top-level package,
        // we need to resolve and add these dependencies directly
        this._resolveAndAddPeerDependencies(projectShrinkwrapMap, parentShrinkwrapEntry);
        return projectShrinkwrapMap;
    }
    _addDependencyRecursive(projectShrinkwrapMap, name, version, parentShrinkwrapEntry, throwIfShrinkwrapEntryMissing = true) {
        const specifier = `${name}@${version}`;
        if (projectShrinkwrapMap.has(specifier)) {
            // getShrinkwrapEntry is idempotent with respect to name and version
            return;
        }
        const shrinkwrapEntry = this.shrinkwrapFile.getShrinkwrapEntry(name, version);
        if (!shrinkwrapEntry) {
            if (throwIfShrinkwrapEntryMissing) {
                throw new InternalError(`Unable to find dependency ${name} with version ${version} in shrinkwrap.`);
            }
            return;
        }
        // Hash the full shrinkwrap entry instead of using just resolution.integrity.
        // This ensures that changes to sub-dependency resolutions are detected.
        // For example, if package A depends on B@1.0 and B@1.0's resolution of C changes
        // from C@1.3 to C@1.2, the hash of A's shrinkwrap entry will change because
        // the dependencies field in the entry reflects the resolved versions.
        const sha256Digest = crypto
            .createHash('sha256')
            .update(JSON.stringify(shrinkwrapEntry))
            .digest('hex');
        const integrity = `${name}@${version}:${sha256Digest}:`;
        // Add the current dependency
        projectShrinkwrapMap.set(specifier, integrity);
        // Add the dependencies of the dependency
        for (const [dependencyName, dependencyVersion] of Object.entries(shrinkwrapEntry.dependencies || {})) {
            this._addDependencyRecursive(projectShrinkwrapMap, dependencyName, dependencyVersion, shrinkwrapEntry);
        }
        // Add the optional dependencies of the dependency, and don't blow up if they don't exist
        for (const [dependencyName, dependencyVersion] of Object.entries(shrinkwrapEntry.optionalDependencies || {})) {
            this._addDependencyRecursive(projectShrinkwrapMap, dependencyName, dependencyVersion, shrinkwrapEntry, 
            /* throwIfShrinkwrapEntryMissing */ false);
        }
        // When using workspaces, hoisting of peer dependencies to a singular top-level project is not possible.
        // Therefore, all packages that are consumed should be specified in the dependency tree. Given this, there
        // is no need to look for peer dependencies, since it is simply a constraint to be validated by the
        // package manager.
        if (!this.shrinkwrapFile.isWorkspaceCompatible) {
            this._resolveAndAddPeerDependencies(projectShrinkwrapMap, shrinkwrapEntry, parentShrinkwrapEntry);
        }
    }
    _resolveAndAddPeerDependencies(projectShrinkwrapMap, shrinkwrapEntry, parentShrinkwrapEntry) {
        var _a, _b, _c;
        for (const peerDependencyName of Object.keys(shrinkwrapEntry.peerDependencies || {})) {
            // Skip peer dependency resolution of local package peer dependencies
            if (peerDependencyName.indexOf(RushConstants.rushTempNpmScope) !== -1) {
                continue;
            }
            // Check to see if the peer dependency is satisfied with the current shrinkwrap
            // entry. If not, check the parent shrinkwrap entry. Finally, if neither have
            // the specified dependency, check that the parent mentions the dependency in
            // it's own peer dependencies. If it is, we can rely on the package manager and
            // make the assumption that we've already found it further up the stack.
            if (((_a = shrinkwrapEntry.dependencies) === null || _a === void 0 ? void 0 : _a.hasOwnProperty(peerDependencyName)) ||
                ((_b = parentShrinkwrapEntry === null || parentShrinkwrapEntry === void 0 ? void 0 : parentShrinkwrapEntry.dependencies) === null || _b === void 0 ? void 0 : _b.hasOwnProperty(peerDependencyName)) ||
                ((_c = parentShrinkwrapEntry === null || parentShrinkwrapEntry === void 0 ? void 0 : parentShrinkwrapEntry.peerDependencies) === null || _c === void 0 ? void 0 : _c.hasOwnProperty(peerDependencyName))) {
                continue;
            }
            // As a last attempt, check if it's been hoisted up as a top-level dependency. If
            // we can't find it, we can assume that it's already been provided somewhere up the
            // dependency tree.
            const topLevelDependencySpecifier = this.shrinkwrapFile.getTopLevelDependencyVersion(peerDependencyName);
            if (topLevelDependencySpecifier) {
                this._addDependencyRecursive(projectShrinkwrapMap, peerDependencyName, this.shrinkwrapFile.getTopLevelDependencyKey(peerDependencyName), shrinkwrapEntry);
            }
        }
    }
    /**
     * Save the current state of the object to project/.rush/temp/shrinkwrap-deps.json
     */
    async saveAsync(projectShrinkwrapMap) {
        const file = {};
        const keys = Array.from(projectShrinkwrapMap.keys()).sort();
        for (const key of keys) {
            file[key] = projectShrinkwrapMap.get(key);
        }
        await JsonFile.saveAsync(file, this.projectShrinkwrapFilePath, { ensureFolderExists: true });
    }
}
//# sourceMappingURL=PnpmProjectShrinkwrapFile.js.map