npm-check-updates
Version:
Find newer versions of dependencies than what your package.json allows
290 lines • 13.5 kB
JavaScript
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const promises_1 = __importDefault(require("fs/promises"));
const rimraf_1 = require("rimraf");
const spawn_please_1 = __importDefault(require("spawn-please"));
const logging_1 = require("../lib/logging");
const bun_1 = __importDefault(require("../package-managers/bun"));
const npm_1 = __importDefault(require("../package-managers/npm"));
const pnpm_1 = __importDefault(require("../package-managers/pnpm"));
const yarn_1 = __importDefault(require("../package-managers/yarn"));
const chalk_1 = __importStar(require("./chalk"));
const loadPackageInfoFromFile_1 = __importDefault(require("./loadPackageInfoFromFile"));
const upgradePackageData_1 = __importDefault(require("./upgradePackageData"));
/** Run npm, yarn, pnpm, or bun. */
const npm = (args, options, print, { spawnOptions } = {}) => {
if (print) {
console.log(chalk_1.default.blue([options.packageManager, ...args].join(' ')));
}
const spawnOptionsMerged = {
cwd: options.cwd || process.cwd(),
env: {
...process.env,
// TODO: Why does CI break pnpm install?
...(options.packageManager !== 'pnpm' ? { CI: '1' } : null),
FORCE_COLOR: '1',
...spawnOptions === null || spawnOptions === void 0 ? void 0 : spawnOptions.env,
},
...spawnOptions,
};
const npmOptions = {
...(options.global ? { location: 'global' } : null),
...(options.prefix ? { prefix: options.prefix } : null),
};
return (options.packageManager === 'pnpm'
? pnpm_1.default
: options.packageManager === 'yarn'
? yarn_1.default
: options.packageManager === 'bun'
? bun_1.default
: npm_1.default)(args, npmOptions, spawnOptionsMerged);
};
/** Load and validate package file and tests. */
const loadPackageFileForDoctor = async (options) => {
var _a;
// assert no --packageData or --packageFile
if (options.packageData || options.packageFile) {
console.error('--packageData and --packageFile are not allowed with --doctor. You must execute "ncu --doctor" in a directory with a package file so it can install dependencies and test them.');
process.exit(1);
}
let packageInfo;
// assert package.json
try {
packageInfo = await (0, loadPackageInfoFromFile_1.default)(options, 'package.json');
}
catch (e) {
console.error('Missing or invalid package.json');
process.exit(1);
}
// assert npm script "test" (unless a custom test script is specified)
if (!options.doctorTest && !((_a = packageInfo.pkg.scripts) === null || _a === void 0 ? void 0 : _a.test)) {
console.error('No npm "test" script defined. You must define a "test" script in the "scripts" section of your package.json to use --doctor.');
process.exit(1);
}
return packageInfo;
};
/** Iteratively installs upgrades and runs tests to identify breaking upgrades. */
// we have to pass run directly since it would be a circular require if doctor included this file
const doctor = async (run, options) => {
var _a;
await (0, chalk_1.chalkInit)();
const lockFileName = options.packageManager === 'yarn'
? 'yarn.lock'
: options.packageManager === 'pnpm'
? 'pnpm-lock.yaml'
: options.packageManager === 'bun'
? 'bun.lockb'
: 'package-lock.json';
const { pkg, pkgFile } = await loadPackageFileForDoctor(options);
// flatten all deps into one so we can iterate over them
const allDependencies = {
...pkg.dependencies,
...pkg.devDependencies,
...pkg.optionalDependencies,
};
/** Install dependencies using "npm run install" or a custom script given by --doctorInstall. */
const runInstall = async () => {
if (options.doctorInstall) {
const [installCommand, ...testArgs] = options.doctorInstall.split(' ');
console.log(chalk_1.default.blue(options.doctorInstall));
await (0, spawn_please_1.default)(installCommand, testArgs);
}
else {
await npm(['install'], { packageManager: options.packageManager }, true);
}
};
/** Run the tests using "npm run test" or a custom script given by --doctorTest. */
const runTests = async () => {
const spawnOptions = {
stderr: (data) => {
console.error(chalk_1.default.red(data.toString()));
},
// Test runners typically write to stdout, so we need to print stdout.
// Otherwise test failures will be silenced.
stdout: (data) => {
process.stdout.write(data.toString());
},
};
if (options.doctorTest) {
const regexp = /"(.+?)"|'(.+?)'|[^ ]+/g;
const matches = options.doctorTest.matchAll(regexp);
let groups = [];
// eslint-disable-next-line fp/no-loops
for (const match of matches) {
groups = [...groups, match[2] || match[1] || match[0]];
}
const [testCommand, ...testArgs] = groups;
console.log(chalk_1.default.blue(options.doctorTest));
await (0, spawn_please_1.default)(testCommand, testArgs, spawnOptions);
}
else {
await npm(['run', 'test'], {
packageManager: options.packageManager,
}, true, { spawnOptions });
}
};
console.log(`Running tests before upgrading`);
// initial install
await runInstall();
// save lock file if there is one
let lockFile = '';
try {
lockFile = await promises_1.default.readFile(lockFileName, 'utf-8');
}
catch (e) { }
// make sure current tests pass before we begin
try {
await runTests();
}
catch (e) {
console.error('Tests failed before we even got started!');
process.exit(1);
}
if (!options.interactive) {
console.log(`Upgrading all dependencies and re-running tests`);
}
// upgrade all dependencies
// save upgrades for later in case we need to iterate
console.log(chalk_1.default.blue('ncu ' +
process.argv
.slice(2)
.filter(arg => arg !== '--doctor')
.join(' ')));
process.env.NCU_DOCTOR = '1';
const upgrades = (await run({
...options,
silent: true,
// --doctor triggers the initial call to doctor, but the internal call needs to executes npm-check-updates normally in order to upgrade the dependencies
doctor: false,
}));
if (Object.keys(upgrades || {}).length === 0) {
console.log('All dependencies are up-to-date ' + chalk_1.default.green.bold(':)'));
return;
}
// track if installing dependencies was successful
// this allows us to skip re-installing when it fails and proceed straight to installing individual dependencies
let installAllSuccess = false;
// run tests on all upgrades
try {
// install after all upgrades
await runInstall();
installAllSuccess = true;
// run tests after all upgrades
await runTests();
console.log(`${chalk_1.default.green('✓')} Tests pass`);
await (0, logging_1.printUpgrades)(options, {
current: allDependencies,
upgraded: upgrades,
total: Object.keys(upgrades || {}).length,
});
console.log(`\n${options.interactive ? 'Chosen' : 'All'} dependencies upgraded and installed ${chalk_1.default.green(':)')}`);
}
catch {
console.error(chalk_1.default.red(installAllSuccess ? 'Tests failed' : 'Install failed'));
console.log(`Identifying broken dependencies`);
// restore package file, lockFile and re-install
await promises_1.default.writeFile('package.json', pkgFile);
if (lockFile) {
await promises_1.default.writeFile(lockFileName, lockFile);
}
else {
await (0, rimraf_1.rimraf)(lockFileName);
}
// save the last package file with passing tests
let lastPkgFile = pkgFile;
// re-install after restoring package file and lock file
// only re-install if the tests failed, not if npm install failed
if (installAllSuccess) {
try {
await runInstall();
}
catch (e) {
const installCommand = (options.packageManager || 'npm') + ' install';
throw new Error(`Error: Doctor mode was about to test individual upgrades, but ${chalk_1.default.cyan(installCommand)} failed after rolling back to your existing package and lock files. This is unexpected since the initial install before any upgrades succeeded. Either npm failed to revert a partial install, or failed anomalously on the second run. Please check your internet connection and retry. If doctor mode fails consistently, report a bug with your complete list of dependency versions at https://github.com/raineorshine/npm-check-updates/issues.`);
}
}
// iterate upgrades
let name, version;
// eslint-disable-next-line fp/no-loops
for ([name, version] of Object.entries(upgrades)) {
try {
// install single dependency
await npm([
...(options.packageManager === 'yarn' ||
options.packageManager === 'pnpm' ||
options.packageManager === 'bun'
? ['add']
: ['install', '--no-save']),
`${name}@${version}`,
], { packageManager: options.packageManager }, true);
// if there is a prepare script, we need to run it manually since --no-save does not run prepare automatically
// https://github.com/raineorshine/npm-check-updates/issues/1170
if ((_a = pkg.scripts) === null || _a === void 0 ? void 0 : _a.prepare) {
try {
await npm(['run', 'prepare'], { packageManager: options.packageManager }, true);
}
catch (e) {
console.error(chalk_1.default.red('Prepare script failed'));
throw e;
}
}
// run tests after individual upgrade
await runTests();
console.log(` ${chalk_1.default.green('✓')} ${name} ${allDependencies[name]} → ${version}`);
// save upgraded package data so that passing versions can still be saved even when there is a failure
lastPkgFile = await (0, upgradePackageData_1.default)(lastPkgFile, { [name]: allDependencies[name] }, { [name]: version }, options);
// save working lock file
lockFile = await promises_1.default.readFile(lockFileName, 'utf-8');
}
catch (e) {
// print failing package
console.error(` ${chalk_1.default.red('✗')} ${name} ${allDependencies[name]} → ${version}\n`);
console.error(chalk_1.default.red(e));
// restore last good lock file
await promises_1.default.writeFile(lockFileName, lockFile);
// restore package.json since yarn and pnpm do not have the --no-save option
if (options.packageManager === 'yarn' ||
options.packageManager === 'pnpm' ||
options.packageManager === 'bun') {
await promises_1.default.writeFile('package.json', lastPkgFile);
}
}
}
// silently restore last passing package file and lock file
// only print message if package file is updated
if (lastPkgFile !== pkgFile) {
console.log('Saving partially upgraded package.json');
await promises_1.default.writeFile('package.json', lastPkgFile);
}
// re-install from restored package.json and lockfile
await runInstall();
}
};
exports.default = doctor;
//# sourceMappingURL=doctor.js.map
;