diff --git a/apps/api-extractor/package.json b/apps/api-extractor/package.json index a0b2ccb54c4..599f7a9d8b1 100644 --- a/apps/api-extractor/package.json +++ b/apps/api-extractor/package.json @@ -70,7 +70,6 @@ "@rushstack/terminal": "workspace:*", "@rushstack/ts-command-line": "workspace:*", "diff": "~8.0.2", - "lodash": "~4.18.1", "minimatch": "10.2.3", "resolve": "~1.22.1", "semver": "~7.5.4", @@ -79,7 +78,6 @@ }, "devDependencies": { "@rushstack/heft": "1.2.12", - "@types/lodash": "4.17.23", "@types/resolve": "1.20.2", "@types/semver": "7.5.0", "decoupled-local-node-rig": "workspace:*", diff --git a/apps/api-extractor/src/api/ExtractorConfig.ts b/apps/api-extractor/src/api/ExtractorConfig.ts index 47f16e6c020..34a46b8fd46 100644 --- a/apps/api-extractor/src/api/ExtractorConfig.ts +++ b/apps/api-extractor/src/api/ExtractorConfig.ts @@ -4,7 +4,6 @@ import * as path from 'node:path'; import * as resolve from 'resolve'; -import lodash = require('lodash'); import { EnumMemberOrder, ReleaseTag } from '@microsoft/api-extractor-model'; import { TSDocConfiguration, TSDocTagDefinition } from '@microsoft/tsdoc'; @@ -14,6 +13,7 @@ import { JsonFile, JsonSchema, FileSystem, + Objects, PackageJsonLookup, type INodePackageJson, PackageName, @@ -629,10 +629,10 @@ export class ExtractorConfig { let currentConfigFilePath: string = path.resolve(jsonFilePath); let configObject: Partial = {}; - // Lodash merges array values by default, which is unintuitive for config files (and makes it impossible for derived configurations to overwrite arrays). - // For example, given a base config containing an array property with value ["foo", "bar"] and a derived config that specifies ["baz"] for that property, lodash will produce ["baz", "bar"], which is unintuitive. - // This customizer function ensures that arrays are always overwritten. - const mergeCustomizer: lodash.MergeWithCustomizer = (objValue, srcValue) => { + // Arrays are overwritten rather than merged, which is the intuitive behavior for config files. + // For example, given a base config containing an array property with value ["foo", "bar"] and a + // derived config that specifies ["baz"] for that property, the result is ["baz"] (not ["baz", "bar"]). + const mergeCustomizer: Objects.MergeWithCustomizer = (objValue, srcValue) => { if (Array.isArray(srcValue)) { return srcValue; } @@ -680,11 +680,11 @@ export class ExtractorConfig { } // This step has to be performed in advance, since the currentConfigFolderPath information will be lost - // after lodash.merge() is performed. + // after the merge is performed. ExtractorConfig._resolveConfigFileRelativePaths(baseConfig, currentConfigFolderPath); // Merge extractorConfig into baseConfig, mutating baseConfig - lodash.mergeWith(baseConfig, configObject, mergeCustomizer); + Objects.mergeWith(baseConfig, configObject, mergeCustomizer); configObject = baseConfig; currentConfigFilePath = extendsField; @@ -694,11 +694,11 @@ export class ExtractorConfig { } // Lastly, apply the defaults - configObject = lodash.mergeWith( - lodash.cloneDeep(ExtractorConfig._defaultConfig), + configObject = Objects.mergeWith( + structuredClone(ExtractorConfig._defaultConfig), configObject, mergeCustomizer - ); + ) as Partial; ExtractorConfig.jsonSchema.validateObject(configObject, jsonFilePath); diff --git a/apps/rush/src/test/sandbox/legacy-repo/project/package.json b/apps/rush/src/test/sandbox/legacy-repo/project/package.json index cef656a4e3a..4809d0e8e37 100644 --- a/apps/rush/src/test/sandbox/legacy-repo/project/package.json +++ b/apps/rush/src/test/sandbox/legacy-repo/project/package.json @@ -9,7 +9,7 @@ "author": "", "license": "ISC", "dependencies": { - "lodash": "^4.17.15", + "semver": "^7.5.4", "react": "^0.14.9" } } diff --git a/apps/rush/src/test/sandbox/repo/project/package.json b/apps/rush/src/test/sandbox/repo/project/package.json index cef656a4e3a..4809d0e8e37 100644 --- a/apps/rush/src/test/sandbox/repo/project/package.json +++ b/apps/rush/src/test/sandbox/repo/project/package.json @@ -9,7 +9,7 @@ "author": "", "license": "ISC", "dependencies": { - "lodash": "^4.17.15", + "semver": "^7.5.4", "react": "^0.14.9" } } diff --git a/build-tests/localization-plugin-test-02/package.json b/build-tests/localization-plugin-test-02/package.json index f518ceee9ab..5fdc6e99a5d 100644 --- a/build-tests/localization-plugin-test-02/package.json +++ b/build-tests/localization-plugin-test-02/package.json @@ -15,11 +15,9 @@ "@rushstack/set-webpack-public-path-plugin": "^4.1.16", "@rushstack/webpack4-localization-plugin": "workspace:*", "@rushstack/webpack4-module-minifier-plugin": "workspace:*", - "@types/lodash": "4.17.23", "@types/webpack-env": "1.18.8", "eslint": "~9.37.0", "html-webpack-plugin": "~4.5.2", - "lodash": "~4.18.1", "local-node-rig": "workspace:*", "webpack": "~4.47.0", "webpack-bundle-analyzer": "~4.5.0", diff --git a/build-tests/localization-plugin-test-02/src/chunks/chunkWithStrings.ts b/build-tests/localization-plugin-test-02/src/chunks/chunkWithStrings.ts index 1291aa604b4..4b343b62cf3 100644 --- a/build-tests/localization-plugin-test-02/src/chunks/chunkWithStrings.ts +++ b/build-tests/localization-plugin-test-02/src/chunks/chunkWithStrings.ts @@ -1,13 +1,18 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import * as lodash from 'lodash'; - import strings from './strings2.loc.json'; +function htmlEscape(str: string): string { + return str.replace( + /[&<>"']/g, + (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c] ?? c + ); +} + export class ChunkWithStringsClass { public doStuff(): void { // eslint-disable-next-line no-console - console.log(lodash.escape(strings.string1)); + console.log(htmlEscape(strings.string1)); } } diff --git a/build-tests/localization-plugin-test-02/src/chunks/chunkWithoutStrings.ts b/build-tests/localization-plugin-test-02/src/chunks/chunkWithoutStrings.ts index 585e18bd6f7..1f62f978da9 100644 --- a/build-tests/localization-plugin-test-02/src/chunks/chunkWithoutStrings.ts +++ b/build-tests/localization-plugin-test-02/src/chunks/chunkWithoutStrings.ts @@ -1,11 +1,16 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import * as lodash from 'lodash'; +function htmlEscape(str: string): string { + return str.replace( + /[&<>"']/g, + (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c] ?? c + ); +} export class ChunkWithoutStringsClass { public doStuff(): void { // eslint-disable-next-line no-console - console.log(lodash.escape('STATIC STRING')); + console.log(htmlEscape('STATIC STRING')); } } diff --git a/build-tests/rush-package-manager-integration-test/src/testNpmMode.ts b/build-tests/rush-package-manager-integration-test/src/testNpmMode.ts index 3392e90eac3..e10ffa64e3b 100644 --- a/build-tests/rush-package-manager-integration-test/src/testNpmMode.ts +++ b/build-tests/rush-package-manager-integration-test/src/testNpmMode.ts @@ -34,7 +34,7 @@ export async function testNpmModeAsync(terminal: ITerminal): Promise { testRepoPath, 'test-project-a', '1.0.0', - { lodash: '^4.17.21' }, + { semver: '^7.5.4' }, `node -e "const fs = require('fs'); fs.mkdirSync('lib', {recursive: true}); fs.writeFileSync('lib/index.js', 'module.exports = { greet: () => \\"Hello from A\\" };');"` ); @@ -65,7 +65,7 @@ export async function testNpmModeAsync(terminal: ITerminal): Promise { await helper.executeRushAsync(['install'], testRepoPath); // Verify node_modules were populated correctly - await helper.verifyDependenciesAsync(testRepoPath, 'test-project-a', ['lodash']); + await helper.verifyDependenciesAsync(testRepoPath, 'test-project-a', ['semver']); await helper.verifyDependenciesAsync(testRepoPath, 'test-project-b', ['test-project-a']); // Run rush build diff --git a/build-tests/rush-package-manager-integration-test/src/testYarnMode.ts b/build-tests/rush-package-manager-integration-test/src/testYarnMode.ts index 5a097c61688..a7d1b826bc2 100644 --- a/build-tests/rush-package-manager-integration-test/src/testYarnMode.ts +++ b/build-tests/rush-package-manager-integration-test/src/testYarnMode.ts @@ -34,7 +34,7 @@ export async function testYarnModeAsync(terminal: ITerminal): Promise { testRepoPath, 'test-project-a', '1.0.0', - { lodash: '^4.17.21' }, + { semver: '^7.5.4' }, `node -e "const fs = require('fs'); fs.mkdirSync('lib', {recursive: true}); fs.writeFileSync('lib/index.js', 'module.exports = { greet: () => \\"Hello from A\\" };');"` ); @@ -65,7 +65,7 @@ export async function testYarnModeAsync(terminal: ITerminal): Promise { await helper.executeRushAsync(['install'], testRepoPath); // Verify node_modules were populated correctly - await helper.verifyDependenciesAsync(testRepoPath, 'test-project-a', ['lodash']); + await helper.verifyDependenciesAsync(testRepoPath, 'test-project-a', ['semver']); await helper.verifyDependenciesAsync(testRepoPath, 'test-project-b', ['test-project-a']); // Run rush build diff --git a/common/changes/@microsoft/api-extractor/remove-lodash_2026-04-16-23-08.json b/common/changes/@microsoft/api-extractor/remove-lodash_2026-04-16-23-08.json new file mode 100644 index 00000000000..abf1be2583b --- /dev/null +++ b/common/changes/@microsoft/api-extractor/remove-lodash_2026-04-16-23-08.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@microsoft/api-extractor", + "comment": "Remove dependecy on `lodash`.", + "type": "patch" + } + ], + "packageName": "@microsoft/api-extractor" +} \ No newline at end of file diff --git a/common/changes/@microsoft/rush/remove-lodash_2026-04-16-23-10.json b/common/changes/@microsoft/rush/remove-lodash_2026-04-16-23-10.json new file mode 100644 index 00000000000..bd7ff97cb34 --- /dev/null +++ b/common/changes/@microsoft/rush/remove-lodash_2026-04-16-23-10.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@microsoft/rush", + "comment": "", + "type": "none" + } + ], + "packageName": "@microsoft/rush" +} \ No newline at end of file diff --git a/common/changes/@rushstack/heft-jest-plugin/remove-lodash_2026-04-16-23-08.json b/common/changes/@rushstack/heft-jest-plugin/remove-lodash_2026-04-16-23-08.json new file mode 100644 index 00000000000..4ccfc22ec9c --- /dev/null +++ b/common/changes/@rushstack/heft-jest-plugin/remove-lodash_2026-04-16-23-08.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@rushstack/heft-jest-plugin", + "comment": "Remove dependecy on `lodash`.", + "type": "patch" + } + ], + "packageName": "@rushstack/heft-jest-plugin" +} \ No newline at end of file diff --git a/common/changes/@rushstack/node-core-library/remove-lodash_2026-04-16-23-08.json b/common/changes/@rushstack/node-core-library/remove-lodash_2026-04-16-23-08.json new file mode 100644 index 00000000000..c0d85dc004e --- /dev/null +++ b/common/changes/@rushstack/node-core-library/remove-lodash_2026-04-16-23-08.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@rushstack/node-core-library", + "comment": "Add two new APIs: `Object.isRecord` asserts if an object is a `Record` object and `Object.mergeWith` is a customizable deep object merge.", + "type": "minor" + } + ], + "packageName": "@rushstack/node-core-library" +} \ No newline at end of file diff --git a/common/changes/@rushstack/npm-check-fork/remove-lodash_2026-04-16-23-08.json b/common/changes/@rushstack/npm-check-fork/remove-lodash_2026-04-16-23-08.json new file mode 100644 index 00000000000..4db2e06df5c --- /dev/null +++ b/common/changes/@rushstack/npm-check-fork/remove-lodash_2026-04-16-23-08.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@rushstack/npm-check-fork", + "comment": "Remove dependecy on `lodash`.", + "type": "patch" + } + ], + "packageName": "@rushstack/npm-check-fork" +} \ No newline at end of file diff --git a/common/config/rush/nonbrowser-approved-packages.json b/common/config/rush/nonbrowser-approved-packages.json index 4eb4bb89b8d..04639a9ec13 100644 --- a/common/config/rush/nonbrowser-approved-packages.json +++ b/common/config/rush/nonbrowser-approved-packages.json @@ -198,10 +198,6 @@ "name": "@rushstack/heft-dev-cert-plugin", "allowedCategories": [ "libraries", "tests" ] }, - { - "name": "@rushstack/heft-static-asset-typings-plugin", - "allowedCategories": [ "libraries", "tests" ] - }, { "name": "@rushstack/heft-isolated-typescript-transpile-plugin", "allowedCategories": [ "tests" ] @@ -242,6 +238,10 @@ "name": "@rushstack/heft-serverless-stack-plugin", "allowedCategories": [ "tests" ] }, + { + "name": "@rushstack/heft-static-asset-typings-plugin", + "allowedCategories": [ "libraries", "tests" ] + }, { "name": "@rushstack/heft-storybook-plugin", "allowedCategories": [ "tests" ] @@ -870,10 +870,6 @@ "name": "local-web-rig", "allowedCategories": [ "libraries", "tests", "vscode-extensions" ] }, - { - "name": "lodash", - "allowedCategories": [ "libraries", "tests" ] - }, { "name": "long", "allowedCategories": [ "tests" ] diff --git a/common/config/subspaces/build-tests-subspace/pnpm-lock.yaml b/common/config/subspaces/build-tests-subspace/pnpm-lock.yaml index b1a8a9c1246..b367cdb66c3 100644 --- a/common/config/subspaces/build-tests-subspace/pnpm-lock.yaml +++ b/common/config/subspaces/build-tests-subspace/pnpm-lock.yaml @@ -4274,7 +4274,6 @@ snapshots: '@rushstack/terminal': file:../../../libraries/terminal(@types/node@20.17.19) '@rushstack/ts-command-line': file:../../../libraries/ts-command-line(@types/node@20.17.19) diff: 8.0.4 - lodash: 4.18.1 minimatch: 10.2.3 resolve: 1.22.11 semver: 7.5.4 @@ -4765,7 +4764,6 @@ snapshots: jest-config: 30.3.0(@types/node@20.17.19) jest-resolve: 30.3.0 jest-snapshot: 30.3.0 - lodash: 4.18.1 optionalDependencies: '@types/jest': 30.0.0 jest-environment-node: 30.3.0 @@ -4858,7 +4856,6 @@ snapshots: dependencies: '@rushstack/node-core-library': file:../../../libraries/node-core-library(@types/node@20.17.19) giturl: 2.0.0 - lodash: 4.18.1 semver: 7.5.4 transitivePeerDependencies: - '@types/node' diff --git a/common/config/subspaces/build-tests-subspace/repo-state.json b/common/config/subspaces/build-tests-subspace/repo-state.json index e8818fc1bab..1efd94fc42b 100644 --- a/common/config/subspaces/build-tests-subspace/repo-state.json +++ b/common/config/subspaces/build-tests-subspace/repo-state.json @@ -1,6 +1,6 @@ // DO NOT MODIFY THIS FILE MANUALLY BUT DO COMMIT IT. It is generated and used by Rush. { - "pnpmShrinkwrapHash": "b1f311ce3022b7da1f29ddfd0c9f36fc9369eb6a", + "pnpmShrinkwrapHash": "bd8dac1e8e2962919f64c6784b22d17b4abf9621", "preferredVersionsHash": "550b4cee0bef4e97db6c6aad726df5149d20e7d9", - "packageJsonInjectedDependenciesHash": "716af2ed336a08a888140c784ff3d11e6aea0e4b" + "packageJsonInjectedDependenciesHash": "7bcd0c465594aefb6e51e674bb235fa385a071a7" } diff --git a/common/config/subspaces/default/pnpm-lock.yaml b/common/config/subspaces/default/pnpm-lock.yaml index 61c401c6fac..2d06acb0d81 100644 --- a/common/config/subspaces/default/pnpm-lock.yaml +++ b/common/config/subspaces/default/pnpm-lock.yaml @@ -86,9 +86,6 @@ importers: diff: specifier: ~8.0.2 version: 8.0.4 - lodash: - specifier: ~4.18.1 - version: 4.18.1 minimatch: specifier: 10.2.3 version: 10.2.3 @@ -108,9 +105,6 @@ importers: '@rushstack/heft': specifier: 1.2.12 version: 1.2.12(@types/node@22.9.3) - '@types/lodash': - specifier: 4.17.23 - version: 4.17.23 '@types/resolve': specifier: 1.20.2 version: 1.20.2 @@ -2589,9 +2583,6 @@ importers: '@rushstack/webpack4-module-minifier-plugin': specifier: workspace:* version: link:../../webpack/webpack4-module-minifier-plugin - '@types/lodash': - specifier: 4.17.23 - version: 4.17.23 '@types/webpack-env': specifier: 1.18.8 version: 1.18.8 @@ -2604,9 +2595,6 @@ importers: local-node-rig: specifier: workspace:* version: link:../../rigs/local-node-rig - lodash: - specifier: ~4.18.1 - version: 4.18.1 webpack: specifier: ~4.47.0 version: 4.47.0 @@ -3240,9 +3228,6 @@ importers: jest-snapshot: specifier: ~30.3.0 version: 30.3.0 - lodash: - specifier: ~4.18.1 - version: 4.18.1 devDependencies: '@jest/types': specifier: 30.3.0 @@ -3250,9 +3235,6 @@ importers: '@rushstack/heft': specifier: workspace:* version: link:../../apps/heft - '@types/lodash': - specifier: 4.17.23 - version: 4.17.23 '@types/node': specifier: 20.17.19 version: 20.17.19 @@ -3915,9 +3897,6 @@ importers: giturl: specifier: ^2.0.0 version: 2.0.0 - lodash: - specifier: ~4.18.1 - version: 4.18.1 semver: specifier: ~7.5.4 version: 7.5.4 @@ -3925,9 +3904,6 @@ importers: '@rushstack/heft': specifier: workspace:* version: link:../../apps/heft - '@types/lodash': - specifier: 4.17.23 - version: 4.17.23 '@types/semver': specifier: 7.5.0 version: 7.5.0 @@ -4625,7 +4601,7 @@ importers: version: 1.2.12(@types/node@20.17.19) '@rushstack/heft-node-rig': specifier: 2.11.34 - version: 2.11.34(@rushstack/heft@1.2.12(@types/node@20.17.19))(@types/node@20.17.19)(babel-plugin-macros@3.1.0)(esbuild-register@3.6.0(esbuild@0.28.0))(jest-environment-jsdom@30.3.0) + version: 2.11.34(@rushstack/heft@1.2.12(@types/node@20.17.19))(@types/node@20.17.19)(esbuild-register@3.6.0(esbuild@0.28.0))(jest-environment-jsdom@30.3.0) '@types/jest': specifier: 30.0.0 version: 30.0.0 @@ -24677,7 +24653,7 @@ snapshots: transitivePeerDependencies: - '@types/node' - '@rushstack/heft-jest-plugin@2.0.1(@rushstack/heft@1.2.12(@types/node@20.17.19))(@types/jest@30.0.0)(@types/node@20.17.19)(babel-plugin-macros@3.1.0)(esbuild-register@3.6.0(esbuild@0.28.0))(jest-environment-jsdom@30.3.0)(jest-environment-node@30.3.0)': + '@rushstack/heft-jest-plugin@2.0.1(@rushstack/heft@1.2.12(@types/node@20.17.19))(@types/jest@30.0.0)(@types/node@20.17.19)(esbuild-register@3.6.0(esbuild@0.28.0))(jest-environment-jsdom@30.3.0)(jest-environment-node@30.3.0)': dependencies: '@jest/core': 30.3.0(babel-plugin-macros@3.1.0)(esbuild-register@3.6.0(esbuild@0.28.0)) '@jest/reporters': 30.3.0 @@ -24711,13 +24687,13 @@ snapshots: transitivePeerDependencies: - '@types/node' - '@rushstack/heft-node-rig@2.11.34(@rushstack/heft@1.2.12(@types/node@20.17.19))(@types/node@20.17.19)(babel-plugin-macros@3.1.0)(esbuild-register@3.6.0(esbuild@0.28.0))(jest-environment-jsdom@30.3.0)': + '@rushstack/heft-node-rig@2.11.34(@rushstack/heft@1.2.12(@types/node@20.17.19))(@types/node@20.17.19)(esbuild-register@3.6.0(esbuild@0.28.0))(jest-environment-jsdom@30.3.0)': dependencies: '@microsoft/api-extractor': 7.58.2(@types/node@20.17.19) '@rushstack/eslint-config': 4.6.4(eslint@9.37.0)(typescript@5.8.2) '@rushstack/heft': 1.2.12(@types/node@20.17.19) '@rushstack/heft-api-extractor-plugin': 1.3.12(@rushstack/heft@1.2.12(@types/node@20.17.19))(@types/node@20.17.19) - '@rushstack/heft-jest-plugin': 2.0.1(@rushstack/heft@1.2.12(@types/node@20.17.19))(@types/jest@30.0.0)(@types/node@20.17.19)(babel-plugin-macros@3.1.0)(esbuild-register@3.6.0(esbuild@0.28.0))(jest-environment-jsdom@30.3.0)(jest-environment-node@30.3.0) + '@rushstack/heft-jest-plugin': 2.0.1(@rushstack/heft@1.2.12(@types/node@20.17.19))(@types/jest@30.0.0)(@types/node@20.17.19)(esbuild-register@3.6.0(esbuild@0.28.0))(jest-environment-jsdom@30.3.0)(jest-environment-node@30.3.0) '@rushstack/heft-lint-plugin': 1.2.12(@rushstack/heft@1.2.12(@types/node@20.17.19))(@types/node@20.17.19) '@rushstack/heft-typescript-plugin': 1.3.7(@rushstack/heft@1.2.12(@types/node@20.17.19))(@types/node@20.17.19) '@types/jest': 30.0.0 diff --git a/common/config/subspaces/default/repo-state.json b/common/config/subspaces/default/repo-state.json index 76b31c4cfb1..489ce5e9c35 100644 --- a/common/config/subspaces/default/repo-state.json +++ b/common/config/subspaces/default/repo-state.json @@ -1,5 +1,5 @@ // DO NOT MODIFY THIS FILE MANUALLY BUT DO COMMIT IT. It is generated and used by Rush. { - "pnpmShrinkwrapHash": "396f64d2259c918775b00218a15e187d9b14c3eb", + "pnpmShrinkwrapHash": "12c99da65e5e0e0e99e172ebdcee3f5a499a4ac1", "preferredVersionsHash": "029c99bd6e65c5e1f25e2848340509811ff9753c" } diff --git a/common/reviews/api/node-core-library.api.md b/common/reviews/api/node-core-library.api.md index 2a557ba9e75..7b787350514 100644 --- a/common/reviews/api/node-core-library.api.md +++ b/common/reviews/api/node-core-library.api.md @@ -679,6 +679,9 @@ export interface IRunWithTimeoutOptions { timeoutMs: number; } +// @public +function isRecord(value: unknown): value is Record; + // @public export interface IStringBuilder { append(text: string): void; @@ -806,6 +809,12 @@ export class MapExtensions { }; } +// @public +function mergeWith(target: TTarget, source: TSource, customizer?: MergeWithCustomizer): TTarget; + +// @public +type MergeWithCustomizer = (objValue: unknown, srcValue: unknown, key: string) => unknown; + // @public export class MinimumHeap { constructor(comparator: (a: T, b: T) => number); @@ -824,7 +833,10 @@ export enum NewlineKind { declare namespace Objects { export { - areDeepEqual + areDeepEqual, + isRecord, + MergeWithCustomizer, + mergeWith } } export { Objects } diff --git a/heft-plugins/heft-jest-plugin/package.json b/heft-plugins/heft-jest-plugin/package.json index 23597da8022..1411d05f849 100644 --- a/heft-plugins/heft-jest-plugin/package.json +++ b/heft-plugins/heft-jest-plugin/package.json @@ -41,13 +41,11 @@ "@rushstack/terminal": "workspace:*", "jest-config": "~30.3.0", "jest-resolve": "~30.3.0", - "jest-snapshot": "~30.3.0", - "lodash": "~4.18.1" + "jest-snapshot": "~30.3.0" }, "devDependencies": { "@jest/types": "30.3.0", "@rushstack/heft": "workspace:*", - "@types/lodash": "4.17.23", "@types/node": "20.17.19", "decoupled-local-node-rig": "workspace:*", "eslint": "~9.37.0", diff --git a/heft-plugins/heft-jest-plugin/src/JestPlugin.ts b/heft-plugins/heft-jest-plugin/src/JestPlugin.ts index fe8bf4e4065..d433cbceb4a 100644 --- a/heft-plugins/heft-jest-plugin/src/JestPlugin.ts +++ b/heft-plugins/heft-jest-plugin/src/JestPlugin.ts @@ -10,7 +10,6 @@ import * as path from 'node:path'; import type { AggregatedResult } from '@jest/reporters'; import type { Config } from '@jest/types'; import { resolveRunner, resolveSequencer, resolveTestEnvironment, resolveWatchPlugin } from 'jest-resolve'; -import { mergeWith, isObject } from 'lodash'; import type { HeftConfiguration, @@ -31,7 +30,7 @@ import { InheritanceType, PathResolutionMethod } from '@rushstack/heft-config-file'; -import { FileSystem, Path, Import, JsonFile, PackageName } from '@rushstack/node-core-library'; +import { FileSystem, Path, Import, JsonFile, Objects, PackageName } from '@rushstack/node-core-library'; import type { ITerminal } from '@rushstack/terminal'; import type { IHeftJestReporterOptions } from './HeftJestReporter'; @@ -702,14 +701,20 @@ export default class JestPlugin implements IHeftTaskPlugin { currentObject: T, parentObject: T ) => T = (currentObject: T, parentObject: T): T => { - return mergeWith(parentObject || {}, currentObject || {}, (value: T, source: T) => { + return Objects.mergeWith(parentObject || {}, currentObject || {}, (value, source) => { // Need to use a custom inheritance function instead of "InheritanceType.merge" since // some properties are allowed to have different types which may be incompatible with // merging. - if (!isObject(source)) { + if (source === null || typeof source !== 'object') { return source; } - return Array.isArray(value) ? [...value, ...(source as Array)] : { ...value, ...source }; + if (Array.isArray(source)) { + return Array.isArray(value) ? [...value, ...source] : source; + } + if (Objects.isRecord(source)) { + return { ...(Objects.isRecord(value) ? value : {}), ...source }; + } + return source; }) as T; }; diff --git a/libraries/node-core-library/src/objects/index.ts b/libraries/node-core-library/src/objects/index.ts index 77fdef00371..6dbd95ce592 100644 --- a/libraries/node-core-library/src/objects/index.ts +++ b/libraries/node-core-library/src/objects/index.ts @@ -2,3 +2,5 @@ // See LICENSE in the project root for license information. export { areDeepEqual } from './areDeepEqual'; +export { isRecord } from './isRecord'; +export { type MergeWithCustomizer, mergeWith } from './mergeWith'; diff --git a/libraries/node-core-library/src/objects/isRecord.ts b/libraries/node-core-library/src/objects/isRecord.ts new file mode 100644 index 00000000000..62f82851a86 --- /dev/null +++ b/libraries/node-core-library/src/objects/isRecord.ts @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +/** + * Returns `true` if `value` is a non-null, non-array plain object (i.e. assignable to + * `Record`), narrowing the type accordingly. + * + * @public + */ +export function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} diff --git a/libraries/node-core-library/src/objects/mergeWith.ts b/libraries/node-core-library/src/objects/mergeWith.ts new file mode 100644 index 00000000000..c31d8fcddee --- /dev/null +++ b/libraries/node-core-library/src/objects/mergeWith.ts @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { isRecord } from './isRecord'; + +/** + * Customizer function for use with `mergeWith`. + * Return `undefined` to fall back to the default deep-merge behavior for that property. + * @public + */ +export type MergeWithCustomizer = (objValue: unknown, srcValue: unknown, key: string) => unknown; + +/** + * Recursively merges own enumerable string-keyed properties of `source` into `target`, invoking + * `customizer` for each property. Mutates and returns `target`. + * + * @remarks + * For each property in `source`, `customizer` is called with `(targetValue, sourceValue, key)`. + * If the customizer returns a value other than `undefined`, that value is assigned directly. + * Otherwise the default behavior applies: plain objects are merged recursively; all other values + * (arrays, primitives, `null`) overwrite the corresponding target property. + * + * @public + */ +export function mergeWith( + target: TTarget, + source: TSource, + customizer?: MergeWithCustomizer +): TTarget { + const targetRecord: Record = target as unknown as Record; + const sourceRecord: Record = source as unknown as Record; + for (const [key, srcValue] of Object.entries(sourceRecord)) { + const objValue: unknown = targetRecord[key]; + const customized: unknown = customizer?.(objValue, srcValue, key); + if (customized !== undefined) { + targetRecord[key] = customized; + } else if (isRecord(srcValue) && isRecord(objValue)) { + mergeWith(objValue, srcValue, customizer); + } else { + targetRecord[key] = srcValue; + } + } + + return target; +} diff --git a/libraries/node-core-library/src/objects/test/mergeWith.test.ts b/libraries/node-core-library/src/objects/test/mergeWith.test.ts new file mode 100644 index 00000000000..333e2a46150 --- /dev/null +++ b/libraries/node-core-library/src/objects/test/mergeWith.test.ts @@ -0,0 +1,157 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { mergeWith } from '../mergeWith'; + +describe(mergeWith.name, () => { + describe('default behavior (no customizer)', () => { + it('returns the target object', () => { + const target: { a: number } = { a: 1 }; + const result: { a: number } = mergeWith(target, { a: 2 }); + expect(result).toBe(target); + }); + + it('copies source properties onto target', () => { + const target: Record = { a: 1 }; + mergeWith(target, { b: 2 }); + expect(target).toEqual({ a: 1, b: 2 }); + }); + + it('overwrites target primitive with source primitive', () => { + const target: Record = { a: 1 }; + mergeWith(target, { a: 2 }); + expect(target.a).toBe(2); + }); + + it('recursively merges nested plain objects', () => { + const target: Record = { nested: { a: 1, b: 2 } }; + mergeWith(target, { nested: { b: 99, c: 3 } }); + expect(target).toEqual({ nested: { a: 1, b: 99, c: 3 } }); + }); + + it('overwrites target array with source array (does not merge by index)', () => { + const target: Record = { arr: [1, 2, 3] }; + mergeWith(target, { arr: [10, 20] }); + expect(target.arr).toEqual([10, 20]); + }); + + it('overwrites nested array with source array', () => { + const target: Record = { nested: { arr: ['a', 'b', 'c'] } }; + mergeWith(target, { nested: { arr: ['x'] } }); + expect((target.nested as Record).arr).toEqual(['x']); + }); + + it('overwrites target object with source null', () => { + const target: Record = { a: { b: 1 } }; + mergeWith(target, { a: null }); + expect(target.a).toBeNull(); + }); + + it('overwrites target null with source object', () => { + const target: Record = { a: null }; + mergeWith(target, { a: { b: 1 } }); + expect(target.a).toEqual({ b: 1 }); + }); + + it('mutates the target object in place', () => { + const nested: Record = { x: 1 }; + const target: Record = { nested }; + mergeWith(target, { nested: { y: 2 } }); + expect(target.nested).toBe(nested); + expect(nested).toEqual({ x: 1, y: 2 }); + }); + + it('handles empty source', () => { + const target: Record = { a: 1 }; + mergeWith(target, {}); + expect(target).toEqual({ a: 1 }); + }); + + it('handles empty target', () => { + const target: Record = {}; + mergeWith(target, { a: 1 }); + expect(target).toEqual({ a: 1 }); + }); + }); + + describe('customizer behavior', () => { + it('uses customizer return value when it is not undefined', () => { + const target: Record = { a: 1 }; + mergeWith(target, { a: 2 }, () => 'custom'); + expect(target.a).toBe('custom'); + }); + + it('falls back to default merge when customizer returns undefined', () => { + const target: Record = { a: 1 }; + mergeWith(target, { a: 2 }, () => undefined); + expect(target.a).toBe(2); + }); + + it('customizer receives (targetValue, sourceValue, key)', () => { + const calls: [unknown, unknown, string][] = []; + const target: Record = { a: 1, b: 2 }; + mergeWith(target, { a: 10, b: 20 }, (obj, src, key) => { + calls.push([obj, src, key]); + return undefined; + }); + expect(calls).toEqual([ + [1, 10, 'a'], + [2, 20, 'b'] + ]); + }); + + it('customizer can overwrite arrays instead of concatenating', () => { + const target: Record = { arr: [1, 2] }; + mergeWith(target, { arr: [3, 4] }, (currentValue, srcValue) => { + void currentValue; + if (Array.isArray(srcValue)) return srcValue; + return undefined; + }); + expect(target.arr).toEqual([3, 4]); + }); + + it('customizer can concatenate arrays', () => { + const target: Record = { arr: [1, 2] }; + mergeWith(target, { arr: [3, 4] }, (objValue, srcValue) => { + if (Array.isArray(objValue) && Array.isArray(srcValue)) return [...objValue, ...srcValue]; + return undefined; + }); + expect(target.arr).toEqual([1, 2, 3, 4]); + }); + + it('customizer is not called for keys not in source', () => { + const calls: string[] = []; + const target: Record = { a: 1, b: 2 }; + mergeWith(target, { a: 10 }, (objValue, srcValue, key) => { + void objValue; + void srcValue; + calls.push(key); + return undefined; + }); + expect(calls).toEqual(['a']); + expect(target.b).toBe(2); + }); + + it('customizer receives undefined targetValue for keys only in source', () => { + let receivedObjValue: unknown = 'sentinel'; + const target: Record = {}; + mergeWith(target, { newKey: 42 }, (objValue) => { + receivedObjValue = objValue; + return undefined; + }); + expect(receivedObjValue).toBeUndefined(); + expect(target.newKey).toBe(42); + }); + + it('deep merge falls back to default when customizer returns undefined for nested objects', () => { + const target: Record = { nested: { a: 1, b: 2 } }; + mergeWith(target, { nested: { b: 99, c: 3 } }, (objValue, srcValue) => { + void objValue; + // Only intercept arrays, let objects deep-merge + if (Array.isArray(srcValue)) return srcValue; + return undefined; + }); + expect(target).toEqual({ nested: { a: 1, b: 99, c: 3 } }); + }); + }); +}); diff --git a/libraries/npm-check-fork/package.json b/libraries/npm-check-fork/package.json index 48e69033a08..2cfcf19ac55 100644 --- a/libraries/npm-check-fork/package.json +++ b/libraries/npm-check-fork/package.json @@ -42,13 +42,11 @@ }, "dependencies": { "giturl": "^2.0.0", - "lodash": "~4.18.1", "semver": "~7.5.4", "@rushstack/node-core-library": "workspace:*" }, "devDependencies": { "@rushstack/heft": "workspace:*", - "@types/lodash": "4.17.23", "@types/semver": "7.5.0", "local-node-rig": "workspace:*", "eslint": "~9.37.0" diff --git a/libraries/npm-check-fork/src/CreatePackageSummary.ts b/libraries/npm-check-fork/src/CreatePackageSummary.ts index 5bdbf601037..72e8f108753 100644 --- a/libraries/npm-check-fork/src/CreatePackageSummary.ts +++ b/libraries/npm-check-fork/src/CreatePackageSummary.ts @@ -1,7 +1,6 @@ import { existsSync } from 'node:fs'; import path from 'node:path'; -import _ from 'lodash'; import semver from 'semver'; import type { INpmCheckState, INpmCheckPackageJson } from './interfaces/INpmCheck.ts'; @@ -83,7 +82,8 @@ export default async function createPackageSummary( packageJson: packageJsonVersion ?? '', // meta - devDependency: _.has(cwdPackageJson?.devDependencies, moduleName), + // TODO: Replace with Object.hasOwn() when the TypeScript target library is upgraded to es2022+ + devDependency: Object.prototype.hasOwnProperty.call(cwdPackageJson?.devDependencies, moduleName), mismatch: packageJsonVersion !== undefined && versionToUse !== null && diff --git a/libraries/npm-check-fork/src/GetLatestFromRegistry.ts b/libraries/npm-check-fork/src/GetLatestFromRegistry.ts index 9bdd76087d7..43b0725e825 100644 --- a/libraries/npm-check-fork/src/GetLatestFromRegistry.ts +++ b/libraries/npm-check-fork/src/GetLatestFromRegistry.ts @@ -3,7 +3,6 @@ import os from 'node:os'; -import _ from 'lodash'; import semver from 'semver'; import { Async } from '@rushstack/node-core-library'; @@ -47,11 +46,9 @@ export default async function getNpmInfo(packageName: string): Promise semver.gt(CRAZY_HIGH_SEMVER, version)) - .sort(semver.compare) - .valueOf(); + const sortedVersions: string[] = Object.keys(rawData.versions) + .filter((version: string) => semver.gt(CRAZY_HIGH_SEMVER, version)) + .sort(semver.compare); const latest: string = rawData['dist-tags'].latest; const next: string = rawData['dist-tags'].next; diff --git a/libraries/npm-check-fork/src/NpmCheck.ts b/libraries/npm-check-fork/src/NpmCheck.ts index aea2339a4c9..2a833ba75de 100644 --- a/libraries/npm-check-fork/src/NpmCheck.ts +++ b/libraries/npm-check-fork/src/NpmCheck.ts @@ -1,5 +1,3 @@ -import _ from 'lodash'; - import type { INpmCheckPackageJson, INpmCheckState } from './interfaces/INpmCheck.ts'; import type { INpmCheckPackageSummary } from './interfaces/INpmCheckPackageSummary'; import createPackageSummary from './CreatePackageSummary'; @@ -30,5 +28,5 @@ function getDependencies(pkg: INpmCheckPackageJson | undefined): Record { - const state: INpmCheckState = _.extend(DefaultNpmCheckOptions, initialOptions); + const state: INpmCheckState = Object.assign(DefaultNpmCheckOptions, initialOptions ?? {}); if (state.cwd) { const cwd: string = path.resolve(state.cwd); diff --git a/libraries/npm-check-fork/src/ReadPackageJson.ts b/libraries/npm-check-fork/src/ReadPackageJson.ts index b8166d486ca..68363c579df 100644 --- a/libraries/npm-check-fork/src/ReadPackageJson.ts +++ b/libraries/npm-check-fork/src/ReadPackageJson.ts @@ -1,5 +1,3 @@ -import _ from 'lodash'; - import type { INpmCheckPackageJson } from './interfaces/INpmCheck.ts'; export default function readPackageJson(filename: string): INpmCheckPackageJson { @@ -14,5 +12,5 @@ export default function readPackageJson(filename: string): INpmCheckPackageJson error = new Error(`A package.json was found at ${filename}, but it is not valid.`); } } - return _.extend({ devDependencies: {}, dependencies: {}, error: error }, pkg); + return { devDependencies: {}, dependencies: {}, error, ...pkg } as INpmCheckPackageJson; } diff --git a/libraries/rush-lib/src/api/test/repo/project1/package.json b/libraries/rush-lib/src/api/test/repo/project1/package.json index cef656a4e3a..4809d0e8e37 100644 --- a/libraries/rush-lib/src/api/test/repo/project1/package.json +++ b/libraries/rush-lib/src/api/test/repo/project1/package.json @@ -9,7 +9,7 @@ "author": "", "license": "ISC", "dependencies": { - "lodash": "^4.17.15", + "semver": "^7.5.4", "react": "^0.14.9" } } diff --git a/libraries/rush-lib/src/api/test/repo/project2/package.json b/libraries/rush-lib/src/api/test/repo/project2/package.json index 939deab00d7..2fa2757c5c0 100644 --- a/libraries/rush-lib/src/api/test/repo/project2/package.json +++ b/libraries/rush-lib/src/api/test/repo/project2/package.json @@ -9,7 +9,7 @@ "author": "", "license": "ISC", "dependencies": { - "lodash": "^4.17.15", + "semver": "^7.5.4", "react": "^15.5.4" } } diff --git a/libraries/rush-lib/src/api/test/repoCatalogs/common/config/rush/pnpm-config.json b/libraries/rush-lib/src/api/test/repoCatalogs/common/config/rush/pnpm-config.json index 797e41e0c2c..dc7c0f32d81 100644 --- a/libraries/rush-lib/src/api/test/repoCatalogs/common/config/rush/pnpm-config.json +++ b/libraries/rush-lib/src/api/test/repoCatalogs/common/config/rush/pnpm-config.json @@ -7,7 +7,7 @@ "typescript": "~5.3.0" }, "internal": { - "lodash": "^4.17.21", + "semver": "^7.5.4", "axios": "^1.6.0" } } diff --git a/libraries/rush-lib/src/logic/test/ProjectChangeAnalyzer.test.ts b/libraries/rush-lib/src/logic/test/ProjectChangeAnalyzer.test.ts index b03269fc014..cb183942e5f 100644 --- a/libraries/rush-lib/src/logic/test/ProjectChangeAnalyzer.test.ts +++ b/libraries/rush-lib/src/logic/test/ProjectChangeAnalyzer.test.ts @@ -613,7 +613,7 @@ describe(ProjectChangeAnalyzer.name, () => { mockPnpmConfigChanged(); // react version bumped in the default catalog mockOldCatalogs({ - default: { react: '^17.0.0', lodash: '^4.17.21' }, + default: { react: '^17.0.0', semver: '^7.5.4' }, tools: { typescript: '~5.3.0', eslint: '^8.50.0' } }); @@ -627,9 +627,9 @@ describe(ProjectChangeAnalyzer.name, () => { terminal }); - // 'a' uses "catalog:" (default) for react (changed) and lodash + // 'a' uses "catalog:" (default) for react (changed) and semver expect(changedProjects.has(rushConfiguration.getProjectByName('a')!)).toBe(true); - // 'd' uses "catalog:" (default) for lodash only (unchanged) — should NOT be detected + // 'd' uses "catalog:" (default) for semver 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); @@ -644,7 +644,7 @@ describe(ProjectChangeAnalyzer.name, () => { mockPnpmConfigChanged(); // typescript version bumped in the tools catalog mockOldCatalogs({ - default: { react: '^18.0.0', lodash: '^4.17.21' }, + default: { react: '^18.0.0', semver: '^7.5.4' }, tools: { typescript: '~5.2.0', eslint: '^8.50.0' } }); @@ -664,7 +664,7 @@ describe(ProjectChangeAnalyzer.name, () => { 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 + // 'd' uses "catalog:" (default) for semver — 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); @@ -675,7 +675,7 @@ describe(ProjectChangeAnalyzer.name, () => { mockPnpmConfigChanged(); // Old catalogs are identical to current mockOldCatalogs({ - default: { react: '^18.0.0', lodash: '^4.17.21' }, + default: { react: '^18.0.0', semver: '^7.5.4' }, tools: { typescript: '~5.3.0', eslint: '^8.50.0' } }); @@ -695,9 +695,9 @@ describe(ProjectChangeAnalyzer.name, () => { 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 + // Only react changed in the default catalog; semver is unchanged mockOldCatalogs({ - default: { react: '^17.0.0', lodash: '^4.17.21' }, + default: { react: '^17.0.0', semver: '^7.5.4' }, tools: { typescript: '~5.3.0', eslint: '^8.50.0' } }); @@ -711,9 +711,9 @@ describe(ProjectChangeAnalyzer.name, () => { terminal }); - // 'a' uses "catalog:" for react (changed) and lodash (unchanged) — should be detected + // 'a' uses "catalog:" for react (changed) and semver (unchanged) — should be detected expect(changedProjects.has(rushConfiguration.getProjectByName('a')!)).toBe(true); - // 'd' uses "catalog:" for lodash only (unchanged) — should NOT be detected + // 'd' uses "catalog:" for semver 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); @@ -726,9 +726,9 @@ describe(ProjectChangeAnalyzer.name, () => { 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 + // Only semver changed in the default catalog; react is unchanged mockOldCatalogs({ - default: { react: '^18.0.0', lodash: '^4.16.0' }, + default: { react: '^18.0.0', semver: '^7.4.0' }, tools: { typescript: '~5.3.0', eslint: '^8.50.0' } }); @@ -742,9 +742,9 @@ describe(ProjectChangeAnalyzer.name, () => { terminal }); - // 'a' uses "catalog:" for react (unchanged) and lodash (changed) — should be detected + // 'a' uses "catalog:" for react (unchanged) and semver (changed) — should be detected expect(changedProjects.has(rushConfiguration.getProjectByName('a')!)).toBe(true); - // 'd' uses "catalog:" for lodash (changed) — should be detected + // 'd' uses "catalog:" for semver (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); @@ -759,7 +759,7 @@ describe(ProjectChangeAnalyzer.name, () => { mockPnpmConfigChanged(); // Only typescript changed in the tools catalog; eslint is unchanged mockOldCatalogs({ - default: { react: '^18.0.0', lodash: '^4.17.21' }, + default: { react: '^18.0.0', semver: '^7.5.4' }, tools: { typescript: '~5.2.0', eslint: '^8.50.0' } }); @@ -790,7 +790,7 @@ describe(ProjectChangeAnalyzer.name, () => { mockPnpmConfigChanged(); // Only eslint changed in the tools catalog; typescript is unchanged mockOldCatalogs({ - default: { react: '^18.0.0', lodash: '^4.17.21' }, + default: { react: '^18.0.0', semver: '^7.5.4' }, tools: { typescript: '~5.3.0', eslint: '^8.40.0' } }); @@ -834,7 +834,7 @@ describe(ProjectChangeAnalyzer.name, () => { terminal }); - // All catalog-using projects detected: 'a' (default: react, lodash), 'b' (tools: typescript), 'd' (default: lodash), 'e' (tools: eslint) + // All catalog-using projects detected: 'a' (default: react, semver), 'b' (tools: typescript), 'd' (default: semver), '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); diff --git a/libraries/rush-lib/src/logic/test/pnpmConfig/common/config/rush/pnpm-config.json b/libraries/rush-lib/src/logic/test/pnpmConfig/common/config/rush/pnpm-config.json index 2a51adf500b..6151e8b1ba5 100644 --- a/libraries/rush-lib/src/logic/test/pnpmConfig/common/config/rush/pnpm-config.json +++ b/libraries/rush-lib/src/logic/test/pnpmConfig/common/config/rush/pnpm-config.json @@ -17,7 +17,7 @@ "globalCatalogs": { "default": { "react": "^18.0.0", - "lodash": "^4.17.21" + "semver": "^7.5.4" }, "test": { "jest": "^29.0.0" diff --git a/libraries/rush-lib/src/logic/test/repoWithCatalogs/a/package.json b/libraries/rush-lib/src/logic/test/repoWithCatalogs/a/package.json index 26e2a7fe95d..893aac61ae0 100644 --- a/libraries/rush-lib/src/logic/test/repoWithCatalogs/a/package.json +++ b/libraries/rush-lib/src/logic/test/repoWithCatalogs/a/package.json @@ -4,6 +4,6 @@ "description": "Project A uses default catalog", "dependencies": { "react": "catalog:", - "lodash": "catalog:" + "semver": "catalog:" } } 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 index 32448ef0221..3402e25453a 100644 --- 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 @@ -3,7 +3,7 @@ "globalCatalogs": { "default": { "react": "^18.0.0", - "lodash": "^4.17.21" + "semver": "^7.5.4" }, "tools": { "typescript": "~5.3.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 index 31e197b4f81..3a9c1e1f890 100644 --- a/libraries/rush-lib/src/logic/test/repoWithCatalogs/d/package.json +++ b/libraries/rush-lib/src/logic/test/repoWithCatalogs/d/package.json @@ -1,8 +1,8 @@ { "name": "d", "version": "1.0.0", - "description": "Project D uses only lodash from default catalog", + "description": "Project D uses only semver from default catalog", "dependencies": { - "lodash": "catalog:" + "semver": "catalog:" } }