@loopback/cli
Version:
Yeoman generator for LoopBack 4
299 lines (277 loc) • 8.41 kB
JavaScript
// Copyright IBM Corp. and LoopBack contributors 2019,2020. All Rights Reserved.
// Node module: @loopback/cli
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
;
const fse = require('fs-extra');
const semver = require('semver');
const chalk = require('chalk');
const latestVersion = require('latest-version');
const cliPkg = require('../package.json');
const g = require('./globalize');
const templateDeps = cliPkg.config.templateDependencies;
/**
* Print @loopback/* versions
* @param log - A function to log information
*/
function printVersions(log = console.log) {
const ver = cliPkg.version;
log('@loopback/cli version: %s', ver);
log('\n@loopback/* dependencies:');
for (const d in templateDeps) {
if (d.startsWith('@loopback/') && d !== '@loopback/cli') {
log(' - %s: %s', d, templateDeps[d]);
}
}
}
/**
* Check project dependencies against module versions from the cli template
* @param generator - Yeoman generator instance
*/
async function checkDependencies(generator) {
const pkg = generator.fs.readJSON(generator.destinationPath('package.json'));
const isUpdate = generator.command === 'update';
const pkgDeps = pkg
? {
dependencies: {...pkg.dependencies},
devDependencies: {...pkg.devDependencies},
peerDependencies: {...pkg.peerDependencies},
}
: {};
if (!pkg) {
if (isUpdate) {
printVersions(generator.log);
await checkCliVersion(generator.log);
return;
}
const err = new Error(
'No package.json found in ' +
generator.destinationRoot() +
'. ' +
'The command must be run in a LoopBack project.',
);
generator.exit(err);
return;
}
const dependentPackage = '@loopback/core';
const projectDepsNames = isUpdate
? Object.keys(
// Check dependencies, devDependencies, and peerDependencies
{
...pkgDeps.dependencies,
...pkgDeps.devDependencies,
...pkgDeps.peerDependencies,
},
)
: Object.keys(pkgDeps.dependencies);
const isLBProj = isUpdate
? projectDepsNames.some(n => n.startsWith('@loopback/'))
: projectDepsNames.includes(dependentPackage);
if (!isLBProj) {
const err = new Error(
'No `@loopback/core` package found in the "dependencies" section of ' +
generator.destinationPath('package.json') +
'. ' +
'The command must be run in a LoopBack project.',
);
generator.exit(err);
return;
}
const incompatibleDeps = {
dependencies: {},
devDependencies: {},
peerDependencies: {},
};
let found = false;
for (const d in templateDeps) {
for (const s in incompatibleDeps) {
const versionRange = pkgDeps[s][d];
if (!versionRange) continue;
const templateDep = templateDeps[d];
// https://github.com/loopbackio/loopback-next/issues/2028
// https://github.com/npm/node-semver/pull/238
// semver.intersects does not like `*`, `x`, or `X`
if (versionRange.match(/^\*|x|X/)) continue;
if (generator.options.semver === false) {
// For `lb4 update` command, check exact matches
if (versionRange !== templateDep) {
incompatibleDeps[s][d] = [versionRange, templateDep];
found = true;
}
continue;
}
if (semver.intersects(versionRange, templateDep)) continue;
incompatibleDeps[s][d] = [versionRange, templateDep];
found = true;
}
}
if (!found) {
// No incompatible dependencies
if (generator.command === 'update') {
generator.log(
chalk.green(
`The project dependencies are compatible with @loopback/cli@${cliPkg.version}`,
),
);
}
return;
}
const originalCliVersion = generator.config.get('version') || '<unknown>';
generator.log(
chalk.red(
g.f(
'The project was originally generated by @loopback/cli@%s.',
originalCliVersion,
),
),
);
generator.log(
chalk.red(
g.f(
'The following dependencies are incompatible with @loopback/cli@%s:',
cliPkg.version,
),
),
);
for (const s in incompatibleDeps) {
generator.log(s);
for (const d in incompatibleDeps[s]) {
generator.log(
chalk.yellow('- %s: %s (cli %s)'),
d,
...incompatibleDeps[s][d],
);
}
}
return incompatibleDeps;
}
/**
* Update project dependencies with module versions from the cli template
* @param pkg - Package json object for the project
* @param generator - Yeoman generator instance
*/
function updateDependencies(generator) {
const pkg =
generator.packageJson.getAll() ||
generator.fs.readJSON(generator.destinationPath('package.json'));
const depUpdates = [];
for (const d in templateDeps) {
if (
pkg.dependencies &&
pkg.dependencies[d] &&
pkg.dependencies[d] !== templateDeps[d]
) {
depUpdates.push(
`- Dependency ${d}: ${pkg.dependencies[d]} => ${templateDeps[d]}`,
);
pkg.dependencies[d] = templateDeps[d];
}
if (
pkg.devDependencies &&
pkg.devDependencies[d] &&
pkg.devDependencies[d] !== templateDeps[d]
) {
depUpdates.push(
`- DevDependency ${d}: ${pkg.devDependencies[d]} => ${templateDeps[d]}`,
);
pkg.devDependencies[d] = templateDeps[d];
}
if (
pkg.peerDependencies &&
pkg.peerDependencies[d] &&
pkg.peerDependencies[d] !== templateDeps[d]
) {
depUpdates.push(
`- PeerDependency ${d}: ${pkg.peerDependencies[d]} => ${templateDeps[d]}`,
);
pkg.peerDependencies[d] = templateDeps[d];
}
}
if (depUpdates.length) {
depUpdates.sort().forEach(d => generator.log(d));
}
generator.log(
chalk.red('Upgrading dependencies may break the current project.'),
);
generator.fs.writeJSON(generator.destinationPath('package.json'), pkg);
// Remove `node_modules` force a fresh install
if (generator.command === 'update' && !generator.options['skip-install']) {
fse.removeSync(generator.destinationPath('node_modules'));
}
generator.pkgManagerInstall();
}
/**
* Check the LoopBack project dependencies and versions
* @param generator - Yeoman generator instance
*/
async function checkLoopBackProject(generator) {
if (generator.shouldExit()) return false;
const incompatibleDeps = await checkDependencies(generator);
if (incompatibleDeps == null) return false;
if (
Object.keys({
...incompatibleDeps.dependencies,
...incompatibleDeps.devDependencies,
...incompatibleDeps.peerDependencies,
}) === 0
)
return false;
const choices = [
{
name: g.f('Upgrade project dependencies'),
value: 'upgrade',
},
{
name: g.f('Skip upgrading project dependencies'),
value: 'continue',
},
];
if (generator.command !== 'update') {
choices.unshift({
name: g.f('Abort now'),
value: 'abort',
});
}
const prompts = [
{
name: 'decision',
message: g.f('How do you want to proceed?'),
type: 'list',
choices,
default: 0,
},
];
const answers = await generator.prompt(prompts);
if (answers && answers.decision === 'continue') {
return false;
}
if (answers && answers.decision === 'upgrade') {
updateDependencies(generator);
return true;
}
generator.exit(new Error('Incompatible dependencies'));
}
/**
* Check if the current cli is out of date
* @param log - Log function
*/
async function checkCliVersion(log = console.log) {
const latestCliVersion = await latestVersion('@loopback/cli');
if (latestCliVersion !== cliPkg.version) {
const current = chalk.grey(cliPkg.version);
const latest = chalk.green(latestCliVersion);
const cmd = chalk.cyan(`npm i -g ${cliPkg.name}`);
const message = `
Update available ${current} ${chalk.reset(' → ')} ${latest}
Run ${cmd} to update.`;
log(message);
} else {
log(chalk.green(`${cliPkg.name}@${cliPkg.version} is up to date.`));
}
}
exports.printVersions = printVersions;
exports.checkCliVersion = checkCliVersion;
exports.checkDependencies = checkDependencies;
exports.updateDependencies = updateDependencies;
exports.checkLoopBackProject = checkLoopBackProject;
exports.cliPkg = cliPkg;