diff --git a/common/changes/@microsoft/rush/rush-change-catalog-entries_2026-02-17-18-48.json b/common/changes/@microsoft/rush/rush-change-catalog-entries_2026-02-17-18-48.json new file mode 100644 index 00000000000..19c8142e89d --- /dev/null +++ b/common/changes/@microsoft/rush/rush-change-catalog-entries_2026-02-17-18-48.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@microsoft/rush", + "comment": "Add support for pnpm global catalog detection to `rush change`. Now, when a dependencyis changed in the pnpm global catalog, changelogs will be required for affected published packages.", + "type": "none" + } + ], + "packageName": "@microsoft/rush" +} \ No newline at end of file diff --git a/libraries/rush-lib/src/logic/ProjectChangeAnalyzer.ts b/libraries/rush-lib/src/logic/ProjectChangeAnalyzer.ts index 77ded049ca3..a90db706a9c 100644 --- a/libraries/rush-lib/src/logic/ProjectChangeAnalyzer.ts +++ b/libraries/rush-lib/src/logic/ProjectChangeAnalyzer.ts @@ -23,6 +23,8 @@ import type { RushConfigurationProject } from '../api/RushConfigurationProject'; import { BaseProjectShrinkwrapFile } from './base/BaseProjectShrinkwrapFile'; import { PnpmShrinkwrapFile } from './pnpm/PnpmShrinkwrapFile'; import { Git } from './Git'; +import { DependencySpecifier, DependencySpecifierType } from './DependencySpecifier'; +import type { IPnpmOptionsJson, PnpmOptionsConfiguration } from './pnpm/PnpmOptionsConfiguration'; import { type IInputsSnapshotProjectMetadata, type IInputsSnapshot, @@ -178,26 +180,42 @@ export class ProjectChangeAnalyzer { { concurrency: 10 } ); - // External dependency changes are not allowed to be filtered, so add these after filtering - if (includeExternalDependencies) { - // Even though changing the installed version of a nested dependency merits a change file, - // ignore lockfile changes for `rush change` for the moment + // Detect per-subspace changes: catalog entries in pnpm-config.json and external dependency lockfiles + const subspaces: Iterable = rushConfiguration.subspacesFeatureEnabled + ? rushConfiguration.subspaces + : [rushConfiguration.defaultSubspace]; - const subspaces: Iterable = rushConfiguration.subspacesFeatureEnabled - ? rushConfiguration.subspaces - : [rushConfiguration.defaultSubspace]; + const variantToUse: string | undefined = includeExternalDependencies + ? (variant ?? (await this._rushConfiguration.getCurrentlyInstalledVariantAsync())) + : undefined; - const variantToUse: string | undefined = - variant ?? (await this._rushConfiguration.getCurrentlyInstalledVariantAsync()); + await Async.forEachAsync(subspaces, async (subspace: Subspace) => { + const subspaceProjects: RushConfigurationProject[] = subspace.getProjects(); + + // Detect changes to pnpm catalog entries in pnpm-config.json + if (rushConfiguration.isPnpm) { + await this._detectCatalogChangesAsync( + subspace, + rushConfiguration, + changedFiles, + mergeCommit, + repoRoot, + terminal, + changedProjects + ); + } + + // External dependency changes are not allowed to be filtered, so add these after filtering + if (includeExternalDependencies) { + // Even though changing the installed version of a nested dependency merits a change file, + // ignore lockfile changes for `rush change` for the moment - await Async.forEachAsync(subspaces, async (subspace: Subspace) => { const fullShrinkwrapPath: string = subspace.getCommittedShrinkwrapFilePath(variantToUse); const relativeShrinkwrapFilePath: string = Path.convertToSlashes( path.relative(repoRoot, fullShrinkwrapPath) ); const shrinkwrapStatus: IFileDiffStatus | undefined = changedFiles.get(relativeShrinkwrapFilePath); - const subspaceProjects: RushConfigurationProject[] = subspace.getProjects(); if (shrinkwrapStatus) { if (shrinkwrapStatus.status !== 'M') { @@ -215,7 +233,7 @@ export class ProjectChangeAnalyzer { } if (rushConfiguration.isPnpm) { - const subspaceHasNoProjects: boolean = subspace.getProjects().length === 0; + const subspaceHasNoProjects: boolean = subspaceProjects.length === 0; const currentShrinkwrap: PnpmShrinkwrapFile | undefined = PnpmShrinkwrapFile.loadFromFile( fullShrinkwrapPath, { subspaceHasNoProjects } @@ -253,12 +271,12 @@ export class ProjectChangeAnalyzer { `Lockfile has changed and lockfile content comparison is only supported for pnpm. Assuming all projects are affected.` ); } - subspace.getProjects().forEach((project) => changedProjects.add(project)); + subspaceProjects.forEach((project) => changedProjects.add(project)); return; } } - }); - } + } + }); // Sort the set by projectRelativeFolder to avoid race conditions in the results const sortedChangedProjects: RushConfigurationProject[] = Array.from(changedProjects); @@ -491,6 +509,133 @@ export class ProjectChangeAnalyzer { return ignoreMatcher; } } + + /** + * Detects changes to pnpm catalog entries in a subspace's pnpm-config.json and marks + * affected projects as changed. + */ + private async _detectCatalogChangesAsync( + subspace: Subspace, + rushConfiguration: RushConfiguration, + changedFiles: Map, + mergeCommit: string, + repoRoot: string, + terminal: ITerminal, + changedProjects: Set + ): Promise { + const pnpmOptions: PnpmOptionsConfiguration | undefined = subspace.getPnpmOptions(); + // Default to an empty object if no global catalogs are configured, handle case of globalCatalogs being deleted + const currentCatalogs: Record> = pnpmOptions?.globalCatalogs ?? {}; + + const pnpmConfigRelativePath: string = Path.convertToSlashes( + path.relative(repoRoot, subspace.getPnpmConfigFilePath()) + ); + + if (!changedFiles.has(pnpmConfigRelativePath)) { + return; + } + + // Determine which specific packages changed within each catalog namespace + // Maps catalogNamespace (e.g. "default", "react17") → Set of changed package names + let oldCatalogs: Record> | undefined; + try { + const oldPnpmConfigText: string = await this._git.getBlobContentAsync({ + blobSpec: `${mergeCommit}:${pnpmConfigRelativePath}`, + repositoryRoot: repoRoot + }); + const oldPnpmConfig: IPnpmOptionsJson = JSON.parse(oldPnpmConfigText); + oldCatalogs = oldPnpmConfig.globalCatalogs ?? {}; + } catch { + // Old file didn't exist or was unparseable — treat all packages in all current catalogs as changed + if (rushConfiguration.subspacesFeatureEnabled) { + terminal.writeLine( + `"${subspace.subspaceName}" subspace pnpm-config.json was created or unparseable. Assuming all projects are affected.` + ); + } else { + terminal.writeLine( + `pnpm-config.json was created or unparseable. Assuming all projects are affected.` + ); + } + } + + const changedCatalogPackages: Map> = new Map>(); + + if (oldCatalogs === undefined) { + // Could not load old catalogs — treat all packages in all current catalogs as changed + for (const [catalogName, packages] of Object.entries(currentCatalogs)) { + changedCatalogPackages.set(catalogName, new Set(Object.keys(packages))); + } + } else { + // Check current catalogs for new or modified package entries + for (const [catalogName, packages] of Object.entries(currentCatalogs)) { + const oldPackages: Record | undefined = oldCatalogs[catalogName]; + if (!oldPackages) { + // Entire catalog is new — all packages in it are changed + changedCatalogPackages.set(catalogName, new Set(Object.keys(packages))); + continue; + } + const changedPkgs: Set = new Set(); + for (const [pkgName, version] of Object.entries(packages)) { + if (oldPackages[pkgName] !== version) { + changedPkgs.add(pkgName); + } + } + // Check for packages that were removed from this catalog + for (const pkgName of Object.keys(oldPackages)) { + if (!(pkgName in packages)) { + changedPkgs.add(pkgName); + } + } + if (changedPkgs.size > 0) { + changedCatalogPackages.set(catalogName, changedPkgs); + } + } + + // Check for catalogs that were entirely removed + for (const [catalogName, oldPackages] of Object.entries(oldCatalogs)) { + if (!(catalogName in currentCatalogs)) { + changedCatalogPackages.set(catalogName, new Set(Object.keys(oldPackages))); + } + } + } + + if (changedCatalogPackages.size > 0) { + // Check each project in the subspace to see if it depends on a changed catalog package + const subspaceProjects: RushConfigurationProject[] = subspace.getProjects(); + for (const project of subspaceProjects) { + const { dependencies, devDependencies, optionalDependencies, peerDependencies } = project.packageJson; + const allDeps: Record[] = [ + dependencies ?? {}, + devDependencies ?? {}, + optionalDependencies ?? {}, + peerDependencies ?? {} + ]; + + let isAffected: boolean = false; + for (const deps of allDeps) { + if (isAffected) { + break; + } + for (const [depName, depVersion] of Object.entries(deps)) { + const specifier: DependencySpecifier = DependencySpecifier.parseWithCache(depName, depVersion); + if (specifier.specifierType === DependencySpecifierType.Catalog) { + // versionSpecifier holds the catalog name (empty string for "catalog:") + const catalogName: string = specifier.versionSpecifier || 'default'; + const changedPkgs: Set | undefined = changedCatalogPackages.get(catalogName); + if (changedPkgs?.has(depName)) { + isAffected = true; + break; + } + } + } + } + + if (isAffected) { + changedProjects.add(project); + } + } + } + } } /** diff --git a/libraries/rush-lib/src/logic/test/ProjectChangeAnalyzer.test.ts b/libraries/rush-lib/src/logic/test/ProjectChangeAnalyzer.test.ts index 7be396977c9..b03269fc014 100644 --- a/libraries/rush-lib/src/logic/test/ProjectChangeAnalyzer.test.ts +++ b/libraries/rush-lib/src/logic/test/ProjectChangeAnalyzer.test.ts @@ -583,6 +583,531 @@ describe(ProjectChangeAnalyzer.name, () => { }); expect(changedProjects.has(rushConfiguration.getProjectByName('e')!)).toBe(true); }); + + describe('catalog change detection', () => { + function getCatalogRushConfiguration(): RushConfiguration { + return RushConfiguration.loadFromConfigurationFile( + resolve(__dirname, 'repoWithCatalogs', 'rush.json') + ); + } + + function mockPnpmConfigChanged(): void { + mockGetRepoChanges.mockReturnValue( + new Map([ + [ + 'common/config/rush/pnpm-config.json', + { mode: 'modified', newhash: 'newhash', oldhash: 'oldhash', status: 'M' } + ] + ]) + ); + } + + function mockOldCatalogs(catalogs: Record>): void { + mockGetBlobContentAsync.mockImplementation(() => { + return Promise.resolve(JSON.stringify({ globalCatalogs: catalogs })); + }); + } + + it('detects change to default catalog', async () => { + const rushConfiguration: RushConfiguration = getCatalogRushConfiguration(); + mockPnpmConfigChanged(); + // react version bumped in the default catalog + mockOldCatalogs({ + default: { react: '^17.0.0', lodash: '^4.17.21' }, + tools: { typescript: '~5.3.0', eslint: '^8.50.0' } + }); + + const projectChangeAnalyzer: ProjectChangeAnalyzer = new ProjectChangeAnalyzer(rushConfiguration); + const terminal: Terminal = new Terminal(new StringBufferTerminalProvider(true)); + + const changedProjects = await projectChangeAnalyzer.getChangedProjectsAsync({ + enableFiltering: false, + includeExternalDependencies: false, + targetBranchName: 'main', + terminal + }); + + // 'a' uses "catalog:" (default) for react (changed) and lodash + expect(changedProjects.has(rushConfiguration.getProjectByName('a')!)).toBe(true); + // 'd' uses "catalog:" (default) for lodash only (unchanged) — should NOT be detected + expect(changedProjects.has(rushConfiguration.getProjectByName('d')!)).toBe(false); + // 'b' uses "catalog:tools" only — tools catalog is unchanged + expect(changedProjects.has(rushConfiguration.getProjectByName('b')!)).toBe(false); + // 'e' uses "catalog:tools" for eslint only — tools catalog is unchanged + expect(changedProjects.has(rushConfiguration.getProjectByName('e')!)).toBe(false); + // 'c' has no catalog deps + expect(changedProjects.has(rushConfiguration.getProjectByName('c')!)).toBe(false); + }); + + it('detects change to named catalog', async () => { + const rushConfiguration: RushConfiguration = getCatalogRushConfiguration(); + mockPnpmConfigChanged(); + // typescript version bumped in the tools catalog + mockOldCatalogs({ + default: { react: '^18.0.0', lodash: '^4.17.21' }, + tools: { typescript: '~5.2.0', eslint: '^8.50.0' } + }); + + const projectChangeAnalyzer: ProjectChangeAnalyzer = new ProjectChangeAnalyzer(rushConfiguration); + const terminal: Terminal = new Terminal(new StringBufferTerminalProvider(true)); + + const changedProjects = await projectChangeAnalyzer.getChangedProjectsAsync({ + enableFiltering: false, + includeExternalDependencies: false, + targetBranchName: 'main', + terminal + }); + + // 'b' uses "catalog:tools" for typescript (changed) + expect(changedProjects.has(rushConfiguration.getProjectByName('b')!)).toBe(true); + // 'e' uses "catalog:tools" for eslint only (unchanged) — should NOT be detected + expect(changedProjects.has(rushConfiguration.getProjectByName('e')!)).toBe(false); + // 'a' uses "catalog:" (default) — default catalog is unchanged + expect(changedProjects.has(rushConfiguration.getProjectByName('a')!)).toBe(false); + // 'd' uses "catalog:" (default) for lodash — default catalog is unchanged + expect(changedProjects.has(rushConfiguration.getProjectByName('d')!)).toBe(false); + // 'c' has no catalog deps + expect(changedProjects.has(rushConfiguration.getProjectByName('c')!)).toBe(false); + }); + + it('no changes when catalogs are unchanged', async () => { + const rushConfiguration: RushConfiguration = getCatalogRushConfiguration(); + mockPnpmConfigChanged(); + // Old catalogs are identical to current + mockOldCatalogs({ + default: { react: '^18.0.0', lodash: '^4.17.21' }, + tools: { typescript: '~5.3.0', eslint: '^8.50.0' } + }); + + const projectChangeAnalyzer: ProjectChangeAnalyzer = new ProjectChangeAnalyzer(rushConfiguration); + const terminal: Terminal = new Terminal(new StringBufferTerminalProvider(true)); + + const changedProjects = await projectChangeAnalyzer.getChangedProjectsAsync({ + enableFiltering: false, + includeExternalDependencies: false, + targetBranchName: 'main', + terminal + }); + + expect(changedProjects.size).toBe(0); + }); + + it('only marks projects whose specific catalog package changed, not all projects in the same catalog namespace', async () => { + const rushConfiguration: RushConfiguration = getCatalogRushConfiguration(); + mockPnpmConfigChanged(); + // Only react changed in the default catalog; lodash is unchanged + mockOldCatalogs({ + default: { react: '^17.0.0', lodash: '^4.17.21' }, + tools: { typescript: '~5.3.0', eslint: '^8.50.0' } + }); + + const projectChangeAnalyzer: ProjectChangeAnalyzer = new ProjectChangeAnalyzer(rushConfiguration); + const terminal: Terminal = new Terminal(new StringBufferTerminalProvider(true)); + + const changedProjects = await projectChangeAnalyzer.getChangedProjectsAsync({ + enableFiltering: false, + includeExternalDependencies: false, + targetBranchName: 'main', + terminal + }); + + // 'a' uses "catalog:" for react (changed) and lodash (unchanged) — should be detected + expect(changedProjects.has(rushConfiguration.getProjectByName('a')!)).toBe(true); + // 'd' uses "catalog:" for lodash only (unchanged) — should NOT be detected + expect(changedProjects.has(rushConfiguration.getProjectByName('d')!)).toBe(false); + // 'b' uses "catalog:tools" for typescript — tools catalog is unchanged + expect(changedProjects.has(rushConfiguration.getProjectByName('b')!)).toBe(false); + // 'e' uses "catalog:tools" for eslint — tools catalog is unchanged + expect(changedProjects.has(rushConfiguration.getProjectByName('e')!)).toBe(false); + // 'c' has no catalog deps + expect(changedProjects.has(rushConfiguration.getProjectByName('c')!)).toBe(false); + }); + + it('marks project when its specific package version changed in catalog, even if other packages in same catalog are unchanged', async () => { + const rushConfiguration: RushConfiguration = getCatalogRushConfiguration(); + mockPnpmConfigChanged(); + // Only lodash changed in the default catalog; react is unchanged + mockOldCatalogs({ + default: { react: '^18.0.0', lodash: '^4.16.0' }, + tools: { typescript: '~5.3.0', eslint: '^8.50.0' } + }); + + const projectChangeAnalyzer: ProjectChangeAnalyzer = new ProjectChangeAnalyzer(rushConfiguration); + const terminal: Terminal = new Terminal(new StringBufferTerminalProvider(true)); + + const changedProjects = await projectChangeAnalyzer.getChangedProjectsAsync({ + enableFiltering: false, + includeExternalDependencies: false, + targetBranchName: 'main', + terminal + }); + + // 'a' uses "catalog:" for react (unchanged) and lodash (changed) — should be detected + expect(changedProjects.has(rushConfiguration.getProjectByName('a')!)).toBe(true); + // 'd' uses "catalog:" for lodash (changed) — should be detected + expect(changedProjects.has(rushConfiguration.getProjectByName('d')!)).toBe(true); + // 'b' uses "catalog:tools" for typescript — tools catalog is unchanged + expect(changedProjects.has(rushConfiguration.getProjectByName('b')!)).toBe(false); + // 'e' uses "catalog:tools" for eslint — tools catalog is unchanged + expect(changedProjects.has(rushConfiguration.getProjectByName('e')!)).toBe(false); + // 'c' has no catalog deps + expect(changedProjects.has(rushConfiguration.getProjectByName('c')!)).toBe(false); + }); + + it('only marks projects whose specific named catalog package changed', async () => { + const rushConfiguration: RushConfiguration = getCatalogRushConfiguration(); + mockPnpmConfigChanged(); + // Only typescript changed in the tools catalog; eslint is unchanged + mockOldCatalogs({ + default: { react: '^18.0.0', lodash: '^4.17.21' }, + tools: { typescript: '~5.2.0', eslint: '^8.50.0' } + }); + + const projectChangeAnalyzer: ProjectChangeAnalyzer = new ProjectChangeAnalyzer(rushConfiguration); + const terminal: Terminal = new Terminal(new StringBufferTerminalProvider(true)); + + const changedProjects = await projectChangeAnalyzer.getChangedProjectsAsync({ + enableFiltering: false, + includeExternalDependencies: false, + targetBranchName: 'main', + terminal + }); + + // 'b' uses "catalog:tools" for typescript (changed) — should be detected + expect(changedProjects.has(rushConfiguration.getProjectByName('b')!)).toBe(true); + // 'e' uses "catalog:tools" for eslint only (unchanged) — should NOT be detected + expect(changedProjects.has(rushConfiguration.getProjectByName('e')!)).toBe(false); + // 'a' uses "catalog:" (default) — default catalog is unchanged + expect(changedProjects.has(rushConfiguration.getProjectByName('a')!)).toBe(false); + // 'd' uses "catalog:" (default) — default catalog is unchanged + expect(changedProjects.has(rushConfiguration.getProjectByName('d')!)).toBe(false); + // 'c' has no catalog deps + expect(changedProjects.has(rushConfiguration.getProjectByName('c')!)).toBe(false); + }); + + it('marks project when its specific named catalog package changed', async () => { + const rushConfiguration: RushConfiguration = getCatalogRushConfiguration(); + mockPnpmConfigChanged(); + // Only eslint changed in the tools catalog; typescript is unchanged + mockOldCatalogs({ + default: { react: '^18.0.0', lodash: '^4.17.21' }, + tools: { typescript: '~5.3.0', eslint: '^8.40.0' } + }); + + const projectChangeAnalyzer: ProjectChangeAnalyzer = new ProjectChangeAnalyzer(rushConfiguration); + const terminal: Terminal = new Terminal(new StringBufferTerminalProvider(true)); + + const changedProjects = await projectChangeAnalyzer.getChangedProjectsAsync({ + enableFiltering: false, + includeExternalDependencies: false, + targetBranchName: 'main', + terminal + }); + + // 'e' uses "catalog:tools" for eslint (changed) — should be detected + expect(changedProjects.has(rushConfiguration.getProjectByName('e')!)).toBe(true); + // 'b' uses "catalog:tools" for typescript only (unchanged) — should NOT be detected + expect(changedProjects.has(rushConfiguration.getProjectByName('b')!)).toBe(false); + // 'a' uses "catalog:" (default) — default catalog is unchanged + expect(changedProjects.has(rushConfiguration.getProjectByName('a')!)).toBe(false); + // 'd' uses "catalog:" (default) — default catalog is unchanged + expect(changedProjects.has(rushConfiguration.getProjectByName('d')!)).toBe(false); + // 'c' has no catalog deps + expect(changedProjects.has(rushConfiguration.getProjectByName('c')!)).toBe(false); + }); + + it('all catalog-using projects marked as changed when no old catalog existed', async () => { + const rushConfiguration: RushConfiguration = getCatalogRushConfiguration(); + mockPnpmConfigChanged(); + // Old file did not exist in git + mockGetBlobContentAsync.mockImplementation(() => { + return Promise.reject(new Error('fatal: path not found')); + }); + + const projectChangeAnalyzer: ProjectChangeAnalyzer = new ProjectChangeAnalyzer(rushConfiguration); + const terminal: Terminal = new Terminal(new StringBufferTerminalProvider(true)); + + const changedProjects = await projectChangeAnalyzer.getChangedProjectsAsync({ + enableFiltering: false, + includeExternalDependencies: false, + targetBranchName: 'main', + terminal + }); + + // All catalog-using projects detected: 'a' (default: react, lodash), 'b' (tools: typescript), 'd' (default: lodash), 'e' (tools: eslint) + expect(changedProjects.has(rushConfiguration.getProjectByName('a')!)).toBe(true); + expect(changedProjects.has(rushConfiguration.getProjectByName('b')!)).toBe(true); + expect(changedProjects.has(rushConfiguration.getProjectByName('d')!)).toBe(true); + expect(changedProjects.has(rushConfiguration.getProjectByName('e')!)).toBe(true); + // 'c' has no catalog deps + expect(changedProjects.has(rushConfiguration.getProjectByName('c')!)).toBe(false); + }); + }); + + describe('subspace catalog change detection', () => { + function getSubspaceCatalogRushConfiguration(): RushConfiguration { + return RushConfiguration.loadFromConfigurationFile( + resolve(__dirname, 'repoWithSubspacesCatalogs', 'rush.json') + ); + } + + it('detects change to default catalog in subspace', async () => { + const rushConfiguration: RushConfiguration = getSubspaceCatalogRushConfiguration(); + + // Only the subspace pnpm-config.json changed + mockGetRepoChanges.mockReturnValue( + new Map([ + [ + 'common/config/subspaces/project-change-analyzer-test-subspace/pnpm-config.json', + { mode: 'modified', newhash: 'newhash', oldhash: 'oldhash', status: 'M' } + ] + ]) + ); + + // foo version bumped in the default catalog + mockGetBlobContentAsync.mockImplementation(() => { + return Promise.resolve( + JSON.stringify({ + globalCatalogs: { + default: { foo: '~1.0.0', bar: '^3.0.0' }, + tools: { typescript: '~5.3.0', eslint: '^8.50.0' } + } + }) + ); + }); + + const projectChangeAnalyzer: ProjectChangeAnalyzer = new ProjectChangeAnalyzer(rushConfiguration); + const terminal: Terminal = new Terminal(new StringBufferTerminalProvider(true)); + + const changedProjects = await projectChangeAnalyzer.getChangedProjectsAsync({ + enableFiltering: false, + includeExternalDependencies: false, + targetBranchName: 'main', + terminal + }); + + // 'd' uses "catalog:" (default) for foo (changed) — should be detected + expect(changedProjects.has(rushConfiguration.getProjectByName('d')!)).toBe(true); + // 'g' uses "catalog:" (default) for bar only (unchanged) — should NOT be detected + expect(changedProjects.has(rushConfiguration.getProjectByName('g')!)).toBe(false); + // 'e' uses "catalog:tools" for typescript — tools catalog is unchanged + expect(changedProjects.has(rushConfiguration.getProjectByName('e')!)).toBe(false); + // 'h' uses "catalog:tools" for eslint — tools catalog is unchanged + expect(changedProjects.has(rushConfiguration.getProjectByName('h')!)).toBe(false); + // 'f' has no catalog deps + expect(changedProjects.has(rushConfiguration.getProjectByName('f')!)).toBe(false); + // default subspace projects should not be affected + expect(changedProjects.has(rushConfiguration.getProjectByName('a')!)).toBe(false); + expect(changedProjects.has(rushConfiguration.getProjectByName('b')!)).toBe(false); + expect(changedProjects.has(rushConfiguration.getProjectByName('c')!)).toBe(false); + }); + + it('detects change to named catalog in subspace', async () => { + const rushConfiguration: RushConfiguration = getSubspaceCatalogRushConfiguration(); + + // Only the subspace pnpm-config.json changed + mockGetRepoChanges.mockReturnValue( + new Map([ + [ + 'common/config/subspaces/project-change-analyzer-test-subspace/pnpm-config.json', + { mode: 'modified', newhash: 'newhash', oldhash: 'oldhash', status: 'M' } + ] + ]) + ); + + // typescript version bumped in the tools catalog + mockGetBlobContentAsync.mockImplementation(() => { + return Promise.resolve( + JSON.stringify({ + globalCatalogs: { + default: { foo: '~2.0.0', bar: '^3.0.0' }, + tools: { typescript: '~5.2.0', eslint: '^8.50.0' } + } + }) + ); + }); + + const projectChangeAnalyzer: ProjectChangeAnalyzer = new ProjectChangeAnalyzer(rushConfiguration); + const terminal: Terminal = new Terminal(new StringBufferTerminalProvider(true)); + + const changedProjects = await projectChangeAnalyzer.getChangedProjectsAsync({ + enableFiltering: false, + includeExternalDependencies: false, + targetBranchName: 'main', + terminal + }); + + // 'e' uses "catalog:tools" for typescript (changed) — should be detected + expect(changedProjects.has(rushConfiguration.getProjectByName('e')!)).toBe(true); + // 'h' uses "catalog:tools" for eslint only (unchanged) — should NOT be detected + expect(changedProjects.has(rushConfiguration.getProjectByName('h')!)).toBe(false); + // 'd' uses "catalog:" (default) for foo — default catalog is unchanged + expect(changedProjects.has(rushConfiguration.getProjectByName('d')!)).toBe(false); + // 'g' uses "catalog:" (default) for bar — default catalog is unchanged + expect(changedProjects.has(rushConfiguration.getProjectByName('g')!)).toBe(false); + // 'f' has no catalog deps + expect(changedProjects.has(rushConfiguration.getProjectByName('f')!)).toBe(false); + // default subspace projects should not be affected + expect(changedProjects.has(rushConfiguration.getProjectByName('a')!)).toBe(false); + }); + + it('only marks subspace projects whose specific default catalog package changed', async () => { + const rushConfiguration: RushConfiguration = getSubspaceCatalogRushConfiguration(); + + mockGetRepoChanges.mockReturnValue( + new Map([ + [ + 'common/config/subspaces/project-change-analyzer-test-subspace/pnpm-config.json', + { mode: 'modified', newhash: 'newhash', oldhash: 'oldhash', status: 'M' } + ] + ]) + ); + + // Only bar changed in the default catalog; foo is unchanged + mockGetBlobContentAsync.mockImplementation(() => { + return Promise.resolve( + JSON.stringify({ + globalCatalogs: { + default: { foo: '~2.0.0', bar: '^2.0.0' }, + tools: { typescript: '~5.3.0', eslint: '^8.50.0' } + } + }) + ); + }); + + const projectChangeAnalyzer: ProjectChangeAnalyzer = new ProjectChangeAnalyzer(rushConfiguration); + const terminal: Terminal = new Terminal(new StringBufferTerminalProvider(true)); + + const changedProjects = await projectChangeAnalyzer.getChangedProjectsAsync({ + enableFiltering: false, + includeExternalDependencies: false, + targetBranchName: 'main', + terminal + }); + + // 'g' uses "catalog:" for bar (changed) — should be detected + expect(changedProjects.has(rushConfiguration.getProjectByName('g')!)).toBe(true); + // 'd' uses "catalog:" for foo only (unchanged) — should NOT be detected + expect(changedProjects.has(rushConfiguration.getProjectByName('d')!)).toBe(false); + // 'e' uses "catalog:tools" — tools catalog is unchanged + expect(changedProjects.has(rushConfiguration.getProjectByName('e')!)).toBe(false); + // 'h' uses "catalog:tools" — tools catalog is unchanged + expect(changedProjects.has(rushConfiguration.getProjectByName('h')!)).toBe(false); + // 'f' has no catalog deps + expect(changedProjects.has(rushConfiguration.getProjectByName('f')!)).toBe(false); + }); + + it('only marks subspace projects whose specific named catalog package changed', async () => { + const rushConfiguration: RushConfiguration = getSubspaceCatalogRushConfiguration(); + + mockGetRepoChanges.mockReturnValue( + new Map([ + [ + 'common/config/subspaces/project-change-analyzer-test-subspace/pnpm-config.json', + { mode: 'modified', newhash: 'newhash', oldhash: 'oldhash', status: 'M' } + ] + ]) + ); + + // Only eslint changed in the tools catalog; typescript is unchanged + mockGetBlobContentAsync.mockImplementation(() => { + return Promise.resolve( + JSON.stringify({ + globalCatalogs: { + default: { foo: '~2.0.0', bar: '^3.0.0' }, + tools: { typescript: '~5.3.0', eslint: '^8.40.0' } + } + }) + ); + }); + + const projectChangeAnalyzer: ProjectChangeAnalyzer = new ProjectChangeAnalyzer(rushConfiguration); + const terminal: Terminal = new Terminal(new StringBufferTerminalProvider(true)); + + const changedProjects = await projectChangeAnalyzer.getChangedProjectsAsync({ + enableFiltering: false, + includeExternalDependencies: false, + targetBranchName: 'main', + terminal + }); + + // 'h' uses "catalog:tools" for eslint (changed) — should be detected + expect(changedProjects.has(rushConfiguration.getProjectByName('h')!)).toBe(true); + // 'e' uses "catalog:tools" for typescript only (unchanged) — should NOT be detected + expect(changedProjects.has(rushConfiguration.getProjectByName('e')!)).toBe(false); + // 'd' uses "catalog:" (default) — default catalog is unchanged + expect(changedProjects.has(rushConfiguration.getProjectByName('d')!)).toBe(false); + // 'g' uses "catalog:" (default) — default catalog is unchanged + expect(changedProjects.has(rushConfiguration.getProjectByName('g')!)).toBe(false); + // 'f' has no catalog deps + expect(changedProjects.has(rushConfiguration.getProjectByName('f')!)).toBe(false); + }); + + it('detects changes when multiple subspace pnpm-configs have catalog changes', async () => { + const rushConfiguration: RushConfiguration = getSubspaceCatalogRushConfiguration(); + + // Both the default subspace and named subspace pnpm-config.json files changed + mockGetRepoChanges.mockReturnValue( + new Map([ + [ + 'common/config/subspaces/default/pnpm-config.json', + { mode: 'modified', newhash: 'newhash1', oldhash: 'oldhash1', status: 'M' } + ], + [ + 'common/config/subspaces/project-change-analyzer-test-subspace/pnpm-config.json', + { mode: 'modified', newhash: 'newhash2', oldhash: 'oldhash2', status: 'M' } + ] + ]) + ); + + // Return old catalogs based on which config is being read + mockGetBlobContentAsync.mockImplementation((opts: { blobSpec: string; repositoryRoot: string }) => { + if (opts.blobSpec.includes('default/pnpm-config.json')) { + // react version bumped in the default subspace + return Promise.resolve(JSON.stringify({ globalCatalogs: { default: { react: '^17.0.0' } } })); + } + if (opts.blobSpec.includes('project-change-analyzer-test-subspace/pnpm-config.json')) { + // foo version bumped in the named subspace; bar, tools unchanged + return Promise.resolve( + JSON.stringify({ + globalCatalogs: { + default: { foo: '~1.0.0', bar: '^3.0.0' }, + tools: { typescript: '~5.3.0', eslint: '^8.50.0' } + } + }) + ); + } + return Promise.resolve('{}'); + }); + + const projectChangeAnalyzer: ProjectChangeAnalyzer = new ProjectChangeAnalyzer(rushConfiguration); + const terminal: Terminal = new Terminal(new StringBufferTerminalProvider(true)); + + const changedProjects = await projectChangeAnalyzer.getChangedProjectsAsync({ + enableFiltering: false, + includeExternalDependencies: false, + targetBranchName: 'main', + terminal + }); + + // 'a' uses "catalog:" (default) for react in the default subspace — should be detected + expect(changedProjects.has(rushConfiguration.getProjectByName('a')!)).toBe(true); + // 'd' uses "catalog:" (default) for foo (changed) in the named subspace — should be detected + expect(changedProjects.has(rushConfiguration.getProjectByName('d')!)).toBe(true); + // 'g' uses "catalog:" (default) for bar (unchanged) in the named subspace — should NOT be detected + expect(changedProjects.has(rushConfiguration.getProjectByName('g')!)).toBe(false); + // 'b' has no catalog deps in default subspace + expect(changedProjects.has(rushConfiguration.getProjectByName('b')!)).toBe(false); + // 'c' has no catalog deps in default subspace + expect(changedProjects.has(rushConfiguration.getProjectByName('c')!)).toBe(false); + // 'e' uses "catalog:tools" for typescript — tools catalog is unchanged in the named subspace + expect(changedProjects.has(rushConfiguration.getProjectByName('e')!)).toBe(false); + // 'h' uses "catalog:tools" for eslint — tools catalog is unchanged in the named subspace + expect(changedProjects.has(rushConfiguration.getProjectByName('h')!)).toBe(false); + // 'f' has no catalog deps + expect(changedProjects.has(rushConfiguration.getProjectByName('f')!)).toBe(false); + }); + }); }); describe('isPackageJsonVersionOnlyChange', () => { diff --git a/libraries/rush-lib/src/logic/test/repoWithCatalogs/a/package.json b/libraries/rush-lib/src/logic/test/repoWithCatalogs/a/package.json new file mode 100644 index 00000000000..26e2a7fe95d --- /dev/null +++ b/libraries/rush-lib/src/logic/test/repoWithCatalogs/a/package.json @@ -0,0 +1,9 @@ +{ + "name": "a", + "version": "1.0.0", + "description": "Project A uses default catalog", + "dependencies": { + "react": "catalog:", + "lodash": "catalog:" + } +} diff --git a/libraries/rush-lib/src/logic/test/repoWithCatalogs/b/package.json b/libraries/rush-lib/src/logic/test/repoWithCatalogs/b/package.json new file mode 100644 index 00000000000..1f535dd8543 --- /dev/null +++ b/libraries/rush-lib/src/logic/test/repoWithCatalogs/b/package.json @@ -0,0 +1,11 @@ +{ + "name": "b", + "version": "1.0.0", + "description": "Project B depends on A and uses tools catalog", + "dependencies": { + "a": "workspace:*" + }, + "devDependencies": { + "typescript": "catalog:tools" + } +} diff --git a/libraries/rush-lib/src/logic/test/repoWithCatalogs/c/package.json b/libraries/rush-lib/src/logic/test/repoWithCatalogs/c/package.json new file mode 100644 index 00000000000..ea46bdf7c6e --- /dev/null +++ b/libraries/rush-lib/src/logic/test/repoWithCatalogs/c/package.json @@ -0,0 +1,5 @@ +{ + "name": "c", + "version": "1.0.0", + "description": "Project C has no catalog dependencies" +} diff --git a/libraries/rush-lib/src/logic/test/repoWithCatalogs/common/config/rush/pnpm-config.json b/libraries/rush-lib/src/logic/test/repoWithCatalogs/common/config/rush/pnpm-config.json new file mode 100644 index 00000000000..32448ef0221 --- /dev/null +++ b/libraries/rush-lib/src/logic/test/repoWithCatalogs/common/config/rush/pnpm-config.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/pnpm-config.schema.json", + "globalCatalogs": { + "default": { + "react": "^18.0.0", + "lodash": "^4.17.21" + }, + "tools": { + "typescript": "~5.3.0", + "eslint": "^8.50.0" + } + } +} diff --git a/libraries/rush-lib/src/logic/test/repoWithCatalogs/d/package.json b/libraries/rush-lib/src/logic/test/repoWithCatalogs/d/package.json new file mode 100644 index 00000000000..31e197b4f81 --- /dev/null +++ b/libraries/rush-lib/src/logic/test/repoWithCatalogs/d/package.json @@ -0,0 +1,8 @@ +{ + "name": "d", + "version": "1.0.0", + "description": "Project D uses only lodash from default catalog", + "dependencies": { + "lodash": "catalog:" + } +} diff --git a/libraries/rush-lib/src/logic/test/repoWithCatalogs/e/package.json b/libraries/rush-lib/src/logic/test/repoWithCatalogs/e/package.json new file mode 100644 index 00000000000..8992f865145 --- /dev/null +++ b/libraries/rush-lib/src/logic/test/repoWithCatalogs/e/package.json @@ -0,0 +1,8 @@ +{ + "name": "e", + "version": "1.0.0", + "description": "Project E uses only eslint from tools catalog", + "devDependencies": { + "eslint": "catalog:tools" + } +} diff --git a/libraries/rush-lib/src/logic/test/repoWithCatalogs/rush.json b/libraries/rush-lib/src/logic/test/repoWithCatalogs/rush.json new file mode 100644 index 00000000000..848cf846c1a --- /dev/null +++ b/libraries/rush-lib/src/logic/test/repoWithCatalogs/rush.json @@ -0,0 +1,27 @@ +{ + "pnpmVersion": "9.15.0", + "rushVersion": "1.0.5", + + "projects": [ + { + "packageName": "a", + "projectFolder": "a" + }, + { + "packageName": "b", + "projectFolder": "b" + }, + { + "packageName": "c", + "projectFolder": "c" + }, + { + "packageName": "d", + "projectFolder": "d" + }, + { + "packageName": "e", + "projectFolder": "e" + } + ] +} diff --git a/libraries/rush-lib/src/logic/test/repoWithSubspacesCatalogs/a/package.json b/libraries/rush-lib/src/logic/test/repoWithSubspacesCatalogs/a/package.json new file mode 100644 index 00000000000..5c102c007ba --- /dev/null +++ b/libraries/rush-lib/src/logic/test/repoWithSubspacesCatalogs/a/package.json @@ -0,0 +1,8 @@ +{ + "name": "a", + "version": "1.0.0", + "description": "Test package a", + "dependencies": { + "react": "catalog:" + } +} diff --git a/libraries/rush-lib/src/logic/test/repoWithSubspacesCatalogs/b/package.json b/libraries/rush-lib/src/logic/test/repoWithSubspacesCatalogs/b/package.json new file mode 100644 index 00000000000..3a7fdf92a46 --- /dev/null +++ b/libraries/rush-lib/src/logic/test/repoWithSubspacesCatalogs/b/package.json @@ -0,0 +1,8 @@ +{ + "name": "b", + "version": "2.0.0", + "description": "Test package b", + "dependencies": { + "foo": "~1.0.0" + } +} diff --git a/libraries/rush-lib/src/logic/test/repoWithSubspacesCatalogs/c/package.json b/libraries/rush-lib/src/logic/test/repoWithSubspacesCatalogs/c/package.json new file mode 100644 index 00000000000..84d308bd6c0 --- /dev/null +++ b/libraries/rush-lib/src/logic/test/repoWithSubspacesCatalogs/c/package.json @@ -0,0 +1,8 @@ +{ + "name": "c", + "version": "3.1.1", + "description": "Test package c", + "dependencies": { + "b": "workspace:*" + } +} diff --git a/libraries/rush-lib/src/logic/test/repoWithSubspacesCatalogs/common/config/rush/experiments.json b/libraries/rush-lib/src/logic/test/repoWithSubspacesCatalogs/common/config/rush/experiments.json new file mode 100644 index 00000000000..a20c6d24388 --- /dev/null +++ b/libraries/rush-lib/src/logic/test/repoWithSubspacesCatalogs/common/config/rush/experiments.json @@ -0,0 +1,3 @@ +{ + "exemptDecoupledDependenciesBetweenSubspaces": true +} diff --git a/libraries/rush-lib/src/logic/test/repoWithSubspacesCatalogs/common/config/rush/pnpm-config.json b/libraries/rush-lib/src/logic/test/repoWithSubspacesCatalogs/common/config/rush/pnpm-config.json new file mode 100644 index 00000000000..fdb1cb3ac59 --- /dev/null +++ b/libraries/rush-lib/src/logic/test/repoWithSubspacesCatalogs/common/config/rush/pnpm-config.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/pnpm-config.schema.json", + "useWorkspaces": true +} diff --git a/libraries/rush-lib/src/logic/test/repoWithSubspacesCatalogs/common/config/rush/subspaces.json b/libraries/rush-lib/src/logic/test/repoWithSubspacesCatalogs/common/config/rush/subspaces.json new file mode 100644 index 00000000000..ab4e6b3a3c4 --- /dev/null +++ b/libraries/rush-lib/src/logic/test/repoWithSubspacesCatalogs/common/config/rush/subspaces.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/subspaces.schema.json", + "subspacesEnabled": true, + "subspaceNames": ["project-change-analyzer-test-subspace"] +} diff --git a/libraries/rush-lib/src/logic/test/repoWithSubspacesCatalogs/common/config/rush/version-policies.json b/libraries/rush-lib/src/logic/test/repoWithSubspacesCatalogs/common/config/rush/version-policies.json new file mode 100644 index 00000000000..fe51488c706 --- /dev/null +++ b/libraries/rush-lib/src/logic/test/repoWithSubspacesCatalogs/common/config/rush/version-policies.json @@ -0,0 +1 @@ +[] diff --git a/libraries/rush-lib/src/logic/test/repoWithSubspacesCatalogs/common/config/subspaces/default/.pnpmfile.cjs b/libraries/rush-lib/src/logic/test/repoWithSubspacesCatalogs/common/config/subspaces/default/.pnpmfile.cjs new file mode 100644 index 00000000000..ee041f83a4e --- /dev/null +++ b/libraries/rush-lib/src/logic/test/repoWithSubspacesCatalogs/common/config/subspaces/default/.pnpmfile.cjs @@ -0,0 +1,9 @@ +'use strict'; + +module.exports = { + hooks: { + readPackage(pkgJson) { + return pkgJson; + } + } +}; diff --git a/libraries/rush-lib/src/logic/test/repoWithSubspacesCatalogs/common/config/subspaces/default/common-versions.json b/libraries/rush-lib/src/logic/test/repoWithSubspacesCatalogs/common/config/subspaces/default/common-versions.json new file mode 100644 index 00000000000..9280fe7b96d --- /dev/null +++ b/libraries/rush-lib/src/logic/test/repoWithSubspacesCatalogs/common/config/subspaces/default/common-versions.json @@ -0,0 +1,8 @@ +/** + * This configuration file specifies NPM dependency version selections that affect all projects + * in a Rush repo. More documentation is available on the Rush website: https://rushjs.io + */ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/common-versions.schema.json", + "ensureConsistentVersions": true +} diff --git a/libraries/rush-lib/src/logic/test/repoWithSubspacesCatalogs/common/config/subspaces/default/pnpm-config.json b/libraries/rush-lib/src/logic/test/repoWithSubspacesCatalogs/common/config/subspaces/default/pnpm-config.json new file mode 100644 index 00000000000..291ec697142 --- /dev/null +++ b/libraries/rush-lib/src/logic/test/repoWithSubspacesCatalogs/common/config/subspaces/default/pnpm-config.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/pnpm-config.schema.json", + "globalCatalogs": { + "default": { + "react": "^18.0.0" + } + } +} diff --git a/libraries/rush-lib/src/logic/test/repoWithSubspacesCatalogs/common/config/subspaces/project-change-analyzer-test-subspace/.pnpmfile.cjs b/libraries/rush-lib/src/logic/test/repoWithSubspacesCatalogs/common/config/subspaces/project-change-analyzer-test-subspace/.pnpmfile.cjs new file mode 100644 index 00000000000..ee041f83a4e --- /dev/null +++ b/libraries/rush-lib/src/logic/test/repoWithSubspacesCatalogs/common/config/subspaces/project-change-analyzer-test-subspace/.pnpmfile.cjs @@ -0,0 +1,9 @@ +'use strict'; + +module.exports = { + hooks: { + readPackage(pkgJson) { + return pkgJson; + } + } +}; diff --git a/libraries/rush-lib/src/logic/test/repoWithSubspacesCatalogs/common/config/subspaces/project-change-analyzer-test-subspace/common-versions.json b/libraries/rush-lib/src/logic/test/repoWithSubspacesCatalogs/common/config/subspaces/project-change-analyzer-test-subspace/common-versions.json new file mode 100644 index 00000000000..9280fe7b96d --- /dev/null +++ b/libraries/rush-lib/src/logic/test/repoWithSubspacesCatalogs/common/config/subspaces/project-change-analyzer-test-subspace/common-versions.json @@ -0,0 +1,8 @@ +/** + * This configuration file specifies NPM dependency version selections that affect all projects + * in a Rush repo. More documentation is available on the Rush website: https://rushjs.io + */ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/common-versions.schema.json", + "ensureConsistentVersions": true +} diff --git a/libraries/rush-lib/src/logic/test/repoWithSubspacesCatalogs/common/config/subspaces/project-change-analyzer-test-subspace/pnpm-config.json b/libraries/rush-lib/src/logic/test/repoWithSubspacesCatalogs/common/config/subspaces/project-change-analyzer-test-subspace/pnpm-config.json new file mode 100644 index 00000000000..cc0a0ac626f --- /dev/null +++ b/libraries/rush-lib/src/logic/test/repoWithSubspacesCatalogs/common/config/subspaces/project-change-analyzer-test-subspace/pnpm-config.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/pnpm-config.schema.json", + "globalCatalogs": { + "default": { + "foo": "~2.0.0", + "bar": "^3.0.0" + }, + "tools": { + "typescript": "~5.3.0", + "eslint": "^8.50.0" + } + } +} diff --git a/libraries/rush-lib/src/logic/test/repoWithSubspacesCatalogs/d/package.json b/libraries/rush-lib/src/logic/test/repoWithSubspacesCatalogs/d/package.json new file mode 100644 index 00000000000..bf308cf79f5 --- /dev/null +++ b/libraries/rush-lib/src/logic/test/repoWithSubspacesCatalogs/d/package.json @@ -0,0 +1,8 @@ +{ + "name": "d", + "version": "4.1.1", + "description": "Test package d", + "dependencies": { + "foo": "catalog:" + } +} diff --git a/libraries/rush-lib/src/logic/test/repoWithSubspacesCatalogs/e/package.json b/libraries/rush-lib/src/logic/test/repoWithSubspacesCatalogs/e/package.json new file mode 100644 index 00000000000..1d6b2e26469 --- /dev/null +++ b/libraries/rush-lib/src/logic/test/repoWithSubspacesCatalogs/e/package.json @@ -0,0 +1,11 @@ +{ + "name": "e", + "version": "10.10.0", + "description": "Test package e", + "dependencies": { + "d": "workspace:*" + }, + "devDependencies": { + "typescript": "catalog:tools" + } +} diff --git a/libraries/rush-lib/src/logic/test/repoWithSubspacesCatalogs/f/package.json b/libraries/rush-lib/src/logic/test/repoWithSubspacesCatalogs/f/package.json new file mode 100644 index 00000000000..5d6ea8a762e --- /dev/null +++ b/libraries/rush-lib/src/logic/test/repoWithSubspacesCatalogs/f/package.json @@ -0,0 +1,5 @@ +{ + "name": "f", + "version": "10.10.0", + "description": "Test package f" +} diff --git a/libraries/rush-lib/src/logic/test/repoWithSubspacesCatalogs/g/package.json b/libraries/rush-lib/src/logic/test/repoWithSubspacesCatalogs/g/package.json new file mode 100644 index 00000000000..42bbb7abbd7 --- /dev/null +++ b/libraries/rush-lib/src/logic/test/repoWithSubspacesCatalogs/g/package.json @@ -0,0 +1,8 @@ +{ + "name": "g", + "version": "1.0.0", + "description": "Test package g — uses only bar from default catalog", + "dependencies": { + "bar": "catalog:" + } +} diff --git a/libraries/rush-lib/src/logic/test/repoWithSubspacesCatalogs/h/package.json b/libraries/rush-lib/src/logic/test/repoWithSubspacesCatalogs/h/package.json new file mode 100644 index 00000000000..d6cc9ac86d6 --- /dev/null +++ b/libraries/rush-lib/src/logic/test/repoWithSubspacesCatalogs/h/package.json @@ -0,0 +1,8 @@ +{ + "name": "h", + "version": "1.0.0", + "description": "Test package h — uses only eslint from tools catalog", + "devDependencies": { + "eslint": "catalog:tools" + } +} diff --git a/libraries/rush-lib/src/logic/test/repoWithSubspacesCatalogs/rush.json b/libraries/rush-lib/src/logic/test/repoWithSubspacesCatalogs/rush.json new file mode 100644 index 00000000000..4714895adf8 --- /dev/null +++ b/libraries/rush-lib/src/logic/test/repoWithSubspacesCatalogs/rush.json @@ -0,0 +1,44 @@ +{ + "rushVersion": "1.0.5", + "pnpmVersion": "9.15.0", + + "projects": [ + { + "packageName": "a", + "projectFolder": "a" + }, + { + "packageName": "b", + "projectFolder": "b" + }, + { + "packageName": "c", + "projectFolder": "c" + }, + { + "packageName": "d", + "projectFolder": "d", + "subspaceName": "project-change-analyzer-test-subspace" + }, + { + "packageName": "e", + "projectFolder": "e", + "subspaceName": "project-change-analyzer-test-subspace" + }, + { + "packageName": "f", + "projectFolder": "f", + "subspaceName": "project-change-analyzer-test-subspace" + }, + { + "packageName": "g", + "projectFolder": "g", + "subspaceName": "project-change-analyzer-test-subspace" + }, + { + "packageName": "h", + "projectFolder": "h", + "subspaceName": "project-change-analyzer-test-subspace" + } + ] +}