diff --git a/common/changes/@microsoft/rush/will-seperate-rush-projects_2024-08-12-23-13.json b/common/changes/@microsoft/rush/will-seperate-rush-projects_2024-08-12-23-13.json new file mode 100644 index 00000000000..17d7921228a --- /dev/null +++ b/common/changes/@microsoft/rush/will-seperate-rush-projects_2024-08-12-23-13.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@microsoft/rush", + "comment": "Enable Rush projects to be defined in a dedicated file \"rush-projects.json\" instead of in \"rush.json\"", + "type": "none" + } + ], + "packageName": "@microsoft/rush" +} \ No newline at end of file diff --git a/common/reviews/api/rush-lib.api.md b/common/reviews/api/rush-lib.api.md index 95f430832c7..5ff29db6e96 100644 --- a/common/reviews/api/rush-lib.api.md +++ b/common/reviews/api/rush-lib.api.md @@ -1229,6 +1229,10 @@ export class RushConfiguration { // // @internal (undocumented) readonly _rushPluginsConfiguration: RushPluginsConfiguration; + // Warning: (ae-forgotten-export) The symbol "IRushProjectsJson" needs to be exported by the entry point index.d.ts + // + // @internal + readonly _rushProjectsJson: IRushProjectsJson | undefined; readonly shrinkwrapFilename: string; get shrinkwrapFilePhrase(): string; // @beta @@ -1351,6 +1355,7 @@ export class RushConstants { static readonly rushPluginManifestFilename: 'rush-plugin-manifest.json'; static readonly rushPluginsConfigFilename: 'rush-plugins.json'; static readonly rushProjectConfigFilename: 'rush-project.json'; + static readonly rushProjectsFilename: 'rush-projects.json'; static readonly rushRecyclerFolderName: 'rush-recycler'; static readonly rushTempFolderName: 'temp'; static readonly rushTempNpmScope: '@rush-temp'; diff --git a/libraries/rush-lib/src/api/RushConfiguration.ts b/libraries/rush-lib/src/api/RushConfiguration.ts index f3fa0cbd6e4..345c6e7d6e4 100644 --- a/libraries/rush-lib/src/api/RushConfiguration.ts +++ b/libraries/rush-lib/src/api/RushConfiguration.ts @@ -38,6 +38,7 @@ import { type IPnpmOptionsJson, PnpmOptionsConfiguration } from '../logic/pnpm/P import { type INpmOptionsJson, NpmOptionsConfiguration } from '../logic/npm/NpmOptionsConfiguration'; import { type IYarnOptionsJson, YarnOptionsConfiguration } from '../logic/yarn/YarnOptionsConfiguration'; import schemaJson from '../schemas/rush.schema.json'; +import projectSchemaJson from '../schemas/projects.schema.json'; import type * as DependencyAnalyzerModuleType from '../logic/DependencyAnalyzer'; import type { PackageManagerOptionsConfigurationBase } from '../logic/base/BasePackageManagerOptionsConfiguration'; @@ -175,6 +176,14 @@ export interface IRushConfigurationJson { variants?: unknown; } +/** + * This represents the JSON data structure for the "rush-projects.json" configuration file. + * See projects.schema.json for documentation + */ +export interface IRushProjectsJson { + projects: IRushConfigurationProjectJson[]; +} + /** * The filter parameters to search from all projects */ @@ -208,6 +217,7 @@ export interface ITryFindRushJsonLocationOptions { */ export class RushConfiguration { private static _jsonSchema: JsonSchema = JsonSchema.fromLoadedObject(schemaJson); + private static _projectsJsonSchema: JsonSchema = JsonSchema.fromLoadedObject(projectSchemaJson); private readonly _pathTrees: Map>; @@ -247,6 +257,13 @@ export class RushConfiguration { */ public readonly rushConfigurationJson: IRushConfigurationJson; + /** + * Gets the JSON data structure for the "rush-projects.json" configuration file. + * + * @internal + */ + public readonly _rushProjectsJson: IRushProjectsJson | undefined; + /** * The absolute path to the "rush.json" configuration file that was loaded to construct this object. */ @@ -867,9 +884,36 @@ export class RushConfiguration { throw new InternalError('The default subspace was not created'); } + // Look for rush projects in a seperate file first, and if it does not exist fallback onto rush.json + + const resolvedRushProjectsFilename: string = `${this.rushJsonFolder}/${RushConstants.rushProjectsFilename}`; + let rushProjects: IRushConfigurationProjectJson[] | undefined; + try { + const rushProjectsJson: IRushProjectsJson = JsonFile.load(resolvedRushProjectsFilename); + RushConfiguration._projectsJsonSchema.validateObject(rushProjectsJson, resolvedRushProjectsFilename); + rushProjects = rushProjectsJson.projects; + } catch (e) { + if (!FileSystem.isNotExistError(e)) { + throw e; + } + } + if (this.rushConfigurationJson.projects) { + if (rushProjects) { + throw new Error( + `When using the ${RushConstants.rushProjectsFilename} configuration file, the "projects" field of the ${RushConstants.rushJsonFilename} is no longer allowed.` + ); + } + rushProjects = this.rushConfigurationJson.projects; + } + + if (!rushProjects) { + throw new Error( + `The rush.json is missing a projects field, and a seperate ${RushConstants.rushProjectsFilename} file is missing. Rush projects need to be defined in one of the two places.` + ); + } // Sort the projects array in alphabetical order. This ensures that the packages // are processed in a deterministic order by the various Rush algorithms. - const sortedProjectJsons: IRushConfigurationProjectJson[] = this.rushConfigurationJson.projects.slice(0); + const sortedProjectJsons: IRushConfigurationProjectJson[] = rushProjects.slice(0); sortedProjectJsons.sort((a: IRushConfigurationProjectJson, b: IRushConfigurationProjectJson) => a.packageName.localeCompare(b.packageName) ); diff --git a/libraries/rush-lib/src/logic/RushConstants.ts b/libraries/rush-lib/src/logic/RushConstants.ts index ad0099d7cc1..09a21d523d8 100644 --- a/libraries/rush-lib/src/logic/RushConstants.ts +++ b/libraries/rush-lib/src/logic/RushConstants.ts @@ -17,6 +17,11 @@ export class RushConstants { */ public static readonly rushJsonFilename: 'rush.json' = 'rush.json'; + /** + * The filename ("rush-projects.json") for the root-level projects configuration file. + */ + public static readonly rushProjectsFilename: 'rush-projects.json' = 'rush-projects.json'; + /** * The filename ("browser-approved-packages.json") for an optional policy configuration file * that stores a list of NPM packages that have been approved for usage by Rush projects. diff --git a/libraries/rush-lib/src/schemas/projects.schema.json b/libraries/rush-lib/src/schemas/projects.schema.json new file mode 100644 index 00000000000..b2008bddd03 --- /dev/null +++ b/libraries/rush-lib/src/schemas/projects.schema.json @@ -0,0 +1,18 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Rush main project list config File", + "description": "The configuration file for the projects used in a Rush monorepo.", + "type": "object", + + "properties": { + "$schema": { + "description": "Part of the JSON Schema standard, this optional keyword declares the URL of the schema that the file conforms to. Editors may download the schema and use it to perform syntax highlighting.", + "type": "string" + }, + "projects": { + "description": "A list of projects managed by this tool." + } + }, + "additionalProperties": false, + "required": ["projects"] +} diff --git a/libraries/rush-lib/src/schemas/rush-project-config.schema.json b/libraries/rush-lib/src/schemas/rush-project-config.schema.json new file mode 100644 index 00000000000..ee813fdb6e8 --- /dev/null +++ b/libraries/rush-lib/src/schemas/rush-project-config.schema.json @@ -0,0 +1,67 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Project list managed by rush.", + "description": "A list of projects managed by rush.", + "type": "array", + "items": { + "type": "object", + "properties": { + "packageName": { + "description": "The NPM package name of the project.", + "type": "string" + }, + "projectFolder": { + "description": "The path to the project folder relative to the Rush config file.", + "type": "string" + }, + "reviewCategory": { + "description": "An optional category for usage in the \"browser-approved-packages.json\" and \"nonbrowser-approved-packages.json\" files. Only strings from reviewCategories are allowed here.", + "type": "string" + }, + "cyclicDependencyProjects": { + "description": "(Deprecated) This field was renamed to \"decoupledLocalDependencies\".", + "type": "array", + "items": { + "type": "string" + } + }, + "decoupledLocalDependencies": { + "description": "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.", + "type": "array", + "items": { + "type": "string" + } + }, + "shouldPublish": { + "description": "A flag indicating that changes to this project will be published to npm, which affects the Rush change and publish workflows.", + "type": "boolean" + }, + "skipRushCheck": { + "description": "If true, then this project will be ignored by the \"rush check\" command. The default value is false.", + "type": "boolean" + }, + "versionPolicyName": { + "description": "An optional version policy associated with the project. Version policies are defined in \"version-policies.json\" file.", + "type": "string" + }, + "publishFolder": { + "description": "Facilitates postprocessing of a project's files prior to publishing. If specified, the \"publishFolder\" is the relative path to a subfolder of the project folder. The \"rush publish\" command will publish the subfolder instead of the project folder. The subfolder must contain its own package.json file, which is typically a build output.", + "type": "string" + }, + "tags": { + "description": "An optional set of custom tags that can be used to select this project. For example, adding \"my-custom-tag\" will allow this project to be selected by the command \"rush list --only tag:my-custom-tag\". The tag name must be one or more words separated by hyphens or slashes, where a word may contain lowercase ASCII letters, digits, \".\", and \"@\" characters.", + "type": "array", + "items": { + "type": "string", + "pattern": "^[a-z0-9.@]+([-/][a-z0-9.@]+)*$" + } + }, + "subspaceName": { + "description": "(EXPERIMENTAL) An optional entry for specifying which subspace this project belongs to if the subspaces feature is enabled.", + "type": "string" + } + }, + "additionalProperties": false, + "required": ["packageName", "projectFolder"] + } +} diff --git a/libraries/rush-lib/src/schemas/rush.schema.json b/libraries/rush-lib/src/schemas/rush.schema.json index 46965cbbd41..498b4ca7b88 100644 --- a/libraries/rush-lib/src/schemas/rush.schema.json +++ b/libraries/rush-lib/src/schemas/rush.schema.json @@ -239,69 +239,7 @@ } }, "projects": { - "description": "A list of projects managed by this tool.", - "type": "array", - "items": { - "type": "object", - "properties": { - "packageName": { - "description": "The NPM package name of the project.", - "type": "string" - }, - "projectFolder": { - "description": "The path to the project folder relative to the Rush config file.", - "type": "string" - }, - "reviewCategory": { - "description": "An optional category for usage in the \"browser-approved-packages.json\" and \"nonbrowser-approved-packages.json\" files. Only strings from reviewCategories are allowed here.", - "type": "string" - }, - "cyclicDependencyProjects": { - "description": "(Deprecated) This field was renamed to \"decoupledLocalDependencies\".", - "type": "array", - "items": { - "type": "string" - } - }, - "decoupledLocalDependencies": { - "description": "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.", - "type": "array", - "items": { - "type": "string" - } - }, - "shouldPublish": { - "description": "A flag indicating that changes to this project will be published to npm, which affects the Rush change and publish workflows.", - "type": "boolean" - }, - "skipRushCheck": { - "description": "If true, then this project will be ignored by the \"rush check\" command. The default value is false.", - "type": "boolean" - }, - "versionPolicyName": { - "description": "An optional version policy associated with the project. Version policies are defined in \"version-policies.json\" file.", - "type": "string" - }, - "publishFolder": { - "description": "Facilitates postprocessing of a project's files prior to publishing. If specified, the \"publishFolder\" is the relative path to a subfolder of the project folder. The \"rush publish\" command will publish the subfolder instead of the project folder. The subfolder must contain its own package.json file, which is typically a build output.", - "type": "string" - }, - "tags": { - "description": "An optional set of custom tags that can be used to select this project. For example, adding \"my-custom-tag\" will allow this project to be selected by the command \"rush list --only tag:my-custom-tag\". The tag name must be one or more words separated by hyphens or slashes, where a word may contain lowercase ASCII letters, digits, \".\", and \"@\" characters.", - "type": "array", - "items": { - "type": "string", - "pattern": "^[a-z0-9.@]+([-/][a-z0-9.@]+)*$" - } - }, - "subspaceName": { - "description": "(EXPERIMENTAL) An optional entry for specifying which subspace this project belongs to if the subspaces feature is enabled.", - "type": "string" - } - }, - "additionalProperties": false, - "required": ["packageName", "projectFolder"] - } + "description": "A list of projects managed by this tool." }, "eventHooks": { "description": "Hooks are customized script actions that Rush executes when specific events occur.", @@ -354,5 +292,5 @@ } }, "additionalProperties": false, - "required": ["rushVersion", "projects"] + "required": ["rushVersion"] }