UNPKG

@loopback/cli

Version:
299 lines (277 loc) 8.41 kB
// 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 'use strict'; 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;