diff --git a/common/changes/@microsoft/rush/bmiddha-resolve-catalogs-in-injected-deps_2026-10-15-05-28.json b/common/changes/@microsoft/rush/bmiddha-resolve-catalogs-in-injected-deps_2026-10-15-05-28.json new file mode 100644 index 0000000000..e86af7239e --- /dev/null +++ b/common/changes/@microsoft/rush/bmiddha-resolve-catalogs-in-injected-deps_2026-10-15-05-28.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "packageName": "@microsoft/rush", + "comment": "Resolve PNPM \"catalog:\" references in projects injected from other subspaces.", + "type": "patch" + } + ], + "packageName": "@microsoft/rush", + "email": "223556219+Copilot@users.noreply.github.com" +} diff --git a/libraries/rush-lib/src/api/Subspace.ts b/libraries/rush-lib/src/api/Subspace.ts index 611f96859a..766bc8038a 100644 --- a/libraries/rush-lib/src/api/Subspace.ts +++ b/libraries/rush-lib/src/api/Subspace.ts @@ -15,7 +15,8 @@ import { CommonVersionsConfiguration } from './CommonVersionsConfiguration'; import { RepoStateFile } from '../logic/RepoStateFile'; import type { PnpmPackageManager } from './packageManager/PnpmPackageManager'; import { PnpmOptionsConfiguration } from '../logic/pnpm/PnpmOptionsConfiguration'; -import { getEffectivePnpmCatalogs } from '../logic/pnpm/EffectivePnpmOptions'; +import { getEffectivePnpmCatalogs, type PnpmCatalogs } from '../logic/pnpm/EffectivePnpmOptions'; +import { tryGetPnpmCatalogVersion, tryParsePnpmCatalogName } from '../logic/pnpm/PnpmCatalogReference'; import { SubspacePnpmfileConfiguration } from '../logic/pnpm/SubspacePnpmfileConfiguration'; import type { ISubspacePnpmfileShimSettings } from '../logic/pnpm/IPnpmfile'; @@ -466,6 +467,26 @@ export class Subspace { this.#rushConfiguration.projects.map((rushProject) => rushProject.packageName) ); + const catalogsBySubspaceName: Map = new Map(); + const getOtherSubspaceCatalogs: (rushProject: RushConfigurationProject) => PnpmCatalogs | undefined = ( + rushProject: RushConfigurationProject + ) => { + // PNPM resolves the catalog references of this subspace's own projects + if (this.contains(rushProject)) { + return undefined; + } + + const { subspace } = rushProject; + if (!catalogsBySubspaceName.has(subspace.subspaceName)) { + catalogsBySubspaceName.set( + subspace.subspaceName, + getEffectivePnpmCatalogs(subspace, this.#rushConfiguration) + ); + } + + return catalogsBySubspaceName.get(subspace.subspaceName); + }; + // get all related package.json while (relatedProjects.length > 0) { const rushProject: RushConfigurationProject = relatedProjects.pop()!; @@ -491,12 +512,14 @@ export class Subspace { } } + // Hash the resolved versions so that editing a catalog entry triggers a reinstall + const catalogs: PnpmCatalogs | undefined = getOtherSubspaceCatalogs(rushProject); allPackageJson.push({ name, bin, - dependencies, - peerDependencies, - optionalDependencies, + dependencies: _resolveCatalogReferences(dependencies, catalogs), + peerDependencies: _resolveCatalogReferences(peerDependencies, catalogs), + optionalDependencies: _resolveCatalogReferences(optionalDependencies, catalogs), dependenciesMeta, peerDependenciesMeta, resolutions @@ -517,3 +540,29 @@ export class Subspace { return packageJsonInjectedDependenciesHash; } } + +function _resolveCatalogReferences( + dependencies: Record | undefined, + catalogs: PnpmCatalogs | undefined +): Record | undefined { + if (!dependencies || !catalogs) { + return dependencies; + } + + let result: Record | undefined; + for (const [dependencyName, versionSpecifier] of Object.entries(dependencies)) { + const catalogName: string | undefined = tryParsePnpmCatalogName(versionSpecifier); + if (catalogName !== undefined) { + const catalogVersion: string | undefined = tryGetPnpmCatalogVersion( + catalogs, + catalogName, + dependencyName + ); + if (catalogVersion !== undefined) { + (result ??= { ...dependencies })[dependencyName] = catalogVersion; + } + } + } + + return result ?? dependencies; +} diff --git a/libraries/rush-lib/src/api/test/Subspace.test.ts b/libraries/rush-lib/src/api/test/Subspace.test.ts index 4087b2b971..ac7fbcf98d 100644 --- a/libraries/rush-lib/src/api/test/Subspace.test.ts +++ b/libraries/rush-lib/src/api/test/Subspace.test.ts @@ -3,6 +3,8 @@ import * as path from 'node:path'; +import type { IPackageJson } from '@rushstack/node-core-library'; + import { RushConfiguration } from '../RushConfiguration'; import { Subspace } from '../Subspace'; @@ -139,5 +141,58 @@ describe(Subspace.name, () => { expect(hashAfter).toBeDefined(); expect(hashBefore).not.toBe(hashAfter); }); + + describe('with catalog references in injected projects from other subspaces', () => { + // "catalog-consumer" (consumer-subspace) injects "catalog-provider" and "transitive-provider" (default) + const injectedSubspaceCatalogsRushJson: string = path.resolve( + __dirname, + '../../logic/pnpm/test/repoWithInjectedSubspaceCatalogs/rush.json' + ); + + it('hashes the catalog versions instead of the catalog references', () => { + const rushConfiguration: RushConfiguration = RushConfiguration.loadFromConfigurationFile( + injectedSubspaceCatalogsRushJson + ); + const consumerSubspace: Subspace = rushConfiguration.getSubspace('consumer-subspace'); + + const hashBefore: string | undefined = consumerSubspace.getPackageJsonInjectedDependenciesHash(); + + // The default subspace's catalog versions, which differ from consumer-subspace's catalogs + const providerPackageJson: IPackageJson = + rushConfiguration.getProjectByName('catalog-provider')!.packageJson; + providerPackageJson.dependencies = { + ...providerPackageJson.dependencies, + lodash: '^4.17.21', + semver: '~7.5.4' + }; + providerPackageJson.optionalDependencies = { fsevents: '~2.3.2' }; + providerPackageJson.peerDependencies = { typescript: '~5.3.0' }; + rushConfiguration.getProjectByName('transitive-provider')!.packageJson.dependencies = { + lodash: '^4.17.21' + }; + + const hashAfter: string | undefined = consumerSubspace.getPackageJsonInjectedDependenciesHash(); + + expect(hashBefore).toBeDefined(); + expect(hashAfter).toBe(hashBefore); + }); + + it("changes when an entry in the injected project's subspace catalog changes", () => { + const rushConfiguration: RushConfiguration = RushConfiguration.loadFromConfigurationFile( + injectedSubspaceCatalogsRushJson + ); + const consumerSubspace: Subspace = rushConfiguration.getSubspace('consumer-subspace'); + + const hashBefore: string | undefined = consumerSubspace.getPackageJsonInjectedDependenciesHash(); + + rushConfiguration.pnpmOptions.globalCatalogs!.default.semver = '~7.6.0'; + + const hashAfter: string | undefined = consumerSubspace.getPackageJsonInjectedDependenciesHash(); + + expect(hashBefore).toBeDefined(); + expect(hashAfter).toBeDefined(); + expect(hashAfter).not.toBe(hashBefore); + }); + }); }); }); diff --git a/libraries/rush-lib/src/logic/pnpm/IPnpmfile.ts b/libraries/rush-lib/src/logic/pnpm/IPnpmfile.ts index 50e9ddadd2..b658c35fdc 100644 --- a/libraries/rush-lib/src/logic/pnpm/IPnpmfile.ts +++ b/libraries/rush-lib/src/logic/pnpm/IPnpmfile.ts @@ -6,6 +6,7 @@ import type { IPackageJson } from '@rushstack/node-core-library'; import type { IPnpmShrinkwrapYaml } from './PnpmShrinkwrapFile'; import type { RushConfigurationProject } from '../../api/RushConfigurationProject'; +import type { PnpmCatalogs } from './EffectivePnpmOptions'; /** * The `settings` parameter passed to {@link IPnpmfileShim.hooks.readPackage} and @@ -26,6 +27,7 @@ export interface IWorkspaceProjectInfo extends Pick { packageVersion: RushConfigurationProject['packageJson']['version']; injectedDependencies: Array; + subspaceName?: string; } /** @@ -37,6 +39,10 @@ export interface ISubspacePnpmfileShimSettings { workspaceProjects: Record; subspaceProjects: Record; userPnpmfilePath?: string; + /** + * The effective catalogs of the other subspaces whose projects are injected into this subspace. + */ + catalogsBySubspaceName?: Record; } /** diff --git a/libraries/rush-lib/src/logic/pnpm/SubspaceGlobalPnpmfileShim.ts b/libraries/rush-lib/src/logic/pnpm/SubspaceGlobalPnpmfileShim.ts index 02c143fcb3..29a3040eb7 100644 --- a/libraries/rush-lib/src/logic/pnpm/SubspaceGlobalPnpmfileShim.ts +++ b/libraries/rush-lib/src/logic/pnpm/SubspaceGlobalPnpmfileShim.ts @@ -8,8 +8,8 @@ import path from 'node:path'; -// This file can use "import type" but otherwise should not reference any other modules, since it will -// be run from the "common/temp" directory +// This file runs from the "common/temp" directory, so besides "import type" it may only import local modules +// without runtime dependencies, which webpack bundles into the shim import type * as TSemver from 'semver'; import type { IPackageJson } from '@rushstack/node-core-library'; @@ -22,11 +22,23 @@ import type { IWorkspaceProjectInfo } from './IPnpmfile'; import type { IPnpmShrinkwrapYaml } from './PnpmShrinkwrapFile'; +import type { PnpmCatalogs } from './EffectivePnpmOptions'; +import { tryGetPnpmCatalogVersion, tryParsePnpmCatalogName } from './PnpmCatalogReference'; -let settings: ISubspacePnpmfileShimSettings; +let settings: ISubspacePnpmfileShimSettings | undefined; let userPnpmfile: IPnpmfile | undefined; let semver: typeof TSemver | undefined; +const INSTALLED_PACKAGE_DEPENDENCY_FIELDS: ReadonlyArray< + 'dependencies' | 'optionalDependencies' | 'peerDependencies' +> = ['dependencies', 'optionalDependencies', 'peerDependencies']; + +export function reset(): void { + settings = undefined; + userPnpmfile = undefined; + semver = undefined; +} + // Initialize all external aspects of the pnpmfile shim. When using the shim, settings // are always expected to be available. Init must be called before running any hook that // depends on a resource obtained from or related to the settings, and will require modules @@ -52,18 +64,87 @@ function init(context: IPnpmfileContext | any): IPnpmfileContext { // Reuse the already initialized settings context.splitWorkspacePnpmfileShimSettings = settings; } + const definedSettings: ISubspacePnpmfileShimSettings = settings!; // If a userPnpmfilePath is provided, we expect it to exist - if (!userPnpmfile && settings.userPnpmfilePath) { - userPnpmfile = require(settings.userPnpmfilePath); + if (!userPnpmfile && definedSettings.userPnpmfilePath) { + userPnpmfile = require(definedSettings.userPnpmfilePath); } // If a semverPath is provided, we expect it to exist - if (!semver && settings.semverPath) { - semver = require(settings.semverPath); + if (!semver && definedSettings.semverPath) { + semver = require(definedSettings.semverPath); } // Return the normalized context return context as IPnpmfileContext; } +// PNPM resolves "catalog:" only for its own workspace projects. Projects from other subspaces are installed as +// external "file:" packages, so resolve their references with the catalogs of their own subspace. +function resolveOtherSubspaceCatalogReferences(pkg: IPackageJson): void { + if (!settings) { + throw new Error(`splitWorkspaceGlobalPnpmfileShimSettings not initialized`); + } + + // Rewriting this subspace's own projects would change the specifiers of the lockfile importers + if (!Object.prototype.hasOwnProperty.call(settings.workspaceProjects, pkg.name)) { + return; + } + + const workspaceProject: IWorkspaceProjectInfo = settings.workspaceProjects[pkg.name]; + for (const dependencyField of INSTALLED_PACKAGE_DEPENDENCY_FIELDS) { + const dependencies: Record | undefined = pkg[dependencyField]; + if (!dependencies) { + continue; + } + + for (const [dependencyName, versionSpecifier] of Object.entries(dependencies)) { + const catalogName: string | undefined = tryParsePnpmCatalogName(versionSpecifier); + if (catalogName !== undefined) { + dependencies[dependencyName] = getCatalogVersion( + workspaceProject, + dependencyName, + versionSpecifier, + catalogName + ); + } + } + } +} + +function getCatalogVersion( + workspaceProject: IWorkspaceProjectInfo, + dependencyName: string, + versionSpecifier: string, + catalogName: string +): string { + const { packageName, subspaceName } = workspaceProject; + const subspaceCatalogs: PnpmCatalogs | undefined = + subspaceName !== undefined && + settings?.catalogsBySubspaceName && + Object.prototype.hasOwnProperty.call(settings.catalogsBySubspaceName, subspaceName) + ? settings.catalogsBySubspaceName[subspaceName] + : undefined; + + const catalogVersion: string | undefined = tryGetPnpmCatalogVersion( + subspaceCatalogs, + catalogName, + dependencyName + ); + if (catalogVersion !== undefined) { + return catalogVersion; + } + + const prefix: string = `The "${packageName}" project depends on "${dependencyName}@${versionSpecifier}", but`; + if (!subspaceCatalogs) { + throw new Error(`${prefix} no catalogs were found for the "${subspaceName}" subspace.`); + } else if (!Object.prototype.hasOwnProperty.call(subspaceCatalogs, catalogName)) { + throw new Error(`${prefix} the "${subspaceName}" subspace does not define a "${catalogName}" catalog.`); + } else { + throw new Error( + `${prefix} the "${catalogName}" catalog of the "${subspaceName}" subspace has no "${dependencyName}" entry.` + ); + } +} + // Rewrite rush project referenced in split workspace. // For example: "project-a": "workspace:*" --> "project-a": "link:../../project-a" function rewriteRushProjectVersions( @@ -160,6 +241,7 @@ export const hooks: IPnpmfileHooks = { // Rewrite workspace protocol to link protocol for non split workspace projects readPackage: (pkg: IPackageJson, context: IPnpmfileContext) => { context = init(context); + resolveOtherSubspaceCatalogReferences(pkg); rewriteRushProjectVersions(pkg.name, pkg.dependencies); rewriteRushProjectVersions(pkg.name, pkg.devDependencies); return userPnpmfile?.hooks?.readPackage ? userPnpmfile.hooks.readPackage(pkg, context) : pkg; diff --git a/libraries/rush-lib/src/logic/pnpm/SubspacePnpmfileConfiguration.ts b/libraries/rush-lib/src/logic/pnpm/SubspacePnpmfileConfiguration.ts index 17bf18a5a5..dc5c64fd8d 100644 --- a/libraries/rush-lib/src/logic/pnpm/SubspacePnpmfileConfiguration.ts +++ b/libraries/rush-lib/src/logic/pnpm/SubspacePnpmfileConfiguration.ts @@ -13,7 +13,7 @@ import type { PnpmPackageManager } from '../../api/packageManager/PnpmPackageMan import { RushConstants } from '../RushConstants'; import type { Subspace } from '../../api/Subspace'; import type { PnpmOptionsConfiguration } from './PnpmOptionsConfiguration'; -import { getEffectivePnpmOptions } from './EffectivePnpmOptions'; +import { getEffectivePnpmCatalogs, getEffectivePnpmOptions, type PnpmCatalogs } from './EffectivePnpmOptions'; /** * Loads PNPM's pnpmfile.js configuration, and invokes it to preprocess package.json files, @@ -76,7 +76,8 @@ export class SubspacePnpmfileConfiguration { packageName, projectRelativeFolder, packageVersion: packageJson.version, - injectedDependencies: Array.from(projectNameToInjectedDependenciesMap.get(packageName) || []) + injectedDependencies: Array.from(projectNameToInjectedDependenciesMap.get(packageName) || []), + subspaceName: project.subspace.subspaceName }; (subspace.contains(project) ? subspaceProjects : workspaceProjects)[packageName] = workspaceProjectInfo; } @@ -87,6 +88,15 @@ export class SubspacePnpmfileConfiguration { semverPath: Import.resolveModule({ modulePath: 'semver', baseFolderPath: __dirname }) }; + const catalogsBySubspaceName: Record | undefined = _getInjectedProjectCatalogs( + rushConfiguration, + subspace, + projectNameToInjectedDependenciesMap + ); + if (catalogsBySubspaceName) { + settings.catalogsBySubspaceName = catalogsBySubspaceName; + } + // common/config/subspaces//.pnpmfile.cjs const userPnpmfilePath: string = path.join( subspace.getVariantDependentSubspaceConfigFolderPath(variant), @@ -100,6 +110,35 @@ export class SubspacePnpmfileConfiguration { } } +function _getInjectedProjectCatalogs( + rushConfiguration: RushConfiguration, + subspace: Subspace, + projectNameToInjectedDependenciesMap: Map> +): Record | undefined { + let result: Record | undefined; + const visitedSubspaceNames: Set = new Set([subspace.subspaceName]); + for (const injectedDependencies of projectNameToInjectedDependenciesMap.values()) { + for (const injectedDependencyName of injectedDependencies) { + const injectedProject: RushConfigurationProject | undefined = + rushConfiguration.getProjectByName(injectedDependencyName); + if (!injectedProject || visitedSubspaceNames.has(injectedProject.subspace.subspaceName)) { + continue; + } + + visitedSubspaceNames.add(injectedProject.subspace.subspaceName); + const catalogs: PnpmCatalogs | undefined = getEffectivePnpmCatalogs( + injectedProject.subspace, + rushConfiguration + ); + if (catalogs) { + (result ??= {})[injectedProject.subspace.subspaceName] = catalogs; + } + } + } + + return result; +} + function _getProjectNameToInjectedDependenciesMap( rushConfiguration: RushConfiguration, subspace: Subspace diff --git a/libraries/rush-lib/src/logic/pnpm/test/SubspaceGlobalPnpmfileShim.test.ts b/libraries/rush-lib/src/logic/pnpm/test/SubspaceGlobalPnpmfileShim.test.ts new file mode 100644 index 0000000000..fa2110039c --- /dev/null +++ b/libraries/rush-lib/src/logic/pnpm/test/SubspaceGlobalPnpmfileShim.test.ts @@ -0,0 +1,91 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import type { IPackageJson } from '@rushstack/node-core-library'; + +import { RushConfiguration } from '../../../api/RushConfiguration'; +import type { IPnpmfileContext, ISubspacePnpmfileShimSettings } from '../IPnpmfile'; +import { hooks, reset } from '../SubspaceGlobalPnpmfileShim'; +import { SubspacePnpmfileConfiguration } from '../SubspacePnpmfileConfiguration'; + +// "catalog-consumer" (consumer-subspace) injects "catalog-provider" (default). Each subspace has its own catalogs. +const INJECTED_SUBSPACE_CATALOGS_RUSH_JSON: string = `${__dirname}/repoWithInjectedSubspaceCatalogs/rush.json`; + +describe('SubspaceGlobalPnpmfileShim', () => { + let rushConfiguration: RushConfiguration; + let settings: ISubspacePnpmfileShimSettings; + + function readPackage(pkg: IPackageJson): IPackageJson { + const context: IPnpmfileContext = { + log: () => {}, + splitWorkspacePnpmfileShimSettings: settings + } as IPnpmfileContext; + return hooks.readPackage!(pkg, context); + } + + function getPackageJson(packageName: string): IPackageJson { + return JSON.parse(JSON.stringify(rushConfiguration.getProjectByName(packageName)!.packageJson)); + } + + beforeEach(() => { + reset(); + rushConfiguration = RushConfiguration.loadFromConfigurationFile(INJECTED_SUBSPACE_CATALOGS_RUSH_JSON); + settings = SubspacePnpmfileConfiguration.getSubspacePnpmfileShimSettings( + rushConfiguration, + rushConfiguration.getSubspace('consumer-subspace'), + undefined + ); + }); + + afterEach(() => { + reset(); + }); + + describe('readPackage', () => { + it("resolves catalog references of an injected project with its own subspace's catalogs", () => { + const pkg: IPackageJson = readPackage(getPackageJson('catalog-provider')); + + // From the default subspace's catalogs, not consumer-subspace's + expect(pkg.dependencies).toEqual({ + 'left-pad': '^1.3.0', + lodash: '^4.17.21', + semver: '~7.5.4', + 'transitive-provider': 'file:../transitive-provider' + }); + expect(pkg.optionalDependencies).toEqual({ fsevents: '~2.3.2' }); + expect(pkg.peerDependencies).toEqual({ typescript: '~5.3.0' }); + // PNPM does not install the devDependencies of an external package + expect(pkg.devDependencies).toEqual({ jest: 'catalog:' }); + }); + + it("does not change the catalog references of the subspace's own projects", () => { + const pkg: IPackageJson = readPackage(getPackageJson('catalog-consumer')); + expect(pkg.dependencies).toEqual({ + lodash: 'catalog:', + 'catalog-provider': 'file:../catalog-provider' + }); + }); + + it('reports an error when the catalog does not have an entry for the dependency', () => { + const pkg: IPackageJson = getPackageJson('catalog-provider'); + pkg.dependencies!['missing-package'] = 'catalog:'; + expect(() => readPackage(pkg)).toThrow( + 'The "catalog-provider" project depends on "missing-package@catalog:", but the "default" catalog of the "default" subspace has no "missing-package" entry.' + ); + }); + + it('reports an error when the catalog does not exist', () => { + const pkg: IPackageJson = getPackageJson('catalog-provider'); + pkg.optionalDependencies!.fsevents = 'catalog:missing-catalog'; + expect(() => readPackage(pkg)).toThrow( + 'The "catalog-provider" project depends on "fsevents@catalog:missing-catalog", but the "default" subspace does not define a "missing-catalog" catalog.' + ); + }); + + it('reports an error when no catalogs are available for the subspace of the project', () => { + expect(() => readPackage(getPackageJson('unrelated'))).toThrow( + 'The "unrelated" project depends on "lodash@catalog:", but no catalogs were found for the "unrelated-subspace" subspace.' + ); + }); + }); +}); diff --git a/libraries/rush-lib/src/logic/pnpm/test/SubspacePnpmfileConfiguration.test.ts b/libraries/rush-lib/src/logic/pnpm/test/SubspacePnpmfileConfiguration.test.ts new file mode 100644 index 0000000000..7706c43c69 --- /dev/null +++ b/libraries/rush-lib/src/logic/pnpm/test/SubspacePnpmfileConfiguration.test.ts @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { RushConfiguration } from '../../../api/RushConfiguration'; +import type { ISubspacePnpmfileShimSettings } from '../IPnpmfile'; +import { SubspacePnpmfileConfiguration } from '../SubspacePnpmfileConfiguration'; + +// "catalog-consumer" (consumer-subspace) injects "catalog-provider" (default). Each subspace has its own catalogs. +const INJECTED_SUBSPACE_CATALOGS_RUSH_JSON: string = `${__dirname}/repoWithInjectedSubspaceCatalogs/rush.json`; + +describe(SubspacePnpmfileConfiguration.name, () => { + describe(SubspacePnpmfileConfiguration.getSubspacePnpmfileShimSettings.name, () => { + it('includes the catalogs of the subspaces of injected projects from other subspaces', () => { + const rushConfiguration: RushConfiguration = RushConfiguration.loadFromConfigurationFile( + INJECTED_SUBSPACE_CATALOGS_RUSH_JSON + ); + + const settings: ISubspacePnpmfileShimSettings = + SubspacePnpmfileConfiguration.getSubspacePnpmfileShimSettings( + rushConfiguration, + rushConfiguration.getSubspace('consumer-subspace'), + undefined + ); + + // Not consumer-subspace's own catalogs, nor those of unrelated-subspace, which has no injected projects + expect(settings.catalogsBySubspaceName).toEqual({ + default: rushConfiguration.pnpmOptions.globalCatalogs + }); + }); + + it('does not include catalogs for injected projects from the same subspace', () => { + const rushConfiguration: RushConfiguration = RushConfiguration.loadFromConfigurationFile( + `${__dirname}/../../../api/test/repoInjectedDeps/rush.json` + ); + + const settings: ISubspacePnpmfileShimSettings = + SubspacePnpmfileConfiguration.getSubspacePnpmfileShimSettings( + rushConfiguration, + rushConfiguration.defaultSubspace, + undefined + ); + expect(settings.subspaceProjects.consumer.injectedDependencies).toEqual(['provider']); + expect(settings).not.toHaveProperty('catalogsBySubspaceName'); + }); + }); +}); diff --git a/libraries/rush-lib/src/logic/pnpm/test/repoWithInjectedSubspaceCatalogs/catalog-consumer/package.json b/libraries/rush-lib/src/logic/pnpm/test/repoWithInjectedSubspaceCatalogs/catalog-consumer/package.json new file mode 100644 index 0000000000..b89876da93 --- /dev/null +++ b/libraries/rush-lib/src/logic/pnpm/test/repoWithInjectedSubspaceCatalogs/catalog-consumer/package.json @@ -0,0 +1,13 @@ +{ + "name": "catalog-consumer", + "version": "1.0.0", + "dependencies": { + "lodash": "catalog:", + "catalog-provider": "workspace:*" + }, + "dependenciesMeta": { + "catalog-provider": { + "injected": true + } + } +} diff --git a/libraries/rush-lib/src/logic/pnpm/test/repoWithInjectedSubspaceCatalogs/catalog-provider/package.json b/libraries/rush-lib/src/logic/pnpm/test/repoWithInjectedSubspaceCatalogs/catalog-provider/package.json new file mode 100644 index 0000000000..6e167affc6 --- /dev/null +++ b/libraries/rush-lib/src/logic/pnpm/test/repoWithInjectedSubspaceCatalogs/catalog-provider/package.json @@ -0,0 +1,19 @@ +{ + "name": "catalog-provider", + "version": "1.0.0", + "dependencies": { + "left-pad": "^1.3.0", + "lodash": "catalog:default", + "semver": "catalog:", + "transitive-provider": "workspace:*" + }, + "optionalDependencies": { + "fsevents": "catalog:" + }, + "peerDependencies": { + "typescript": "catalog:tools" + }, + "devDependencies": { + "jest": "catalog:" + } +} diff --git a/libraries/rush-lib/src/logic/pnpm/test/repoWithInjectedSubspaceCatalogs/common/config/rush/experiments.json b/libraries/rush-lib/src/logic/pnpm/test/repoWithInjectedSubspaceCatalogs/common/config/rush/experiments.json new file mode 100644 index 0000000000..a20c6d2438 --- /dev/null +++ b/libraries/rush-lib/src/logic/pnpm/test/repoWithInjectedSubspaceCatalogs/common/config/rush/experiments.json @@ -0,0 +1,3 @@ +{ + "exemptDecoupledDependenciesBetweenSubspaces": true +} diff --git a/libraries/rush-lib/src/logic/pnpm/test/repoWithInjectedSubspaceCatalogs/common/config/rush/pnpm-config.json b/libraries/rush-lib/src/logic/pnpm/test/repoWithInjectedSubspaceCatalogs/common/config/rush/pnpm-config.json new file mode 100644 index 0000000000..adc750f925 --- /dev/null +++ b/libraries/rush-lib/src/logic/pnpm/test/repoWithInjectedSubspaceCatalogs/common/config/rush/pnpm-config.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/pnpm-config.schema.json", + "useWorkspaces": true, + "globalCatalogs": { + "default": { + "fsevents": "~2.3.2", + "jest": "^29.0.0", + "lodash": "^4.17.21", + "semver": "~7.5.4" + }, + "tools": { + "typescript": "~5.3.0" + } + } +} diff --git a/libraries/rush-lib/src/logic/pnpm/test/repoWithInjectedSubspaceCatalogs/common/config/rush/subspaces.json b/libraries/rush-lib/src/logic/pnpm/test/repoWithInjectedSubspaceCatalogs/common/config/rush/subspaces.json new file mode 100644 index 0000000000..e95203d5d1 --- /dev/null +++ b/libraries/rush-lib/src/logic/pnpm/test/repoWithInjectedSubspaceCatalogs/common/config/rush/subspaces.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/subspaces.schema.json", + "subspacesEnabled": true, + "subspaceNames": ["consumer-subspace", "unrelated-subspace"] +} diff --git a/libraries/rush-lib/src/logic/pnpm/test/repoWithInjectedSubspaceCatalogs/common/config/rush/version-policies.json b/libraries/rush-lib/src/logic/pnpm/test/repoWithInjectedSubspaceCatalogs/common/config/rush/version-policies.json new file mode 100644 index 0000000000..fe51488c70 --- /dev/null +++ b/libraries/rush-lib/src/logic/pnpm/test/repoWithInjectedSubspaceCatalogs/common/config/rush/version-policies.json @@ -0,0 +1 @@ +[] diff --git a/libraries/rush-lib/src/logic/pnpm/test/repoWithInjectedSubspaceCatalogs/common/config/subspaces/consumer-subspace/common-versions.json b/libraries/rush-lib/src/logic/pnpm/test/repoWithInjectedSubspaceCatalogs/common/config/subspaces/consumer-subspace/common-versions.json new file mode 100644 index 0000000000..9280fe7b96 --- /dev/null +++ b/libraries/rush-lib/src/logic/pnpm/test/repoWithInjectedSubspaceCatalogs/common/config/subspaces/consumer-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/pnpm/test/repoWithInjectedSubspaceCatalogs/common/config/subspaces/consumer-subspace/pnpm-config.json b/libraries/rush-lib/src/logic/pnpm/test/repoWithInjectedSubspaceCatalogs/common/config/subspaces/consumer-subspace/pnpm-config.json new file mode 100644 index 0000000000..b6d29fc30a --- /dev/null +++ b/libraries/rush-lib/src/logic/pnpm/test/repoWithInjectedSubspaceCatalogs/common/config/subspaces/consumer-subspace/pnpm-config.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/pnpm-config.schema.json", + "useWorkspaces": true, + "globalCatalogs": { + "default": { + "lodash": "^3.10.1", + "semver": "^6.0.0" + } + } +} diff --git a/libraries/rush-lib/src/logic/pnpm/test/repoWithInjectedSubspaceCatalogs/common/config/subspaces/default/common-versions.json b/libraries/rush-lib/src/logic/pnpm/test/repoWithInjectedSubspaceCatalogs/common/config/subspaces/default/common-versions.json new file mode 100644 index 0000000000..9280fe7b96 --- /dev/null +++ b/libraries/rush-lib/src/logic/pnpm/test/repoWithInjectedSubspaceCatalogs/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/pnpm/test/repoWithInjectedSubspaceCatalogs/common/config/subspaces/unrelated-subspace/common-versions.json b/libraries/rush-lib/src/logic/pnpm/test/repoWithInjectedSubspaceCatalogs/common/config/subspaces/unrelated-subspace/common-versions.json new file mode 100644 index 0000000000..9280fe7b96 --- /dev/null +++ b/libraries/rush-lib/src/logic/pnpm/test/repoWithInjectedSubspaceCatalogs/common/config/subspaces/unrelated-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/pnpm/test/repoWithInjectedSubspaceCatalogs/common/config/subspaces/unrelated-subspace/pnpm-config.json b/libraries/rush-lib/src/logic/pnpm/test/repoWithInjectedSubspaceCatalogs/common/config/subspaces/unrelated-subspace/pnpm-config.json new file mode 100644 index 0000000000..07cec4c4da --- /dev/null +++ b/libraries/rush-lib/src/logic/pnpm/test/repoWithInjectedSubspaceCatalogs/common/config/subspaces/unrelated-subspace/pnpm-config.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/pnpm-config.schema.json", + "useWorkspaces": true, + "globalCatalogs": { + "default": { + "lodash": "^2.4.2" + } + } +} diff --git a/libraries/rush-lib/src/logic/pnpm/test/repoWithInjectedSubspaceCatalogs/rush.json b/libraries/rush-lib/src/logic/pnpm/test/repoWithInjectedSubspaceCatalogs/rush.json new file mode 100644 index 0000000000..a29b8cc2a9 --- /dev/null +++ b/libraries/rush-lib/src/logic/pnpm/test/repoWithInjectedSubspaceCatalogs/rush.json @@ -0,0 +1,25 @@ +{ + "rushVersion": "1.0.5", + "pnpmVersion": "9.15.0", + + "projects": [ + { + "packageName": "catalog-provider", + "projectFolder": "catalog-provider" + }, + { + "packageName": "transitive-provider", + "projectFolder": "transitive-provider" + }, + { + "packageName": "catalog-consumer", + "projectFolder": "catalog-consumer", + "subspaceName": "consumer-subspace" + }, + { + "packageName": "unrelated", + "projectFolder": "unrelated", + "subspaceName": "unrelated-subspace" + } + ] +} diff --git a/libraries/rush-lib/src/logic/pnpm/test/repoWithInjectedSubspaceCatalogs/transitive-provider/package.json b/libraries/rush-lib/src/logic/pnpm/test/repoWithInjectedSubspaceCatalogs/transitive-provider/package.json new file mode 100644 index 0000000000..241ac6813b --- /dev/null +++ b/libraries/rush-lib/src/logic/pnpm/test/repoWithInjectedSubspaceCatalogs/transitive-provider/package.json @@ -0,0 +1,7 @@ +{ + "name": "transitive-provider", + "version": "1.0.0", + "dependencies": { + "lodash": "catalog:" + } +} diff --git a/libraries/rush-lib/src/logic/pnpm/test/repoWithInjectedSubspaceCatalogs/unrelated/package.json b/libraries/rush-lib/src/logic/pnpm/test/repoWithInjectedSubspaceCatalogs/unrelated/package.json new file mode 100644 index 0000000000..2e8ae5e1a4 --- /dev/null +++ b/libraries/rush-lib/src/logic/pnpm/test/repoWithInjectedSubspaceCatalogs/unrelated/package.json @@ -0,0 +1,7 @@ +{ + "name": "unrelated", + "version": "1.0.0", + "dependencies": { + "lodash": "catalog:" + } +}