Skip to content

Commit 8d056c4

Browse files
committed
feat(cli): check project deps against cli template
Sometimes a user try to run newer version of `lb4` against existing applications. This feature will prompt users to confirm if some of the project deps do not satisify the cli template. It also checks exit status to avoid unnessary prompts.
1 parent 203c3f9 commit 8d056c4

File tree

10 files changed

+95
-18
lines changed

10 files changed

+95
-18
lines changed

packages/cli/generators/app/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,12 @@ module.exports = class AppGenerator extends ProjectGenerator {
5454
}
5555

5656
promptProjectName() {
57+
if (this.shouldExit()) return;
5758
return super.promptProjectName();
5859
}
5960

6061
promptProjectDir() {
62+
if (this.shouldExit()) return;
6163
return super.promptProjectDir();
6264
}
6365

@@ -83,6 +85,7 @@ module.exports = class AppGenerator extends ProjectGenerator {
8385
}
8486

8587
promptOptions() {
88+
if (this.shouldExit()) return;
8689
return super.promptOptions();
8790
}
8891

packages/cli/generators/controller/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,11 +66,13 @@ module.exports = class ControllerGenerator extends ArtifactGenerator {
6666
}
6767

6868
promptArtifactName() {
69+
if (this.shouldExit()) return;
6970
return super.promptArtifactName();
7071
}
7172

