UNPKG

npm-check-extras

Version:

CLI app to check for outdated and unused dependencies, and run update/delete action over selected ones

161 lines (160 loc) 6.34 kB
import fs from 'node:fs'; import path from 'node:path'; import * as R from 'ramda'; import { atom, computed } from 'nanostores'; import { execa, execaCommand } from 'execa'; import { format } from 'date-fns'; import { getCommandFromSentence } from '../helpers.js'; import RU from '../ramda-utils.js'; import { dependenciesKinds } from '../constants.js'; import { travelServices } from '../services/index.js'; import { optionsManager } from './options.js'; import { $submittedInput } from './submitted-input.js'; import { statusesManager } from './status.js'; export const $allItems = atom([]); export const selectedItems = R.filter(R.propEq(true, 'isSelected'), $allItems.get()); export const $filteredItems = computed([$allItems, $submittedInput], (allItems, submittedInput) => { return R.filter(R.propSatisfies(R.includes(submittedInput), 'name'), allItems); }); export function clearPackages() { $allItems.set([]); } export const getUpdateCandidatePackages = async () => { const temporary = []; const addSelectedOptionsToArgs = (optionNames) => { return R.reduce((acc, optionName) => optionsManager.isSelectedByName(optionName) ? R.append(`--${optionName}`, acc) : acc, [], optionNames); }; const args = [ '--no-colors', '--no-emoji', ...addSelectedOptionsToArgs([ 'dev-only', 'production', 'global', 'skip-unused', ]), ]; const isExecaError = (error) => { return R.isNotNil(error.stdout); }; try { await execa('npx', ['npm-check', ...args]); } catch (error) { if (isExecaError(error)) { const lines = String(error.stdout).split('\n'); for (const curr of R.splitWhenever(R.equals(''), lines)) { const parts = R.head(curr).split(/\s{2,}/); if (!R.pathSatisfies(R.includes('npm-check -u'), [0])(parts) && R.length(curr) > 1 && !R.pathSatisfies((value) => value.includes('PKG ERR! Not in'), [1], curr)) temporary.push({ name: R.head(parts), message: R.join(' ', R.drop(2, parts)), actionInfo: R.compose(R.trim, R.last)(curr), isSelected: false, isActive: false, }); } } } return temporary; }; const checkPackages = async () => { clearPackages(); statusesManager.setFetching(); clearPackages(); try { const reportedPackages = await getUpdateCandidatePackages(); $allItems.set(reportedPackages); statusesManager.setDone(); } catch { statusesManager.setFailed(); } }; const getOperationFromInfo = (message) => { return R.cond([ [ R.compose(R.not, R.isEmpty, R.match(/to go from \d+/)), R.always('update'), ], [R.includes('remove this package'), R.always('delete')], [R.T, R.always('unknown')], ])(message); }; export const runUpdate = async () => { const needsToStoreHistory = optionsManager.isSelectedByName('store-history'); const itemsToUpdate = R.filter(R.propEq(true, 'isSelected'), $allItems.get()); packageActionsManager.setRunning(); let dependenciesValues = []; try { const pathToFile = path.join(process.cwd(), '.npm-check-history.json'); // eslint-disable-line n/prefer-global/process const today = format(new Date().toISOString(), 'yyyy-MM-dd HH:mm:ss'); let previousContent = {}; if (fs.existsSync(pathToFile)) previousContent = JSON.parse(fs.readFileSync(pathToFile).toString()); const contentToSave = R.propOr([], today, previousContent); if (needsToStoreHistory) { const packageJsonContent = await travelServices.readPackageJson(); dependenciesValues = R.toPairs(R.pick(dependenciesKinds, packageJsonContent)); } await Promise.all(R.map(async (selectedItem) => { const actionValue = getCommandFromSentence(selectedItem.actionInfo); await execaCommand(actionValue); if (needsToStoreHistory) { const foundPair = R.find((pair) => R.keys(R.last(pair)).includes(selectedItem.name), dependenciesValues); let packageDepTargetKey = 'dependencies'; let packageSemverValue; if (!R.isNil(foundPair)) { packageDepTargetKey = R.head(foundPair); packageSemverValue = R.prop(selectedItem.name, R.last(foundPair)); } contentToSave.push({ name: selectedItem.name, kindOfDependencyKey: packageDepTargetKey, semverValue: packageSemverValue, message: selectedItem.message, command: getCommandFromSentence(selectedItem.actionInfo), operation: getOperationFromInfo(selectedItem.actionInfo), info: selectedItem.actionInfo, }); } }, itemsToUpdate)); if (needsToStoreHistory) { const newContent = R.assoc(today, contentToSave, previousContent); fs.writeFileSync(pathToFile, JSON.stringify(newContent, null, 2)); } packageActionsManager.setSuccess(); } catch (error) { if (error instanceof Error) { fs.appendFileSync('.npm-check-extras-debug.log', `${error.message}\n`); packageActionsManager.setFailed(); } } }; export const $actionStatus = atom('WAITING'); export const hasPackages = computed($allItems, allItems => !R.isEmpty(allItems)); export const packageActionsManager = { runUpdate, checkPackages, isWaiting: () => RU.eqWaiting($actionStatus.get()), isRunning: () => RU.eqRunning($actionStatus.get()), isSuccess: () => RU.eqSuccess($actionStatus.get()), isFailed: () => RU.eqFailed($actionStatus.get()), setWaiting() { $actionStatus.set('WAITING'); }, setRunning() { $actionStatus.set('RUNNING'); }, setSuccess() { $actionStatus.set('SUCCESS'); }, setFailed() { $actionStatus.set('FAILED'); }, };