// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import * as path from 'node:path';
import * as semver from 'semver';
import { FileSystem, FileConstants } from '@rushstack/node-core-library';
import { RushConstants } from '../logic/RushConstants';
import { PackageNameParsers } from './PackageNameParsers';
import { DependencySpecifier, DependencySpecifierType } from '../logic/DependencySpecifier';
import { SaveCallbackPackageJsonEditor } from './SaveCallbackPackageJsonEditor';
/**
 * This represents the configuration of a project that is built by Rush, based on
 * the Rush.json configuration file.
 * @public
 */
export class RushConfigurationProject {
    /**
     * The parsed NPM "package.json" file from projectFolder.
     */
    get packageJson() {
        return this._packageJson;
    }
    /** @internal */
    constructor(options) {
        this._versionPolicy = undefined;
        this._dependencyProjects = undefined;
        this._consumingProjects = undefined;
        const { projectJson, rushConfiguration, tempProjectName, allowedProjectTags } = options;
        const { packageName, projectFolder: projectRelativeFolder } = projectJson;
        this.rushConfiguration = rushConfiguration;
        this.packageName = packageName;
        this.projectRelativeFolder = projectRelativeFolder;
        validateRelativePathField(projectRelativeFolder, 'projectFolder', rushConfiguration.rushJsonFile);
        // For example, the depth of "a/b/c" would be 3.  The depth of "a" is 1.
        const projectFolderDepth = projectRelativeFolder.split('/').length;
        if (projectFolderDepth < rushConfiguration.projectFolderMinDepth) {
            throw new Error(`To keep things organized, this repository has a projectFolderMinDepth policy` +
                ` requiring project folders to be at least ${rushConfiguration.projectFolderMinDepth} levels deep.` +
                `  Problem folder: "${projectRelativeFolder}"`);
        }
        if (projectFolderDepth > rushConfiguration.projectFolderMaxDepth) {
            throw new Error(`To keep things organized, this repository has a projectFolderMaxDepth policy` +
                ` preventing project folders from being deeper than ${rushConfiguration.projectFolderMaxDepth} levels.` +
                `  Problem folder:  "${projectRelativeFolder}"`);
        }
        const absoluteProjectFolder = path.join(rushConfiguration.rushJsonFolder, projectRelativeFolder);
        this.projectFolder = absoluteProjectFolder;
        const packageJsonFilename = path.join(absoluteProjectFolder, FileConstants.PackageJson);
        try {
            const packageJsonText = FileSystem.readFile(packageJsonFilename);
            // JSON.parse is native and runs in less than 1/2 the time of jju.parse. package.json is required to be strict JSON by NodeJS.
            this._packageJson = JSON.parse(packageJsonText);
        }
        catch (error) {
            if (FileSystem.isNotExistError(error)) {
                throw new Error(`Could not find package.json for ${packageName} at ${packageJsonFilename}`);
            }
            // Encountered an error while loading the package.json file. Please append the error message with the corresponding file location.
            if (error instanceof SyntaxError) {
                error.message = `${error.message}\nFilename: ${packageJsonFilename}`;
            }
            throw error;
        }
        this.projectRushConfigFolder = path.join(absoluteProjectFolder, 'config', 'rush');
        this.projectRushTempFolder = path.join(absoluteProjectFolder, RushConstants.projectRushFolderName, RushConstants.rushTempFolderName);
        // Are we using a package review file?
        if (rushConfiguration.approvedPackagesPolicy.enabled) {
            // If so, then every project needs to have a reviewCategory that was defined
            // by the reviewCategories array.
            if (!projectJson.reviewCategory) {
                throw new Error(`The "approvedPackagesPolicy" feature is enabled ${RushConstants.rushJsonFilename}, but a reviewCategory` +
                    ` was not specified for the project "${packageName}".`);
            }
            if (!rushConfiguration.approvedPackagesPolicy.reviewCategories.has(projectJson.reviewCategory)) {
                throw new Error(`The project "${packageName}" specifies its reviewCategory as` +
                    `"${projectJson.reviewCategory}" which is not one of the defined reviewCategories.`);
            }
            this.reviewCategory = projectJson.reviewCategory;
        }
        if (this.packageJson.name !== this.packageName) {
            throw new Error(`The package name "${this.packageName}" specified in ${RushConstants.rushJsonFilename} does not` +
                ` match the name "${this.packageJson.name}" from package.json`);
        }
        if (!semver.valid(this.packageJson.version)) {
            throw new Error(`The value "${this.packageJson.version}" is not valid SemVer syntax for the \"version\" field` +
                ` in the file "${packageJsonFilename}"`);
        }
        this.packageJsonEditor = SaveCallbackPackageJsonEditor.fromObjectWithCallback({
            object: this.packageJson,
            filename: packageJsonFilename,
            onSaved: (newObject) => {
                // Just update the in-memory copy, don't bother doing the validation again
                this._packageJson = newObject;
                this._dependencyProjects = undefined; // Reset the cached dependency projects
            }
        });
        this.tempProjectName = tempProjectName;
        // The "rushProject.tempProjectName" is guaranteed to be unique name (e.g. by adding the "-2"
        // suffix).  Even after we strip the NPM scope, it will still be unique.
        // Example: "my-project-2"
        this.unscopedTempProjectName = PackageNameParsers.permissive.getUnscopedName(tempProjectName);
        this.decoupledLocalDependencies = new Set();
        if (projectJson.cyclicDependencyProjects || projectJson.decoupledLocalDependencies) {
            if (projectJson.cyclicDependencyProjects && projectJson.decoupledLocalDependencies) {
                throw new Error('A project configuration cannot specify both "decoupledLocalDependencies" and "cyclicDependencyProjects". Please use "decoupledLocalDependencies" only -- the other name is deprecated.');
            }
            for (const cyclicDependencyProject of projectJson.cyclicDependencyProjects ||
                projectJson.decoupledLocalDependencies) {
                this.decoupledLocalDependencies.add(cyclicDependencyProject);
            }
        }
        this._shouldPublish = !!projectJson.shouldPublish;
        this.skipRushCheck = !!projectJson.skipRushCheck;
        this.versionPolicyName = projectJson.versionPolicyName;
        if (this._shouldPublish && this.packageJson.private) {
            throw new Error(`The project "${packageName}" specifies "shouldPublish": true, ` +
                `but the package.json file specifies "private": true.`);
        }
        this.publishFolder = absoluteProjectFolder;
        const { publishFolder } = projectJson;
        if (publishFolder) {
            validateRelativePathField(publishFolder, 'publishFolder', rushConfiguration.rushJsonFile);
            this.publishFolder = path.join(this.publishFolder, publishFolder);
        }
        if (allowedProjectTags && projectJson.tags) {
            const tags = new Set();
            for (const tag of projectJson.tags) {
                if (!allowedProjectTags.has(tag)) {
                    throw new Error(`The tag "${tag}" specified for project "${packageName}" is not listed in the ` +
                        `allowedProjectTags field in ${RushConstants.rushJsonFilename}.`);
                }
                else {
                    tags.add(tag);
                }
            }
            this.tags = tags;
        }
        else {
            this.tags = new Set(projectJson.tags);
        }
        this.configuredSubspaceName = projectJson.subspaceName;
        this.subspace = options.subspace;
    }
    /**
     * A list of local projects that appear as devDependencies for this project, but cannot be
     * locally linked because it would create a cyclic dependency; instead, the last published
     * version will be installed in the Common folder.
     *
     * These are package names that would be found by RushConfiguration.getProjectByName().
     *
     * @deprecated Use `decoupledLocalDependencies` instead, as it better describes the purpose of the data.
     */
    get cyclicDependencyProjects() {
        return this.decoupledLocalDependencies;
    }
    /**
     * An array of projects within the Rush configuration which directly depend on this package.
     * @deprecated Use `consumingProjectNames` instead, as it has Set semantics, which better reflect the nature
     * of the data.
     */
    get downstreamDependencyProjects() {
        return Array.from(this.consumingProjects, (project) => project.packageName);
    }
    /**
     * An array of projects within the Rush configuration which this project declares as dependencies.
     * @deprecated Use `dependencyProjects` instead, as it has Set semantics, which better reflect the nature
     * of the data.
     */
    get localDependencyProjects() {
        return [...this.dependencyProjects];
    }
    /**
     * The set of projects within the Rush configuration which this project declares as dependencies.
     *
     * @remarks
     * Can be used recursively to walk the project dependency graph to find all projects that are directly or indirectly
     * referenced from this project.
     */
    get dependencyProjects() {
        var _a, _b;
        let dependencyProjects = this._dependencyProjects;
        if (!dependencyProjects) {
            this._dependencyProjects = dependencyProjects = new Set();
            const { packageJson } = this;
            for (const dependencySet of [
                packageJson.dependencies,
                packageJson.devDependencies,
                packageJson.optionalDependencies
            ]) {
                if (dependencySet) {
                    for (const [dependency, version] of Object.entries(dependencySet)) {
                        const dependencySpecifier = DependencySpecifier.parseWithCache(dependency, version);
                        const dependencyName = (_b = (_a = dependencySpecifier.aliasTarget) === null || _a === void 0 ? void 0 : _a.packageName) !== null && _b !== void 0 ? _b : dependencySpecifier.packageName;
                        // Skip if we can't find the local project or it's a cyclic dependency
                        const localProject = this.rushConfiguration.getProjectByName(dependencyName);
                        if (localProject && !this.decoupledLocalDependencies.has(dependency)) {
                            // Set the value if it's a workspace project, or if we have a local project and the semver is satisfied
                            switch (dependencySpecifier.specifierType) {
                                case DependencySpecifierType.Version:
                                case DependencySpecifierType.Range:
                                    if (semver.satisfies(localProject.packageJson.version, dependencySpecifier.versionSpecifier)) {
                                        dependencyProjects.add(localProject);
                                    }
                                    break;
                                case DependencySpecifierType.Workspace:
                                    dependencyProjects.add(localProject);
                                    break;
                            }
                        }
                    }
                }
            }
        }
        return dependencyProjects;
    }
    /**
     * The set of projects within the Rush configuration which declare this project as a dependency.
     * Excludes those that declare this project as a `cyclicDependencyProject`.
     *
     * @remarks
     * This field is the counterpart to `dependencyProjects`, and can be used recursively to walk the project dependency
     * graph to find all projects which will be impacted by changes to this project.
     */
    get consumingProjects() {
        if (!this._consumingProjects) {
            // Force initialize all dependency relationships
            // This needs to operate on every project in the set because the relationships are only specified
            // in the consuming project
            const { projects } = this.rushConfiguration;
            for (const project of projects) {
                project._consumingProjects = new Set();
            }
            for (const project of projects) {
                for (const dependency of project.dependencyProjects) {
                    dependency._consumingProjects.add(project);
                }
            }
        }
        return this._consumingProjects;
    }
    /**
     * A flag which indicates whether changes to this project should be published. This controls
     * whether or not the project would show up when running `rush change`, and whether or not it
     * should be published during `rush publish`.
     */
    get shouldPublish() {
        return this._shouldPublish || !!this.versionPolicyName;
    }
    /**
     * Version policy of the project
     * @beta
     */
    get versionPolicy() {
        if (!this._versionPolicy) {
            if (this.versionPolicyName && this.rushConfiguration.versionPolicyConfiguration) {
                this._versionPolicy = this.rushConfiguration.versionPolicyConfiguration.getVersionPolicy(this.versionPolicyName);
            }
        }
        return this._versionPolicy;
    }
    /**
     * Indicate whether this project is the main project for the related version policy.
     *
     * False if the project is not for publishing.
     * True if the project is individually versioned or if its lockstep version policy does not specify main project.
     * False if the project is lockstepped and is not the main project for its version policy.
     *
     * @beta
     */
    get isMainProject() {
        if (!this.shouldPublish) {
            return false;
        }
        let isMain = true;
        if (this.versionPolicy && this.versionPolicy.isLockstepped) {
            const lockStepPolicy = this.versionPolicy;
            if (lockStepPolicy.mainProject && lockStepPolicy.mainProject !== this.packageName) {
                isMain = false;
            }
        }
        return isMain;
    }
}
export function validateRelativePathField(relativePath, field, file) {
    // path.isAbsolute delegates depending on platform; however, path.posix.isAbsolute('C:/a') returns false,
    // while path.win32.isAbsolute('C:/a') returns true. We want consistent validation across platforms.
    if (path.posix.isAbsolute(relativePath) || path.win32.isAbsolute(relativePath)) {
        throw new Error(`The value "${relativePath}" in the "${field}" field in "${file}" must be a relative path.`);
    }
    if (relativePath.includes('\\')) {
        throw new Error(`The value "${relativePath}" in the "${field}" field in "${file}" may not contain backslashes ('\\'), since they are interpreted differently` +
            ` on POSIX and Windows. Paths must use '/' as the path separator.`);
    }
    if (relativePath.endsWith('/')) {
        throw new Error(`The value "${relativePath}" in the "${field}" field in "${file}" may not end with a trailing '/' character.`);
    }
    const normalized = path.posix.normalize(relativePath);
    if (relativePath !== normalized) {
        throw new Error(`The value "${relativePath}" in the "${field}" field in "${file}" should be replaced with its normalized form "${normalized}".`);
    }
}
//# sourceMappingURL=RushConfigurationProject.js.map