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
JavaScript
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');
},
};