From 1ee390e629000effaa08bfbdf6ed703bda44b2a7 Mon Sep 17 00:00:00 2001 From: Aramis Sennyey Date: Tue, 17 Mar 2026 10:15:44 -0400 Subject: [PATCH 1/3] fix(rush): don't update injected state hash on devDep change Signed-off-by: Aramis Sennyey --- common/reviews/api/node-core-library.api.md | 4 +- libraries/rush-lib/src/api/Subspace.ts | 2 - .../rush-lib/src/api/test/Subspace.test.ts | 76 +++++++++++++++++++ .../repoInjectedDeps/consumer/package.json | 12 +++ .../repoInjectedDeps/provider/package.json | 10 +++ .../src/api/test/repoInjectedDeps/rush.json | 17 +++++ 6 files changed, 117 insertions(+), 4 deletions(-) create mode 100644 libraries/rush-lib/src/api/test/repoInjectedDeps/consumer/package.json create mode 100644 libraries/rush-lib/src/api/test/repoInjectedDeps/provider/package.json create mode 100644 libraries/rush-lib/src/api/test/repoInjectedDeps/rush.json diff --git a/common/reviews/api/node-core-library.api.md b/common/reviews/api/node-core-library.api.md index c5b2ebe4f9b..1d377c3b3f3 100644 --- a/common/reviews/api/node-core-library.api.md +++ b/common/reviews/api/node-core-library.api.md @@ -42,8 +42,8 @@ export class Async { static mapAsync(iterable: Iterable | AsyncIterable, callback: (entry: TEntry, arrayIndex: number) => Promise, options: IAsyncParallelismOptions & { weighted: true; }): Promise; - static runWithRetriesAsync(input: IRunWithRetriesOptions): Promise; - static runWithTimeoutAsync(input: IRunWithTimeoutOptions): Promise; + static runWithRetriesAsync({ action, maxRetries, retryDelayMs }: IRunWithRetriesOptions): Promise; + static runWithTimeoutAsync({ action, timeoutMs, timeoutMessage }: IRunWithTimeoutOptions): Promise; static sleepAsync(ms: number): Promise; static validateWeightedIterable(operation: IWeighted): void; } diff --git a/libraries/rush-lib/src/api/Subspace.ts b/libraries/rush-lib/src/api/Subspace.ts index 624b1a33bd6..1058788727e 100644 --- a/libraries/rush-lib/src/api/Subspace.ts +++ b/libraries/rush-lib/src/api/Subspace.ts @@ -478,7 +478,6 @@ export class Subspace { name, bin, dependencies, - devDependencies, peerDependencies, optionalDependencies, dependenciesMeta, @@ -500,7 +499,6 @@ export class Subspace { name, bin, dependencies, - devDependencies, peerDependencies, optionalDependencies, dependenciesMeta, diff --git a/libraries/rush-lib/src/api/test/Subspace.test.ts b/libraries/rush-lib/src/api/test/Subspace.test.ts index b7e01689519..7f1844be7ce 100644 --- a/libraries/rush-lib/src/api/test/Subspace.test.ts +++ b/libraries/rush-lib/src/api/test/Subspace.test.ts @@ -74,4 +74,80 @@ describe(Subspace.name, () => { expect(hashWithCatalogs).toBeDefined(); }); }); + + describe('getPackageJsonInjectedDependenciesHash', () => { + it('returns undefined when no injected dependencies exist', () => { + const rushJsonFilename: string = path.resolve(__dirname, 'repo', 'rush-pnpm.json'); + const rushConfiguration: RushConfiguration = + RushConfiguration.loadFromConfigurationFile(rushJsonFilename); + const defaultSubspace: Subspace = rushConfiguration.defaultSubspace; + + const hash: string | undefined = defaultSubspace.getPackageJsonInjectedDependenciesHash(); + expect(hash).toBeUndefined(); + }); + + it('computes a hash when injected dependencies exist', () => { + const rushJsonFilename: string = path.resolve(__dirname, 'repoInjectedDeps', 'rush.json'); + const rushConfiguration: RushConfiguration = + RushConfiguration.loadFromConfigurationFile(rushJsonFilename); + const defaultSubspace: Subspace = rushConfiguration.defaultSubspace; + + const hash: string | undefined = defaultSubspace.getPackageJsonInjectedDependenciesHash(); + expect(hash).toBeDefined(); + expect(typeof hash).toBe('string'); + expect(hash).toHaveLength(40); // SHA1 hash + }); + + it('does not change when devDependencies of the injected package change', () => { + const rushJsonFilename: string = path.resolve(__dirname, 'repoInjectedDeps', 'rush.json'); + const rushConfiguration: RushConfiguration = + RushConfiguration.loadFromConfigurationFile(rushJsonFilename); + const defaultSubspace: Subspace = rushConfiguration.defaultSubspace; + + const hashBefore: string | undefined = defaultSubspace.getPackageJsonInjectedDependenciesHash(); + + // Mutate devDependencies of the injected provider package + const providerProject = rushConfiguration.getProjectByName('provider')!; + const originalDevDeps = providerProject.packageJson.devDependencies; + providerProject.packageJson.devDependencies = { + ...originalDevDeps, + jest: '^29.0.0' + }; + + const hashAfter: string | undefined = defaultSubspace.getPackageJsonInjectedDependenciesHash(); + + expect(hashBefore).toBeDefined(); + expect(hashAfter).toBeDefined(); + expect(hashBefore).toBe(hashAfter); + + // Restore + providerProject.packageJson.devDependencies = originalDevDeps; + }); + + it('changes when production dependencies of the injected package change', () => { + const rushJsonFilename: string = path.resolve(__dirname, 'repoInjectedDeps', 'rush.json'); + const rushConfiguration: RushConfiguration = + RushConfiguration.loadFromConfigurationFile(rushJsonFilename); + const defaultSubspace: Subspace = rushConfiguration.defaultSubspace; + + const hashBefore: string | undefined = defaultSubspace.getPackageJsonInjectedDependenciesHash(); + + // Mutate dependencies of the injected provider package + const providerProject = rushConfiguration.getProjectByName('provider')!; + const originalDeps = providerProject.packageJson.dependencies; + providerProject.packageJson.dependencies = { + ...originalDeps, + axios: '^1.6.0' + }; + + const hashAfter: string | undefined = defaultSubspace.getPackageJsonInjectedDependenciesHash(); + + expect(hashBefore).toBeDefined(); + expect(hashAfter).toBeDefined(); + expect(hashBefore).not.toBe(hashAfter); + + // Restore + providerProject.packageJson.dependencies = originalDeps; + }); + }); }); diff --git a/libraries/rush-lib/src/api/test/repoInjectedDeps/consumer/package.json b/libraries/rush-lib/src/api/test/repoInjectedDeps/consumer/package.json new file mode 100644 index 00000000000..9fb49f623fd --- /dev/null +++ b/libraries/rush-lib/src/api/test/repoInjectedDeps/consumer/package.json @@ -0,0 +1,12 @@ +{ + "name": "consumer", + "version": "1.0.0", + "dependencies": { + "provider": "workspace:*" + }, + "dependenciesMeta": { + "provider": { + "injected": true + } + } +} diff --git a/libraries/rush-lib/src/api/test/repoInjectedDeps/provider/package.json b/libraries/rush-lib/src/api/test/repoInjectedDeps/provider/package.json new file mode 100644 index 00000000000..22b969cb820 --- /dev/null +++ b/libraries/rush-lib/src/api/test/repoInjectedDeps/provider/package.json @@ -0,0 +1,10 @@ +{ + "name": "provider", + "version": "1.0.0", + "dependencies": { + "lodash": "^4.17.21" + }, + "devDependencies": { + "typescript": "~5.3.0" + } +} diff --git a/libraries/rush-lib/src/api/test/repoInjectedDeps/rush.json b/libraries/rush-lib/src/api/test/repoInjectedDeps/rush.json new file mode 100644 index 00000000000..eebb706445e --- /dev/null +++ b/libraries/rush-lib/src/api/test/repoInjectedDeps/rush.json @@ -0,0 +1,17 @@ +{ + "pnpmVersion": "9.5.0", + "rushVersion": "5.46.1", + "projectFolderMinDepth": 1, + "projectFolderMaxDepth": 99, + + "projects": [ + { + "packageName": "consumer", + "projectFolder": "consumer" + }, + { + "packageName": "provider", + "projectFolder": "provider" + } + ] +} From a9b7b59f4cec24338c25f0340f6fde9d694fbecd Mon Sep 17 00:00:00 2001 From: Aramis Sennyey Date: Tue, 17 Mar 2026 10:17:35 -0400 Subject: [PATCH 2/3] Rush change --- ...nnyeya-fix-injected-dep-hash_2026-03-17-14-17.json | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 common/changes/@microsoft/rush/sennyeya-fix-injected-dep-hash_2026-03-17-14-17.json diff --git a/common/changes/@microsoft/rush/sennyeya-fix-injected-dep-hash_2026-03-17-14-17.json b/common/changes/@microsoft/rush/sennyeya-fix-injected-dep-hash_2026-03-17-14-17.json new file mode 100644 index 00000000000..494238eee0f --- /dev/null +++ b/common/changes/@microsoft/rush/sennyeya-fix-injected-dep-hash_2026-03-17-14-17.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "comment": "Fixes a bug where the injected dependency state hash updated on devDependency changes that wouldn't impact the lockfile.", + "type": "none", + "packageName": "@microsoft/rush" + } + ], + "packageName": "@microsoft/rush", + "email": "aramissennyeydd@users.noreply.github.com" +} \ No newline at end of file From ecf2bc26f97138accd3aa437ef3c3c3dcdf470c7 Mon Sep 17 00:00:00 2001 From: Aramis Sennyey Date: Tue, 17 Mar 2026 10:32:48 -0400 Subject: [PATCH 3/3] remove unchanged review Signed-off-by: Aramis Sennyey --- common/reviews/api/node-core-library.api.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/common/reviews/api/node-core-library.api.md b/common/reviews/api/node-core-library.api.md index 1d377c3b3f3..c5b2ebe4f9b 100644 --- a/common/reviews/api/node-core-library.api.md +++ b/common/reviews/api/node-core-library.api.md @@ -42,8 +42,8 @@ export class Async { static mapAsync(iterable: Iterable | AsyncIterable, callback: (entry: TEntry, arrayIndex: number) => Promise, options: IAsyncParallelismOptions & { weighted: true; }): Promise; - static runWithRetriesAsync({ action, maxRetries, retryDelayMs }: IRunWithRetriesOptions): Promise; - static runWithTimeoutAsync({ action, timeoutMs, timeoutMessage }: IRunWithTimeoutOptions): Promise; + static runWithRetriesAsync(input: IRunWithRetriesOptions): Promise; + static runWithTimeoutAsync(input: IRunWithTimeoutOptions): Promise; static sleepAsync(ms: number): Promise; static validateWeightedIterable(operation: IWeighted): void; }