Skip to content

Commit

Permalink
npm: Factor out and memoize mergeNpmConfigs to avoid duplicate verbos…
Browse files Browse the repository at this point in the history
…e messaging.
  • Loading branch information
raineorshine committed Sep 16, 2023
1 parent cf7fe3e commit 424937a
Show file tree
Hide file tree
Showing 5 changed files with 90 additions and 63 deletions.
14 changes: 7 additions & 7 deletions src/lib/logging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T extends { [key: string]: any }>(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<keyof Options, any>(
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. */
Expand Down
4 changes: 2 additions & 2 deletions src/lib/runGlobal.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -10,7 +10,7 @@ import upgradePackageDefinitions from './upgradePackageDefinitions'
/** Checks global dependencies for upgrades. */
async function runGlobal(options: Options): Promise<Index<string> | void> {
print(options, '\nOptions:', 'verbose')
printOptionsSorted(options, 'verbose')
printSorted(options, options, 'verbose')

print(options, '\nGetting installed packages', 'verbose')
const globalPackages = await getInstalledPackages(
Expand Down
4 changes: 2 additions & 2 deletions src/lib/runLocal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -152,7 +152,7 @@ async function runLocal(
pkgFile?: Maybe<string>,
): Promise<PackageFile | Index<VersionSpec>> {
print(options, '\nOptions:', 'verbose')
printOptionsSorted(options, 'verbose')
printSorted(options, options, 'verbose')

let pkg: PackageFile

Expand Down
125 changes: 77 additions & 48 deletions src/package-managers/npm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -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 {
Expand Down
6 changes: 2 additions & 4 deletions test/workspaces.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -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 })
}
Expand Down

0 comments on commit 424937a

Please sign in to comment.