7273
promptArtifactType() {
7374
debug('Prompting for controller type');
75+
if (this.shouldExit()) return;
7476
return this.prompt([
7577
{
7678
type: 'list',
@@ -92,6 +94,7 @@ module.exports = class ControllerGenerator extends ArtifactGenerator {
9294
}
9395

9496
async promptArtifactCrudVars() {
97+
if (this.shouldExit()) return;
9598
if (
9699
!this.artifactInfo.controllerType ||
97100
this.artifactInfo.controllerType === ControllerGenerator.BASIC

packages/cli/generators/datasource/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ module.exports = class DataSourceGenerator extends ArtifactGenerator {
9999
*/
100100
promptConnector() {
101101
debug('prompting for datasource connector');
102+
if (this.shouldExit()) return;
102103
const prompts = [
103104
{
104105
name: 'connector',
@@ -123,6 +124,7 @@ module.exports = class DataSourceGenerator extends ArtifactGenerator {
123124
* `npm` module name for the connector.
124125
*/
125126
promptCustomConnectorInfo() {
127+
if (this.shouldExit()) return;
126128
if (this.artifactInfo.connector !== 'other') {
127129
debug('custom connector option was not selected');
128130
return;
@@ -149,6 +151,7 @@ module.exports = class DataSourceGenerator extends ArtifactGenerator {
149151
*/
150152
promptConnectorConfig() {
151153
debug('prompting for connector config');
154+
if (this.shouldExit()) return;
152155
// Check to make sure connector is from connectors list (not custom)
153156
const settings =
154157
(connectors[this.artifactInfo.connector] &&

packages/cli/generators/example/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ module.exports = class extends BaseGenerator {
5454
_describeExamples() {}
5555

5656
promptExampleName() {
57+
if (this.shouldExit()) return;
5758
if (this.options['example-name']) {
5859
this.exampleName = this.options['example-name'];
5960
return;
@@ -80,6 +81,7 @@ module.exports = class extends BaseGenerator {
8081
}
8182

8283
validateExampleName() {
84+
if (this.shouldExit()) return;
8385
if (this.exampleName in EXAMPLES) return;
8486
this.exit(
8587
`Invalid example name: ${this.exampleName}\n` +

packages/cli/generators/model/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ module.exports = class ModelGenerator extends ArtifactGenerator {
6767

6868
// Prompt a user for Model Name
6969
async promptArtifactName() {
70+
if (this.shouldExit()) return;
7071
await super.promptArtifactName();
7172
this.artifactInfo.className = utils.toClassName(this.artifactInfo.name);
7273
this.log();
@@ -77,6 +78,7 @@ module.exports = class ModelGenerator extends ArtifactGenerator {
7778

7879
// Prompt for a Property Name
7980
async promptPropertyName() {
81+
if (this.shouldExit()) return;
8082
this.log(`Enter an empty property name when done`);
8183
this.log();
8284

packages/cli/generators/openapi/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ module.exports = class OpenApiGenerator extends BaseGenerator {
4646
}
4747

4848
async askForSpecUrlOrPath() {
49+
if (this.shouldExit()) return;
4950
const prompts = [
5051
{
5152
name: 'url',
@@ -64,6 +65,7 @@ module.exports = class OpenApiGenerator extends BaseGenerator {
6465
}
6566

6667
async loadAndBuildApiSpec() {
68+
if (this.shouldExit()) return;
6769
try {
6870
const result = await loadAndBuildSpec(this.url, {
6971
log: this.log,

packages/cli/generators/repository/index.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,12 @@ module.exports = class RepositoryGenerator extends ArtifactGenerator {
177177
return super.setOptions();
178178
}
179179

180+
checkLoopBackProject() {
181+
return super.checkLoopBackProject();
182+
}
183+
180184
async checkPaths() {
185+
if (this.shouldExit()) return;
181186
// check for datasources
182187
if (!fs.existsSync(this.artifactInfo.datasourcesDir)) {
183188
return this.exit(

packages/cli/generators/service/index.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,12 @@ module.exports = class ServiceGenerator extends ArtifactGenerator {
5858
return super.setOptions();
5959
}
6060

61+
checkLoopBackProject() {
62+
return super.checkLoopBackProject();
63+
}
64+
6165
async checkPaths() {
66+
if (this.shouldExit()) return;
6267
// check for datasources
6368
if (!fs.existsSync(this.artifactInfo.datasourcesDir)) {
6469
new Error(
@@ -76,6 +81,7 @@ module.exports = class ServiceGenerator extends ArtifactGenerator {
7681
*/
7782
async promptArtifactName() {
7883
debug('Prompting for service name');
84+
if (this.shouldExit()) return;
7985

8086
if (this.options.name) {
8187
Object.assign(this.artifactInfo, {name: this.options.name});
@@ -99,6 +105,7 @@ module.exports = class ServiceGenerator extends ArtifactGenerator {
99105

100106
async promptDataSourceName() {
101107
debug('Prompting for a datasource ');
108+
if (this.shouldExit()) return;
102109
let cmdDatasourceName;
103110
let datasourcesList;
104111

@@ -186,6 +193,7 @@ module.exports = class ServiceGenerator extends ArtifactGenerator {
186193
* strongly-typed service proxies and corresponding model definitions.
187194
*/
188195
async inferInterfaces() {
196+
if (this.shouldExit()) return;
189197
let connectorType = utils.getDataSourceConnectorName(
190198
this.artifactInfo.datasourcesDir,
191199
this.artifactInfo.dataSourceClass,

packages/cli/lib/base-generator.js

Lines changed: 48 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,8 @@ const chalk = require('chalk');
1010
const {StatusConflicter, readTextFromStdin} = require('./utils');
1111
const path = require('path');
1212
const fs = require('fs');
13-
const readline = require('readline');
1413
const debug = require('./debug')('base-generator');
15-
const assert = require('assert');
14+
const semver = require('semver');
1615

1716
/**
1817
* Base Generator for LoopBack 4
@@ -130,17 +129,16 @@ module.exports = class BaseGenerator extends Generator {
130129
* @param {*} question
131130
*/
132131
async _getDefaultAnswer(question, answers) {
132+
// First check existing answers
133+
let defaultVal = answers[question.name];
134+
if (defaultVal != null) return defaultVal;
135+
136+
// Now check the `default` of the prompt
133137
let def = question.default;
134138
if (typeof question.default === 'function') {
135139
def = await question.default(answers);
136140
}
137-
let defaultVal = def;
138-
139-
if (def == null) {
140-
// No `default` is set for the question, check existing answers
141-
defaultVal = answers[question.name];
142-
if (defaultVal != null) return defaultVal;
143-
}
141+
defaultVal = def;
144142

145143
if (question.type === 'confirm') {
146144
return defaultVal != null ? defaultVal : true;
@@ -274,7 +272,7 @@ module.exports = class BaseGenerator extends Generator {
274272
* keyword 'loopback' under 'keywords' attribute in package.json.
275273
* 'keywords' is an array
276274
*/
277-
checkLoopBackProject() {
275+
async checkLoopBackProject() {
278276
debug('Checking for loopback project');
279277
if (this.shouldExit()) return false;
280278
const pkg = this.fs.readJSON(this.destinationPath('package.json'));
@@ -297,8 +295,48 @@ module.exports = class BaseGenerator extends Generator {
297295
'The command must be run in a LoopBack project.',
298296
);
299297
this.exit(err);
298+
return;
300299
}
301300
this.packageJson = pkg;
301+
302+
const projectDeps = pkg.dependencies || {};
303+
const projectDevDeps = pkg.devDependencies || {};
304+
305+
const cliPkg = require('../package.json');
306+
const templateDeps = cliPkg.config.templateDependencies;
307+
const incompatibleDeps = {};
308+
for (const d in templateDeps) {
309+
const versionRange = projectDeps[d] || projectDevDeps[d];
310+
if (!versionRange || semver.intersects(versionRange, templateDeps[d]))
311+
continue;
312+
incompatibleDeps[d] = [versionRange, templateDeps[d]];
313+
}
314+
315+
if (Object.keys(incompatibleDeps).length === 0) {
316+
// No incompatible dependencies
317+
return;
318+
}
319+
this.log(
320+
chalk.red(
321+
'The project has dependencies with incompatible versions required by the CLI:',
322+
),
323+
);
324+
for (const d in incompatibleDeps) {
325+
this.log(chalk.yellow('- %s: %s (cli %s)'), d, ...incompatibleDeps[d]);
326+
}
327+
const prompts = [
328+
{
329+
name: 'ignoreIncompatibleDependencies',
330+
message: `Continue to run the command?`,
331+
type: 'confirm',
332+
default: false,
333+
},
334+
];
335+
const answers = await this.prompt(prompts);
336+
if (answers && answers.ignoreIncompatibleDependencies) {
337+
return;
338+
}
339+
this.exit(new Error('Incompatible dependencies'));
302340
}
303341

304342
_runNpmScript(projectDir, args) {

packages/cli/test/integration/lib/artifact-generator.js

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -85,27 +85,38 @@ module.exports = function(artiGenerator) {
8585
/No `loopback` keyword found/,
8686
);
8787

88-
it('passes if "keywords" maps to "loopback"', () => {
88+
testCheckLoopBack(
89+
'throws an error if dependencies have incompatible versions',
90+
{
91+
keywords: ['loopback'],
92+
dependencies: {'@loopback/context': '^0.0.0'},
93+
},
94+
/Incompatible dependencies/,
95+
);
96+
97+
it('passes if "keywords" maps to "loopback"', async () => {
8998
gen.fs.readJSON.returns({keywords: ['test', 'loopback']});
90-
assert.doesNotThrow(() => {
91-
gen.checkLoopBackProject();
92-
}, Error);
99+
await gen.checkLoopBackProject();
93100
});
94101

95102
function testCheckLoopBack(testName, obj, expected) {
96-
it(testName, () => {
103+
it(testName, async () => {
97104
let logs = [];
98105
gen.log = function(...args) {
99106
logs = logs.concat(args);
100107
};
108+
gen.prompt = async () => ({
109+
ignoreIncompatibleDependencies: false,
110+
});
101111
gen.fs.readJSON.returns(obj);
102-
gen.checkLoopBackProject();
112+
await gen.checkLoopBackProject();
103113
assert(gen.exitGeneration instanceof Error);
104114
assert(gen.exitGeneration.message.match(expected));
105115
gen.end();
106-
assert.deepEqual(logs, [
116+
assert.equal(
117+
logs[logs.length - 1],
107118
chalk.red('Generation is aborted:', gen.exitGeneration),
108-
]);
119+
);
109120
});
110121
}
111122
});

0 commit comments

Comments
 (0)