From 463d381afbefae6e65c75abe0b10905465814cd7 Mon Sep 17 00:00:00 2001 From: Raine Revere Date: Mon, 1 Aug 2022 09:27:06 -0400 Subject: [PATCH] Deep mode: Read npm config from each project directory. (#1177) --- package-lock.json | 13 +++ package.json | 1 + src/lib/figgy-pudding/index.js | 7 ++ src/lib/runLocal.ts | 6 +- src/package-managers/npm.ts | 143 +++++++++++++++++++++------------ 5 files changed, 115 insertions(+), 55 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5b051e8c..4bfeade7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -48,6 +48,7 @@ "@types/chai-string": "^1.4.2", "@types/cli-table": "^0.3.0", "@types/hosted-git-info": "^3.0.2", + "@types/ini": "^1.3.31", "@types/json-parse-helpfulerror": "^1.0.1", "@types/jsonlines": "^0.1.2", "@types/lodash": "^4.14.182", @@ -1256,6 +1257,12 @@ "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz", "integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==" }, + "node_modules/@types/ini": { + "version": "1.3.31", + "resolved": "https://registry.npmjs.org/@types/ini/-/ini-1.3.31.tgz", + "integrity": "sha512-8ecxxaG4AlVEM1k9+BsziMw8UsX0qy3jYI1ad/71RrDZ+rdL6aZB0wLfAuflQiDhkD5o4yJ0uPK3OSUic3fG0w==", + "dev": true + }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", @@ -8857,6 +8864,12 @@ "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz", "integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==" }, + "@types/ini": { + "version": "1.3.31", + "resolved": "https://registry.npmjs.org/@types/ini/-/ini-1.3.31.tgz", + "integrity": "sha512-8ecxxaG4AlVEM1k9+BsziMw8UsX0qy3jYI1ad/71RrDZ+rdL6aZB0wLfAuflQiDhkD5o4yJ0uPK3OSUic3fG0w==", + "dev": true + }, "@types/istanbul-lib-coverage": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", diff --git a/package.json b/package.json index 5eaf4696..a367b925 100644 --- a/package.json +++ b/package.json @@ -90,6 +90,7 @@ "@types/chai-string": "^1.4.2", "@types/cli-table": "^0.3.0", "@types/hosted-git-info": "^3.0.2", + "@types/ini": "^1.3.31", "@types/json-parse-helpfulerror": "^1.0.1", "@types/jsonlines": "^0.1.2", "@types/lodash": "^4.14.182", diff --git a/src/lib/figgy-pudding/index.js b/src/lib/figgy-pudding/index.js index 17a50d52..a419c269 100644 --- a/src/lib/figgy-pudding/index.js +++ b/src/lib/figgy-pudding/index.js @@ -18,6 +18,13 @@ class FiggyPudding { get(key) { return pudGet(this, key, true) } + toJSON() { + const obj = {} + this.forEach((val, key) => { + obj[key] = val + }) + return obj + } forEach(fn, thisArg = this) { for (let [key, value] of this.entries()) { fn.call(thisArg, value, key, this) diff --git a/src/lib/runLocal.ts b/src/lib/runLocal.ts index 176f9338..06435572 100644 --- a/src/lib/runLocal.ts +++ b/src/lib/runLocal.ts @@ -173,7 +173,7 @@ async function runLocal( const current = getCurrentDependencies(pkg, options) - print(options, '\nCurrent:', 'verbose') + print(options, '\nCurrent versions:', 'verbose') print(options, current, 'verbose') print(options, `\nFetching ${options.target} versions`, 'verbose') @@ -195,10 +195,10 @@ async function runLocal( print(options, upgradedPeerDependencies, 'verbose') } - print(options, '\nFetched:', 'verbose') + print(options, '\nFetched versions:', 'verbose') print(options, latest, 'verbose') - print(options, '\nUpgraded:', 'verbose') + print(options, '\nUpgraded versions:', 'verbose') print(options, upgraded, 'verbose') // filter out satisfied deps when using --minimal diff --git a/src/package-managers/npm.ts b/src/package-managers/npm.ts index 6ee3f007..1f6c51f1 100644 --- a/src/package-managers/npm.ts +++ b/src/package-managers/npm.ts @@ -1,14 +1,16 @@ import memoize from 'fast-memoize' import fs from 'fs' -import assign from 'lodash/assign' +import ini from 'ini' import camelCase from 'lodash/camelCase' import filter from 'lodash/filter' import get from 'lodash/get' import isEqual from 'lodash/isEqual' import last from 'lodash/last' +import omit from 'lodash/omit' import overEvery from 'lodash/overEvery' import pullAll from 'lodash/pullAll' import pacote from 'pacote' +import path from 'path' import semver from 'semver' import spawn from 'spawn-please' import { keyValueBy } from '../lib/keyValueBy' @@ -29,10 +31,12 @@ import { satisfiesPeerDependencies, } from './filters' +type NpmConfig = Index any)> + const TIME_FIELDS = ['modified', 'created'] -/** Reads the local npm config and normalizes keys for pacote. */ -const readNpmConfig = () => { +/** Normalizes the keys of an npm config for pacote. */ +const normalizeNpmConfig = (npmConfig: NpmConfig): NpmConfig => { const npmConfigToPacoteMap = { cafile: (path: string) => { // load-cafile, based on github.com/npm/cli/blob/40c1b0f/lib/config/load-cafile.js @@ -60,13 +64,7 @@ const readNpmConfig = () => { // needed until pacote supports full npm config compatibility // See: https://github.com/zkat/pacote/issues/156 - const config: Index = {} - // libnpmconfig incorrectly (?) ignores NPM_CONFIG_USERCONFIG because it is always overridden by the default builtin.userconfig - // set userconfig manually so that it is prioritized - const builtinsWithUserConfig = { - userconfig: process.env.npm_config_userconfig || process.env.NPM_CONFIG_USERCONFIG, - } - libnpmconfig.read(null, builtinsWithUserConfig).forEach((value: string, key: string) => { + const config: NpmConfig = keyValueBy(npmConfig, (key: string, value: string | boolean | ((path: string) => any)) => { // replace env ${VARS} in strings with the process.env value const normalizedValue = typeof value !== 'string' @@ -76,22 +74,54 @@ const readNpmConfig = () => { ? stringToBoolean(value) : value.replace(/\${([^}]+)}/, (_, envVar) => process.env[envVar] as string) + // normalize the key for pacote const { [key]: pacoteKey }: Index any)> = npmConfigToPacoteMap - if (typeof pacoteKey === 'string') { - config[pacoteKey] = normalizedValue - } else if (typeof pacoteKey === 'function') { - assign(config, pacoteKey(normalizedValue.toString())) - } else { - config[key.match(/^[a-z]/i) ? camelCase(key) : key] = normalizedValue - } - }) - config.cache = false + return typeof pacoteKey === 'string' + ? // key is mapped to a string + { [pacoteKey]: normalizedValue } + : // key is mapped to a function + typeof pacoteKey === 'function' + ? { ...pacoteKey(normalizedValue.toString()) } + : // otherwise assign the camel-cased key + { [key.match(/^[a-z]/i) ? camelCase(key) : key]: normalizedValue } + }) return config } -const npmConfig = readNpmConfig() +/** Finds and parses the npm config at the given path. If the path does not exist, returns null. If no path is provided, finds and merges the global and user npm configs using libnpmconfig and sets cache: false. */ +const findNpmConfig = (path?: string): NpmConfig | null => { + let config + + if (path) { + try { + config = ini.parse(fs.readFileSync(path, 'utf-8')) + } catch (err: any) { + if (err.code === 'ENOENT') { + return null + } else { + throw err + } + } + } else { + // libnpmconfig incorrectly (?) ignores NPM_CONFIG_USERCONFIG because it is always overridden by the default builtin.userconfig + // set userconfig manually so that it is prioritized + const opts = libnpmconfig.read(null, { + userconfig: process.env.npm_config_userconfig || process.env.NPM_CONFIG_USERCONFIG, + }) + config = { + ...opts.toJSON(), + cache: false, + } + } + + return normalizeNpmConfig(config) +} + +// get the base config that is used for all npm queries +// this may be partially overwritten by .npmrc config files when using --deep +const npmConfig = findNpmConfig() /** A promise that returns true if --global is deprecated on the system npm. Spawns "npm --version". */ const isGlobalDeprecated = new Promise((resolve, reject) => { @@ -147,7 +177,7 @@ export async function packageAuthorChanged( currentVersion: VersionSpec, upgradedVersion: VersionSpec, options: Options = {}, - npmConfigLocal?: Index, + npmConfigLocal?: NpmConfig, ) { const result = await pacote.packument(packageName, { ...npmConfigLocal, @@ -169,12 +199,6 @@ export async function packageAuthorChanged( return false } -export interface ViewOptions { - registry?: string - timeout?: number - retry?: number -} - /** * Returns an object of specified values retrieved by npm view. * @@ -187,33 +211,47 @@ export async function viewMany( packageName: string, fields: string[], currentVersion: Version, - { registry, timeout, retry }: ViewOptions = {}, + options: Options, retried = 0, - npmConfigLocal?: Index, + npmConfigLocal?: NpmConfig, ) { if (currentVersion && (!semver.validRange(currentVersion) || versionUtil.isWildCard(currentVersion))) { return Promise.resolve({} as Packument) } + // merge project npm config with base config + const npmConfigProjectPath = options.packageFile ? path.join(options.packageFile, '../.npmrc') : null + const npmConfigProject = options.packageFile ? findNpmConfig(npmConfigProjectPath!) : null + const npmConfigCWDPath = options.cwd ? path.join(options.cwd, '.npmrc') : null + const npmConfigCWD = options.cwd ? findNpmConfig(npmConfigCWDPath!) : null + + if (npmConfigProject) { + print(options, `\nUsing npm config in project directory: ${npmConfigProjectPath}:`, 'verbose') + print(options, omit(npmConfigProject, 'cache'), 'verbose') + } + + if (npmConfigCWD) { + print(options, `\nUsing npm config in current working directory: ${npmConfigCWDPath}:`, 'verbose') + // omit cache since it is added to every config + print(options, omit(npmConfigCWD, 'cache'), 'verbose') + } + + const npmOptions = { + ...npmConfig, + ...npmConfigLocal, + ...npmConfigProject, + ...npmConfigCWD, + ...(options.registry ? { registry: options.registry, silent: true } : null), + ...(options.timeout ? { timeout: options.timeout } : null), + fullMetadata: fields.includes('time'), + } + let result: any try { - result = await pacote.packument(packageName, { - ...npmConfigLocal, - ...npmConfig, - fullMetadata: fields.includes('time'), - ...(registry ? { registry, silent: true } : null), - ...(timeout ? { timeout } : null), - }) + result = await pacote.packument(packageName, npmOptions) } catch (err: any) { - if (retry && ++retried <= retry) { - const packument: Packument = await viewMany( - packageName, - fields, - currentVersion, - { registry, timeout, retry }, - retried, - npmConfigLocal, - ) + if (options.retry && ++retried <= options.retry) { + const packument: Packument = await viewMany(packageName, fields, currentVersion, options, retried, npmConfigLocal) return packument } @@ -246,8 +284,8 @@ export async function viewOne( packageName: string, field: string, currentVersion: Version, - options: ViewOptions = {}, - npmConfigLocal?: Index, + options: Options, + npmConfigLocal?: NpmConfig, ) { const result = await viewManyMemoized(packageName, [field], currentVersion, options, 0, npmConfigLocal) return result && result[field as keyof Packument] @@ -414,11 +452,12 @@ export const list = async (options: Options = {}) => { * @returns */ export const distTag: GetVersion = async (packageName, currentVersion, options: Options = {}) => { - const revision = (await viewOne(packageName, `dist-tags.${options.distTag}`, currentVersion, { - registry: options.registry, - timeout: options.timeout, - retry: options.retry, - })) as unknown as Packument // known type based on dist-tags.latest + const revision = (await viewOne( + packageName, + `dist-tags.${options.distTag}`, + currentVersion, + options, + )) as unknown as Packument // known type based on dist-tags.latest // latest should not be deprecated // if latest exists and latest is not a prerelease version, return it