diff --git a/src/lib/logging.ts b/src/lib/logging.ts index 80d7765f..15359c3f 100755 --- a/src/lib/logging.ts +++ b/src/lib/logging.ts @@ -84,18 +84,18 @@ export function printSimpleJoinedString(object: any, join: string) { ) } -/** Prints the options object sorted. */ -export function printOptionsSorted(options: Options, loglevel: LogLevel) { +/** Prints an object sorted by key. */ +export function printSorted(options: Options, obj: T, loglevel: LogLevel) { // eslint-disable-next-line fp/no-mutating-methods - const sortedKeys = Object.keys(options).sort() as (keyof Options)[] - const optionsString = transform( + const sortedKeys = Object.keys(obj).sort() as (keyof T)[] + const objSorted = transform( sortedKeys, (accum, key) => { - accum[key] = options[key] + accum[key] = obj[key] }, - {}, + {} as T, ) - print(options, optionsString, loglevel) + print(options, objSorted, loglevel) } /** Create a table with the appropriate columns and alignment to render dependency upgrades. */ diff --git a/src/lib/runGlobal.ts b/src/lib/runGlobal.ts index 4962a8b4..47011f8c 100644 --- a/src/lib/runGlobal.ts +++ b/src/lib/runGlobal.ts @@ -1,5 +1,5 @@ import pick from 'lodash/pick' -import { print, printJson, printOptionsSorted, printUpgrades } from '../lib/logging' +import { print, printJson, printSorted, printUpgrades } from '../lib/logging' import { Index } from '../types/IndexType' import { Options } from '../types/Options' import chalk from './chalk' @@ -10,7 +10,7 @@ import upgradePackageDefinitions from './upgradePackageDefinitions' /** Checks global dependencies for upgrades. */ async function runGlobal(options: Options): Promise | void> { print(options, '\nOptions:', 'verbose') - printOptionsSorted(options, 'verbose') + printSorted(options, options, 'verbose') print(options, '\nGetting installed packages', 'verbose') const globalPackages = await getInstalledPackages( diff --git a/src/lib/runLocal.ts b/src/lib/runLocal.ts index 23d3e919..6612197b 100644 --- a/src/lib/runLocal.ts +++ b/src/lib/runLocal.ts @@ -17,7 +17,7 @@ import getIgnoredUpgrades from './getIgnoredUpgrades' import getPackageManager from './getPackageManager' import getPeerDependencies from './getPeerDependencies' import keyValueBy from './keyValueBy' -import { print, printIgnoredUpdates, printJson, printOptionsSorted, printUpgrades, toDependencyTable } from './logging' +import { print, printIgnoredUpdates, printJson, printSorted, printUpgrades, toDependencyTable } from './logging' import programError from './programError' import resolveDepSections from './resolveDepSections' import upgradePackageData from './upgradePackageData' @@ -152,7 +152,7 @@ async function runLocal( pkgFile?: Maybe, ): Promise> { print(options, '\nOptions:', 'verbose') - printOptionsSorted(options, 'verbose') + printSorted(options, options, 'verbose') let pkg: PackageFile diff --git a/src/package-managers/npm.ts b/src/package-managers/npm.ts index 3097cb44..0e66b508 100644 --- a/src/package-managers/npm.ts +++ b/src/package-managers/npm.ts @@ -17,7 +17,7 @@ import untildify from 'untildify' import filterObject from '../lib/filterObject' import { keyValueBy } from '../lib/keyValueBy' import libnpmconfig from '../lib/libnpmconfig' -import { print } from '../lib/logging' +import { print, printSorted } from '../lib/logging' import * as versionUtil from '../lib/version-util' import { GetVersion } from '../types/GetVersion' import { Index } from '../types/IndexType' @@ -337,6 +337,74 @@ export const mockViewMany = ) } +/** Merges the workspace, global, user, local, project, and cwd npm configs (in that order). */ +// Note that this is memoized on configs and options, but not on package name. This avoids duplicate messages when log level is verbose. findNpmConfig is memoized on config path, so it is not expensive to call multiple times. +const mergeNpmConfigs = memoize( + ( + { + npmConfigLocal, + npmConfigUser, + npmConfigWorkspaceProject, + }: { + npmConfigLocal?: NpmConfig + npmConfigUser?: NpmConfig + npmConfigWorkspaceProject?: NpmConfig + }, + options: Options, + ) => { + // merge project npm config with base config + const npmConfigProjectPath = options.packageFile ? path.join(options.packageFile, '../.npmrc') : null + const npmConfigProject = options.packageFile ? findNpmConfig(npmConfigProjectPath || undefined) : null + const npmConfigCWDPath = options.cwd ? path.join(options.cwd, '.npmrc') : null + const npmConfigCWD = options.cwd ? findNpmConfig(npmConfigCWDPath!) : null + + if (npmConfigWorkspaceProject && Object.keys(npmConfigWorkspaceProject).length > 0) { + print(options, `\nnpm config (workspace project):`, 'verbose') + printSorted(options, omit(npmConfigWorkspaceProject, 'cache'), 'verbose') + } + + if (npmConfigUser && Object.keys(npmConfigUser).length > 0) { + print(options, `\nnpm config (user):`, 'verbose') + printSorted(options, omit(npmConfigUser, 'cache'), 'verbose') + } + + if (npmConfigLocal && Object.keys(npmConfigLocal).length > 0) { + print(options, `\nnpm config (local override):`, 'verbose') + printSorted(options, omit(npmConfigLocal, 'cache'), 'verbose') + } + + if (npmConfigProject && Object.keys(npmConfigProject).length > 0) { + print(options, `\nnpm config (project: ${npmConfigProjectPath}):`, 'verbose') + printSorted(options, omit(npmConfigProject, 'cache'), 'verbose') + } + + if (npmConfigCWD && Object.keys(npmConfigCWD).length > 0) { + print(options, `\nnpm config (cwd: ${npmConfigCWDPath}):`, 'verbose') + // omit cache since it is added to every config + printSorted(options, omit(npmConfigCWD, 'cache'), 'verbose') + } + + const npmConfigMerged = { + ...npmConfigWorkspaceProject, + ...npmConfigUser, + ...npmConfigLocal, + ...npmConfigProject, + ...npmConfigCWD, + ...(options.registry ? { registry: options.registry, silent: true } : null), + ...(options.timeout ? { timeout: options.timeout } : null), + } + + const isMerged = npmConfigWorkspaceProject || npmConfigLocal || npmConfigProject || npmConfigCWD + if (isMerged) { + print(options, `\nmerged npm config:`, 'verbose') + // omit cache since it is added to every config + printSorted(options, omit(npmConfigMerged, 'cache'), 'verbose') + } + + return npmConfigMerged + }, +) + /** * Returns an object of specified values retrieved by npm view. * @@ -366,53 +434,14 @@ async function viewMany( const fieldsExtended = options.format?.includes('time') ? [...fields, 'time'] : fields - // merge project npm config with base config - const npmConfigProjectPath = options.packageFile ? path.join(options.packageFile, '../.npmrc') : null - const npmConfigProject = options.packageFile ? findNpmConfig(npmConfigProjectPath || undefined) : null - const npmConfigCWDPath = options.cwd ? path.join(options.cwd, '.npmrc') : null - const npmConfigCWD = options.cwd ? findNpmConfig(npmConfigCWDPath!) : null - - if (npmConfigWorkspaceProject && Object.keys(npmConfigWorkspaceProject).length > 0) { - print(options, `\nnpm config (workspace project):`, 'verbose') - print(options, omit(npmConfigWorkspaceProject, 'cache'), 'verbose') - } - - if (npmConfig && Object.keys(npmConfig).length > 0) { - print(options, `\nnpm config (local):`, 'verbose') - print(options, omit(npmConfig, 'cache'), 'verbose') - } - - if (npmConfigLocal && Object.keys(npmConfigLocal).length > 0) { - print(options, `\nnpm config (local override):`, 'verbose') - print(options, omit(npmConfigLocal, 'cache'), 'verbose') - } - - if (npmConfigProject && Object.keys(npmConfigProject).length > 0) { - print(options, `\nnpm config (project: ${npmConfigProjectPath}):`, 'verbose') - print(options, omit(npmConfigProject, 'cache'), 'verbose') - } - - if (npmConfigCWD && Object.keys(npmConfigCWD).length > 0) { - print(options, `\nnpm config (cwd: ${npmConfigCWDPath}):`, 'verbose') - // omit cache since it is added to every config - print(options, omit(npmConfigCWD, 'cache'), 'verbose') - } - - const npmConfigMerged = { - ...npmConfigWorkspaceProject, - ...npmConfig, - ...npmConfigLocal, - ...npmConfigProject, - ...npmConfigCWD, - ...(options.registry ? { registry: options.registry, silent: true } : null), - ...(options.timeout ? { timeout: options.timeout } : null), - fullMetadata: fieldsExtended.includes('time'), - } - - const isMerged = npmConfigWorkspaceProject || npmConfigLocal || npmConfigProject || npmConfigCWD - print(options, `\nUsing${isMerged ? ' merged' : ''} npm config:`, 'verbose') - // omit cache since it is added to every config - print(options, omit(npmConfigMerged, 'cache'), 'verbose') + const npmConfigMerged = mergeNpmConfigs( + { + npmConfigUser: { ...npmConfig, fullMetadata: fieldsExtended.includes('time') }, + npmConfigLocal, + npmConfigWorkspaceProject, + }, + options, + ) let result: any try { diff --git a/test/workspaces.test.ts b/test/workspaces.test.ts index 85569cea..8331212d 100644 --- a/test/workspaces.test.ts +++ b/test/workspaces.test.ts @@ -476,7 +476,7 @@ describe('workspaces', () => { // cannot be stubbed while npm config printing occurs in viewMany describe('not stubbed', () => { - // TODO: Find a less fragile way to test npm config than comparing exact verbose output + // TODO: Find a less fragile way to test npm config than comparing verbose output it('merge local npm config with pnpm workspace npm config', async () => { // colors must be stripped on node v18+ const { default: stripAnsi } = await import('strip-ansi') @@ -489,9 +489,7 @@ describe('workspaces', () => { }) stripAnsi(output).should.include(`npm config (workspace project): { ncutest: 'root' }`) - stripAnsi(output).should.include(`Using merged npm config: -{ - ncutest: 'a',`) + stripAnsi(output).split('merged npm config:')[1].should.include(`ncutest: 'a'`) } finally { await fs.rm(tempDir, { recursive: true, force: true }) }