From d88b4c1b27bc80fbbea1b2d2a9fdbb2164704744 Mon Sep 17 00:00:00 2001 From: ominestre Date: Wed, 25 May 2022 16:52:53 -0400 Subject: [PATCH] Adds support for only checking prod deps refs #1 - Adds a prod-only flag to CLI - Changes configuration to also accept prod only - Adds `npm ls` command runner to npm interactions - Uses `npm ls --prod` to get a list of prod dependencies and filter the outdated dependencies based upon those results - Created test helpers and moved directory traversal helpers to a shared file --- src/bin/rotten-deps.ts | 9 +++++ src/lib/config.ts | 1 + src/lib/index.ts | 39 +++++++++++++++++++- src/lib/npm-interactions.ts | 51 ++++++++++++++++++++++++++ test/api.test.ts | 14 +------ test/helpers/test-directory-helpers.ts | 18 +++++++++ test/npm-interactions.test.ts | 23 +++++++++++- wallaby.conf.js | 1 + 8 files changed, 139 insertions(+), 17 deletions(-) create mode 100644 test/helpers/test-directory-helpers.ts diff --git a/src/bin/rotten-deps.ts b/src/bin/rotten-deps.ts index 87673fe..cd1f6e7 100644 --- a/src/bin/rotten-deps.ts +++ b/src/bin/rotten-deps.ts @@ -52,6 +52,12 @@ yargs boolean: true, requiresArg: false, }) + .option('ignore-dev', { + description: 'ignores developer dependencies', + boolean: true, + requiresArg: false, + default: false, + }) .parseAsync() .then(argv => { if (argv.help) yargs.showHelp(); @@ -147,12 +153,14 @@ yargs const configPath = argv['config-path']; const defaultExpiration = argv['default-expiration']; + const ignoreDev = argv['ignore-dev']; const configParser = (raw: string): Promise => new Promise( (resolve) => { const parsed = JSON.parse(raw); if (defaultExpiration) parsed.defaultExpiration = defaultExpiration; + if (ignoreDev) parsed.ignoreDevDependencies = ignoreDev; resolve(parsed); }, ); @@ -175,6 +183,7 @@ yargs }; if (defaultExpiration) config.defaultExpiration = defaultExpiration; + if (ignoreDev) config.ignoreDevDependencies = ignoreDev; maestro(config); } diff --git a/src/lib/config.ts b/src/lib/config.ts index 817c7b5..a285c22 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -17,6 +17,7 @@ interface Rule { export interface Config { defaultExpiration?: number; + ignoreDevDependencies?: boolean, readonly kind?: 'config'; readonly rules: Rule[]; } diff --git a/src/lib/index.ts b/src/lib/index.ts index fbca995..789550b 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -4,7 +4,12 @@ */ import { createFileReader, createConfig } from './config'; -import { createOutdatedRequest, createDetailsRequest, PackageDetails } from './npm-interactions'; +import { + createOutdatedRequest, + createDetailsRequest, + PackageDetails, + createListRequest, +} from './npm-interactions'; import type { Config } from './config'; import type { OutdatedPackage, OutdatedData } from './npm-interactions'; @@ -79,6 +84,34 @@ const getIndividualPackageDetails = async (outdated: OutdatedData): Promise => { + if (!c.ignoreDevDependencies) return outdated; + + const listRequest = createListRequest(true); + const maybeModules = await listRequest(); + + if (maybeModules instanceof Error) throw maybeModules; + const setOfInstalledDependencies = new Set(maybeModules.getListOfInstalledDependencies()); + const setOfOutdatedDependencies = new Set(Object.entries(outdated)); + + const filtered: OutdatedData = {}; + + setOfOutdatedDependencies.forEach((dep) => { + const [name, data] = dep; + if (!setOfInstalledDependencies.has(name)) setOfOutdatedDependencies.delete(dep); + filtered[name] = data; + }); + + return filtered; +}; + + /** * Compares the details on each dependency flagged as outdated in order to * determine how stale a version actually is. @@ -96,10 +129,12 @@ export const generateReport = async (c: Config, r?: Reporter): Promise; } +interface ListDependency { + version: string; + resolved: string; +} + +interface ListResponse { + version: string, + name: string, + dependencies: Record; +} + +interface ListWithHelpers { + data: ListResponse; + getListOfInstalledDependencies: () => Array; +} + +interface ListRequest { + (): Promise; +} + /** * Creates a function for running `npm outdated` @@ -77,7 +97,38 @@ export const createDetailsRequest = (dependencyName: string): DetailsRequest => }; +const isDependencyList = (result: ListResponse | any): result is ListResponse => + (result as ListResponse).version !== undefined + && (result as ListResponse).name !== undefined + && (result as ListResponse).dependencies !== undefined; + + +/** + * Uses the `npm ls` command to get a list of installed dependencies of a project. + * @param prod if this is set to true dev dependencies will be ignored + */ +export const createListRequest = (prod = false): ListRequest => { + const command = process.platform === 'win32' ? 'npm.cmd' : 'npm'; + const args = ['ls', '--json']; + + if (prod) args.push('--prod'); + + return () => new Promise(resolve => { + proc.execFile(command, args, { encoding: 'utf8' }, (err, stdout) => { + const results = JSON.parse(stdout); + if (!isDependencyList(results)) resolve(new Error('unexpected list response')); + + resolve({ + data: results, + getListOfInstalledDependencies: () => Object.getOwnPropertyNames(results.dependencies), + }); + }); + }); +}; + + export default { createOutdatedRequest, createDetailsRequest, + createListRequest, }; diff --git a/test/api.test.ts b/test/api.test.ts index 4fcab15..92c0f25 100644 --- a/test/api.test.ts +++ b/test/api.test.ts @@ -4,23 +4,11 @@ import { assert } from 'chai'; import { spy } from 'sinon'; import { generateReport } from '../src/lib/index'; import { createOutdatedRequest } from '../src/lib/npm-interactions'; - - -const defaultDir = process.cwd(); -const changeDir = (directory: string) => () => process.chdir(directory); -const restoreDir = changeDir(defaultDir); -const sampleAppDir = changeDir( - join(__dirname, './dummies/sample-app'), -); -const noInstallDir = changeDir( - join(__dirname, './dummies/sample-app-no-install'), -); - +import { restoreDir, sampleAppDir, noInstallDir } from './helpers/test-directory-helpers'; const sampleConfigDir = join(__dirname, './dummies/'); const getTestConfig = (configID: string) => JSON.parse(readFileSync(`${sampleConfigDir}/${configID}.json`, { encoding: 'utf8' })); - describe('API integrations', () => { afterEach(restoreDir); diff --git a/test/helpers/test-directory-helpers.ts b/test/helpers/test-directory-helpers.ts new file mode 100644 index 0000000..7f90ba2 --- /dev/null +++ b/test/helpers/test-directory-helpers.ts @@ -0,0 +1,18 @@ +import { join } from 'path'; + +const defaultDir = process.cwd(); +const changeDir = (directory: string) => () => process.chdir(directory); + +export const restoreDir = changeDir(defaultDir); +export const sampleAppDir = changeDir( + join(__dirname, '../dummies/sample-app'), +); +export const noInstallDir = changeDir( + join(__dirname, '../dummies/sample-app-no-install'), +); + +export default { + restoreDir, + sampleAppDir, + noInstallDir, +} diff --git a/test/npm-interactions.test.ts b/test/npm-interactions.test.ts index b4ad196..9a87f05 100644 --- a/test/npm-interactions.test.ts +++ b/test/npm-interactions.test.ts @@ -1,5 +1,6 @@ import { assert } from 'chai'; import npmLib from '../src/lib/npm-interactions'; +import { restoreDir, sampleAppDir } from './helpers/test-directory-helpers'; /** @@ -16,23 +17,27 @@ const setPlatformWindows = setProcessPlatform('win32'); const setPlatformDarwin = setProcessPlatform('darwin'); describe('NPM Interaction Library', () => { - it('Should create a function for outdated and view requests for windows machines', () => { + it('Should create functions for interactions on windows machines', () => { setPlatformWindows(); const getOutdatedRequest = npmLib.createOutdatedRequest(); const getDetailsRequest = npmLib.createDetailsRequest('banana'); + const getListRequest = npmLib.createListRequest(); assert(typeof getOutdatedRequest === 'function'); assert(typeof getDetailsRequest === 'function'); + assert(typeof getListRequest === 'function'); restorePlatform(); }); - it('Should create a function for outdated and view requests for non-windows machines', () => { + it('Should create functions for interactions on non-windows machines', () => { setPlatformDarwin(); const getOutdatedRequest = npmLib.createOutdatedRequest(); const getDetailsRequest = npmLib.createDetailsRequest('banana'); + const getListRequest = npmLib.createListRequest(); assert(typeof getOutdatedRequest === 'function'); assert(typeof getDetailsRequest === 'function'); + assert(typeof getListRequest === 'function'); restorePlatform(); }); @@ -47,5 +52,19 @@ describe('NPM Interaction Library', () => { const response = await getDetailsRequest(); assert.equal(response.name, 'express'); }).timeout(8000); + + it('Should get a list of installed dependencies', async () => { + sampleAppDir(); + const getListRequest = npmLib.createListRequest(); + const response = await getListRequest(); + + assert(!(response instanceof Error)); + + const listOfInstalledDependencies = response.getListOfInstalledDependencies(); + assert(listOfInstalledDependencies.includes('mocha')); + restoreDir(); + }).timeout(8000); + + it('Should get a list of installed prod dependencies'); }); diff --git a/wallaby.conf.js b/wallaby.conf.js index 48a3121..46c8272 100644 --- a/wallaby.conf.js +++ b/wallaby.conf.js @@ -4,6 +4,7 @@ module.exports = () => ({ 'src/lib/*.ts', { pattern: 'src/bin/rotten-deps.ts', instrument: false }, { pattern: 'test/dummies/**/*', instrument: false }, + { pattern: 'test/helpers/**/*', instrument: false }, ], tests: [ 'test/*.test.js',