Skip to content

Commit c95aa23

Browse files
Raymond Fengraymondfeng
authored andcommitted
feat: Add exit() to abort generation
Throwing an error in yeoman is ugly. This PR allows a generator to abort with a reason. The reason will be logged at the end.
1 parent 445e893 commit c95aa23

File tree

8 files changed

+135
-32
lines changed

8 files changed

+135
-32
lines changed

packages/cli/generators/app/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ module.exports = class extends ProjectGenerator {
3535
}
3636

3737
promptApplication() {
38+
if (this.shouldExit()) return false;
3839
const prompts = [
3940
{
4041
type: 'input',
@@ -62,6 +63,7 @@ module.exports = class extends ProjectGenerator {
6263
}
6364

6465
end() {
66+
if (!super.end()) return false;
6567
this.log();
6668
this.log(
6769
'Application %s is now created in %s.',

packages/cli/generators/controller/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ module.exports = class ControllerGenerator extends ArtifactGenerator {
3131

3232
scaffold() {
3333
super.scaffold();
34+
if (this.shouldExit()) return false;
3435
this.artifactInfo.filename =
3536
utils.kebabCase(this.artifactInfo.name) + '.controller.ts';
3637

@@ -46,6 +47,8 @@ module.exports = class ControllerGenerator extends ArtifactGenerator {
4647
}
4748

4849
end() {
50+
super.end();
51+
if (this.shouldExit()) return false;
4952
// logs a message if there is no file conflict
5053
if (
5154
this.conflicter.generationStatus[this.artifactInfo.filename] !== 'skip' &&

packages/cli/generators/extension/index.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ module.exports = class extends ProjectGenerator {
3939
}
4040

4141
promptComponent() {
42+
if (this.shouldExit()) return false;
4243
const prompts = [
4344
{
4445
type: 'input',
@@ -65,4 +66,8 @@ module.exports = class extends ProjectGenerator {
6566
install() {
6667
return super.install();
6768
}
69+
70+
end() {
71+
return super.end();
72+
}
6873
};

packages/cli/lib/artifact-generator.js

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@
44
// License text available at https://opensource.org/licenses/MIT
55

66
'use strict';
7-
const Generator = require('yeoman-generator');
7+
const BaseGenerator = require('./base-generator');
88
const utils = require('./utils');
99
const StatusConflicter = utils.StatusConflicter;
1010

11-
module.exports = class ArtifactGenerator extends Generator {
11+
module.exports = class ArtifactGenerator extends BaseGenerator {
1212
// Note: arguments and options should be defined in the constructor.
1313
constructor(args, opts) {
1414
super(args, opts);
@@ -34,34 +34,44 @@ module.exports = class ArtifactGenerator extends Generator {
3434
);
3535
}
3636

37-
/**
38-
* Override the usage text by replacing `yo loopback4:` with `lb4 `.
39-
*/
40-
usage() {
41-
const text = super.usage();
42-
return text.replace(/^yo loopback4:/g, 'lb4 ');
43-
}
44-
4537
/**
4638
* Checks if current directory is a LoopBack project by checking for
4739
* keyword 'loopback' under 'keywords' attribute in package.json.
4840
* 'keywords' is an array
4941
*/
5042
checkLoopBackProject() {
43+
if (this.shouldExit()) return false;
5144
const pkg = this.fs.readJSON(this.destinationPath('package.json'));
5245
const key = 'loopback';
53-
if (!pkg) throw new Error('unable to load package.json');
54-
if (!pkg.keywords || !pkg.keywords.includes(key))
55-
throw new Error('keywords does not map to loopback in package.json');
56-
return;
46+
if (!pkg) {
47+
const err = new Error(
48+
'No package.json found in ' +
49+
this.destinationRoot() +
50+
'. ' +
51+
'The command must be run in a LoopBack project.'
52+
);
53+
this.exit(err);
54+
return;
55+
}
56+
if (!pkg.keywords || !pkg.keywords.includes(key)) {
57+
const err = new Error(
58+
'No `loopback` keyword found in ' +
59+
this.destinationPath('package.json') +
60+
'. ' +
61+
'The command must be run in a LoopBack project.'
62+
);
63+
this.exit(err);
64+
}
5765
}
5866

5967
promptArtifactName() {
68+
if (this.shouldExit()) return false;
6069
const prompts = [
6170
{
6271
type: 'input',
6372
name: 'name',
64-
message: utils.toClassName(this.artifactInfo.type) + ' class name:', // capitalization
73+
// capitalization
74+
message: utils.toClassName(this.artifactInfo.type) + ' class name:',
6575
when: this.artifactInfo.name === undefined,
6676
validate: utils.validateClassName,
6777
},
@@ -73,6 +83,7 @@ module.exports = class ArtifactGenerator extends Generator {
7383
}
7484

7585
scaffold() {
86+
if (this.shouldExit()) return false;
7687
// Capitalize class name
7788
this.artifactInfo.name = utils.toClassName(this.artifactInfo.name);
7889

@@ -85,6 +96,5 @@ module.exports = class ArtifactGenerator extends Generator {
8596
{},
8697
{globOptions: {dot: true}}
8798
);
88-
return;
8999
}
90100
};

packages/cli/lib/base-generator.js

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// Copyright IBM Corp. 2017. All Rights Reserved.
2+
// Node module: @loopback/cli
3+
// This file is licensed under the MIT License.
4+
// License text available at https://opensource.org/licenses/MIT
5+
6+
'use strict';
7+
const Generator = require('yeoman-generator');
8+
const path = require('path');
9+
const utils = require('./utils');
10+
const chalk = require('chalk');
11+
12+
/**
13+
* Base Generator for LoopBack 4
14+
*/
15+
module.exports = class BaseGenerator extends Generator {
16+
// Note: arguments and options should be defined in the constructor.
17+
constructor(args, opts) {
18+
super(args, opts);
19+
this._setupGenerator();
20+
}
21+
22+
/**
23+
* Subclasses can extend _setupGenerator() to set up the generator
24+
*/
25+
_setupGenerator() {
26+
// No operation
27+
}
28+
29+
/**
30+
* Override the usage text by replacing `yo loopback4:` with `lb4 `.
31+
*/
32+
usage() {
33+
const text = super.usage();
34+
return text.replace(/^yo loopback4:/g, 'lb4 ');
35+
}
36+
37+
/**
38+
* Tell this generator to exit with the given reason
39+
* @param {string|Error} reason
40+
*/
41+
exit(reason) {
42+
// exit(false) should not exit
43+
if (reason === false) return;
44+
// exit(), exit(undefined), exit('') should exit
45+
if (!reason) reason = true;
46+
this.exitGeneration = reason;
47+
}
48+
49+
/**
50+
* Check if the generator should exit
51+
*/
52+
shouldExit() {
53+
return !!this.exitGeneration;
54+
}
55+
56+
/**
57+
* Print out the exit reason if this generator is told to exit before it ends
58+
*/
59+
end() {
60+
if (this.shouldExit()) {
61+
this.log(chalk.red('Generation is aborted:', this.exitGeneration));
62+
return false;
63+
}
64+
return true;
65+
}
66+
};

packages/cli/lib/project-generator.js

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@
44
// License text available at https://opensource.org/licenses/MIT
55

66
'use strict';
7-
const Generator = require('yeoman-generator');
7+
const BaseGenerator = require('./base-generator');
88
const path = require('path');
99
const utils = require('./utils');
1010

11-
module.exports = class extends Generator {
11+
module.exports = class ProjectGenerator extends BaseGenerator {
1212
// Note: arguments and options should be defined in the constructor.
1313
constructor(args, opts) {
1414
super(args, opts);
@@ -53,14 +53,6 @@ module.exports = class extends Generator {
5353
});
5454
}
5555

56-
/**
57-
* Override the usage text by replacing `yo loopback4:` with `lb4 `.
58-
*/
59-
usage() {
60-
const text = super.usage();
61-
return text.replace(/^yo loopback4:/g, 'lb4 ');
62-
}
63-
6456
setOptions() {
6557
this.projectInfo = {projectType: this.projectType};
6658
[
@@ -80,6 +72,7 @@ module.exports = class extends Generator {
8072
}
8173

8274
promptProjectName() {
75+
if (this.shouldExit()) return false;
8376
const prompts = [
8477
{
8578
type: 'input',
@@ -103,6 +96,7 @@ module.exports = class extends Generator {
10396
}
10497

10598
promptProjectDir() {
99+
if (this.shouldExit()) return false;
106100
const prompts = [
107101
{
108102
type: 'input',
@@ -122,6 +116,7 @@ module.exports = class extends Generator {
122116
}
123117

124118
promptOptions() {
119+
if (this.shouldExit()) return false;
125120
const choices = [];
126121
['tslint', 'prettier', 'mocha', 'loopbackBuild'].forEach(f => {
127122
if (!this.options[f]) {
@@ -155,6 +150,7 @@ module.exports = class extends Generator {
155150
}
156151

157152
scaffold() {
153+
if (this.shouldExit()) return false;
158154
this.destinationRoot(this.projectInfo.outdir);
159155

160156
// First copy common files from ../../project/templates
@@ -210,6 +206,7 @@ module.exports = class extends Generator {
210206
}
211207

212208
install() {
209+
if (this.shouldExit()) return false;
213210
this.npmInstall(null, {}, {cwd: this.destinationRoot()});
214211
}
215212
};

packages/cli/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
},
3636
"dependencies": {
3737
"camelcase-keys": "^4.1.0",
38+
"chalk": "^2.3.0",
3839
"debug": "^3.1.0",
3940
"lodash": "^4.17.4",
4041
"minimist": "^1.2.0",

packages/cli/test/artifact.js

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
const assert = require('yeoman-assert');
88
const yeoman = require('yeoman-environment');
99
const sinon = require('sinon');
10+
const chalk = require('chalk');
1011
var fs = require('mem-fs-editor').create(require('mem-fs').create());
1112

1213
module.exports = function(artiGenerator) {
@@ -18,17 +19,20 @@ module.exports = function(artiGenerator) {
1819
testSetUpGen({args: '2foobar'});
1920
}, Error);
2021
});
22+
2123
it('succeeds if no arg is provided', () => {
2224
assert.doesNotThrow(() => {
2325
testSetUpGen();
2426
}, Error);
2527
});
28+
2629
it('succeeds if arg is valid', () => {
2730
assert.doesNotThrow(() => {
2831
testSetUpGen({args: ['foobar']});
2932
}, Error);
3033
});
3134
});
35+
3236
it('has name argument set up', () => {
3337
let gen = testSetUpGen();
3438
let helpText = gen.help();
@@ -37,12 +41,14 @@ module.exports = function(artiGenerator) {
3741
assert(helpText.match(/Type: String/));
3842
assert(helpText.match(/Required: false/));
3943
});
44+
4045
it('sets up artifactInfo', () => {
4146
let gen = testSetUpGen({args: ['test']});
4247
assert(gen.artifactInfo);
4348
assert(gen.artifactInfo.name == 'test');
4449
});
4550
});
51+
4652
describe('usage', () => {
4753
it('prints lb4', () => {
4854
let gen = testSetUpGen();
@@ -51,22 +57,24 @@ module.exports = function(artiGenerator) {
5157
assert(!helpText.match(/loopback4:/));
5258
});
5359
});
60+
5461
describe('checkLoopBackProject', () => {
5562
testCheckLoopBack(
5663
'throws an error if no package.json is present',
5764
undefined,
58-
/unable to load package.json/
65+
/No package.json found/
5966
);
6067
testCheckLoopBack(
6168
'throws an error if "keywords" key does not exist',
6269
{foobar: 'test'},
63-
/does not map to loopback/
70+
/No `loopback` keyword found/
6471
);
6572
testCheckLoopBack(
6673
'throws an error if "keywords" key does not map to an array with "loopback" as a member',
6774
{keywords: ['foobar', 'test']},
68-
/does not map to loopback/
75+
/No `loopback` keyword found/
6976
);
77+
7078
it('passes if "keywords" maps to "loopback"', () => {
7179
let gen = testSetUpGen();
7280
gen.fs.readJSON = sinon.stub(fs, 'readJSON');
@@ -76,18 +84,28 @@ module.exports = function(artiGenerator) {
7684
}, Error);
7785
gen.fs.readJSON.restore();
7886
});
87+
7988
function testCheckLoopBack(testName, obj, expected) {
8089
it(testName, () => {
8190
let gen = testSetUpGen();
91+
let logs = [];
92+
gen.log = function(...args) {
93+
logs = logs.concat(args);
94+
};
8295
gen.fs.readJSON = sinon.stub(fs, 'readJSON');
8396
gen.fs.readJSON.returns(obj);
84-
assert.throws(() => {
85-
gen.checkLoopBackProject();
86-
}, expected);
97+
gen.checkLoopBackProject();
98+
assert(gen.exitGeneration instanceof Error);
99+
assert(gen.exitGeneration.message.match(expected));
100+
gen.end();
101+
assert.deepEqual(logs, [
102+
chalk.red('Generation is aborted:', gen.exitGeneration),
103+
]);
87104
gen.fs.readJSON.restore();
88105
});
89106
}
90107
});
108+
91109
describe('promptArtifactName', () => {
92110
it('incorporates user input into artifactInfo', () => {
93111
let gen = testSetUpGen();
@@ -100,6 +118,7 @@ module.exports = function(artiGenerator) {
100118
});
101119
});
102120
});
121+
103122
// returns the generator
104123
function testSetUpGen(arg) {
105124
arg = arg || {};

0 commit comments

Comments
 (0)