Skip to content

Commit 14e7897

Browse files
authored
feat(cli): Tooling for empty controller (#762)
* feat(cli): Tooling for empty controller scaffold CLI tooling to scaffold an empty controller * feat(cli): remove Conflicter use existing Conflicter packaged * feat(cli): tweak name validation * feat(cli): applied feedback and added more tests * feat(cli): change to kebab-case * feat(cli): change keywords mapping requirement * feat(cli): does not log if file conflict * feat(cli): use custom Conflicter
1 parent 1cb0994 commit 14e7897

File tree

9 files changed

+579
-20
lines changed

9 files changed

+579
-20
lines changed

packages/cli/bin/cli.js

100644100755
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ var env = yeoman.createEnv();
3131

3232
env.register(path.join(__dirname, '../generators/app'), 'loopback4:app');
3333
env.register(path.join(__dirname, '../generators/extension'), 'loopback4:extension');
34+
env.register(path.join(__dirname, '../generators/controller'), 'loopback4:controller');
3435

3536
// list generators
3637
if (opts.commands) {
@@ -62,4 +63,4 @@ const options = camelCaseKeys(opts, {exclude: ['--', /^\w$/, 'argv']});
6263
Object.assign(options, opts);
6364

6465
debug('env.run %j %j', args, options);
65-
env.run(args, options);
66+
env.run(args, options);
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
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 ArtifactGenerator = require('../../lib/artifact-generator');
8+
const utils = require('../../lib/utils');
9+
10+
module.exports = class ControllerGenerator extends ArtifactGenerator {
11+
// Note: arguments and options should be defined in the constructor.
12+
constructor(args, opts) {
13+
super(args, opts);
14+
}
15+
16+
_setupGenerator() {
17+
this.artifactInfo = {
18+
type: 'controller',
19+
outdir: 'src/controllers/',
20+
};
21+
return super._setupGenerator();
22+
}
23+
24+
checkLoopBackProject() {
25+
return super.checkLoopBackProject();
26+
}
27+
28+
promptArtifactName() {
29+
return super.promptArtifactName();
30+
}
31+
32+
scaffold() {
33+
super.scaffold();
34+
this.artifactInfo.filename =
35+
utils.kebabCase(this.artifactInfo.name) + '.controller.ts';
36+
37+
// renames the file
38+
this.fs.move(
39+
this.destinationPath(this.artifactInfo.outdir + 'controller-template.ts'),
40+
this.destinationPath(
41+
this.artifactInfo.outdir + this.artifactInfo.filename
42+
),
43+
{globOptions: {dot: true}}
44+
);
45+
return;
46+
}
47+
48+
end() {
49+
// logs a message if there is no file conflict
50+
if (
51+
this.conflicter.generationStatus[this.artifactInfo.filename] !== 'skip' &&
52+
this.conflicter.generationStatus[this.artifactInfo.filename] !==
53+
'identical'
54+
) {
55+
this.log();
56+
this.log(
57+
'Controller %s is now created in src/controllers/',
58+
this.artifactInfo.name
59+
);
60+
}
61+
}
62+
};
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// Uncomment these imports to begin using these cool features!
2+
3+
// import {inject} from @loopback/context;
4+
5+
6+
export class <%= name %>Controller {
7+
constructor() {}
8+
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
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 utils = require('./utils');
9+
const StatusConflicter = utils.StatusConflicter;
10+
11+
module.exports = class ArtifactGenerator extends Generator {
12+
// Note: arguments and options should be defined in the constructor.
13+
constructor(args, opts) {
14+
super(args, opts);
15+
this._setupGenerator();
16+
}
17+
18+
_setupGenerator() {
19+
this.argument('name', {
20+
type: String,
21+
required: false,
22+
description: 'Name for the ' + this.artifactInfo.type,
23+
});
24+
// argument validation
25+
if (this.args.length) {
26+
const validationMsg = utils.validateClassName(this.args[0]);
27+
if (typeof validationMsg === 'string') throw new Error(validationMsg);
28+
}
29+
this.artifactInfo.name = this.args[0];
30+
this.artifactInfo.defaultName = 'new';
31+
this.conflicter = new StatusConflicter(
32+
this.env.adapter,
33+
this.options.force
34+
);
35+
}
36+
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+
45+
/**
46+
* Checks if current directory is a LoopBack project by checking for
47+
* keyword 'loopback' under 'keywords' attribute in package.json.
48+
* 'keywords' is an array
49+
*/
50+
checkLoopBackProject() {
51+
const pkg = this.fs.readJSON(this.destinationPath('package.json'));
52+
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;
57+
}
58+
59+
promptArtifactName() {
60+
const prompts = [
61+
{
62+
type: 'input',
63+
name: 'name',
64+
message: utils.toClassName(this.artifactInfo.type) + ' name:', // capitalization
65+
when: this.artifactInfo.name === undefined,
66+
default: this.artifactInfo.defaultName,
67+
validate: utils.validateClassName,
68+
},
69+
];
70+
71+
return this.prompt(prompts).then(props => {
72+
Object.assign(this.artifactInfo, props);
73+
});
74+
}
75+
76+
scaffold() {
77+
// Capitalize class name
78+
this.artifactInfo.name = utils.toClassName(this.artifactInfo.name);
79+
80+
// Copy template files from ./templates
81+
// Renaming of the files should be done in the generator inheriting from this one
82+
this.fs.copyTpl(
83+
this.templatePath('**/*'),
84+
this.destinationPath(),
85+
this.artifactInfo,
86+
{},
87+
{globOptions: {dot: true}}
88+
);
89+
return;
90+
}
91+
};

packages/cli/lib/utils.js

Lines changed: 74 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,36 +7,70 @@
77

88
const fs = require('fs');
99
const util = require('util');
10+
const regenerate = require('regenerate');
11+
const _ = require('lodash');
12+
const Conflicter = require('yeoman-generator/lib/util/conflicter');
13+
14+
/**
15+
* Returns a valid variable name regex;
16+
* taken from https://gist.github.com/mathiasbynens/6334847
17+
*/
18+
function generateValidRegex() {
19+
const get = function(what) {
20+
return require('unicode-10.0.0/' + what + '/code-points.js');
21+
};
22+
const ID_Start = get('Binary_Property/ID_Start');
23+
const ID_Continue = get('Binary_Property/ID_Continue');
24+
const compileRegex = _.template(
25+
'^(?:<%= identifierStart %>)(?:<%= identifierPart %>)*$'
26+
);
27+
const identifierStart = regenerate(ID_Start).add('$', '_');
28+
const identifierPart = regenerate(ID_Continue).add(
29+
'$',
30+
'_',
31+
'\u200C',
32+
'\u200D'
33+
);
34+
const regex = compileRegex({
35+
identifierStart: identifierStart.toString(),
36+
identifierPart: identifierPart.toString(),
37+
});
38+
return new RegExp(regex);
39+
}
40+
const validRegex = generateValidRegex();
41+
exports.validRegex = validRegex;
1042

1143
/**
1244
* validate application (module) name
1345
* @param name
1446
* @returns {String|Boolean}
1547
*/
16-
exports.validateAppName = function(name) {
17-
if (name.charAt(0) === '.') {
18-
return util.format('Application name cannot start with .: %s', name);
48+
exports.validateClassName = function(name) {
49+
if (!name || name === '') {
50+
return 'Class name cannot be empty';
1951
}
20-
if (name.match(/[\/@\s\+%:]/)) {
21-
return util.format(
22-
'Application name cannot contain special characters (/@+%: ): %s',
23-
name
24-
);
52+
if (name.match(validRegex)) {
53+
return true;
54+
}
55+
if (!isNaN(name.charAt(0))) {
56+
return util.format('Class name cannot start with a number: %s', name);
2557
}
26-
if (name.toLowerCase() === 'node_modules') {
27-
return util.format('Application name cannot be node_modules');
58+
if (name.includes('.')) {
59+
return util.format('Class name cannot contain .: %s', name);
2860
}
29-
if (name.toLowerCase() === 'favicon.ico') {
30-
return util.format('Application name cannot be favicon.ico');
61+
if (name.includes(' ')) {
62+
return util.format('Class name cannot contain spaces: %s', name);
3163
}
32-
if (name !== encodeURIComponent(name)) {
64+
if (name.includes('-')) {
65+
return util.format('Class name cannot contain hyphens: %s', name);
66+
}
67+
if (name.match(/[\/@\s\+%:]/)) {
3368
return util.format(
34-
'Application name cannot contain special characters escaped by' +
35-
' encodeURIComponent: %s',
69+
'Class name cannot contain special characters (/@+%: ): %s',
3670
name
3771
);
3872
}
39-
return true;
73+
return util.format('Class name is invalid: %s', name);
4074
};
4175

4276
/**
@@ -50,9 +84,30 @@ exports.validateyNotExisting = function(path) {
5084
};
5185

5286
/**
53-
* Convert a name to class name
87+
* Converts a name to class name after validation
5488
*/
5589
exports.toClassName = function(name) {
56-
const n = name.substring(0, 1).toUpperCase() + name.substring(1);
57-
return n.replace(/[\-\.@\s\+]/g, '_');
90+
if (name == '') return new Error('no input');
91+
if (typeof name != 'string' || name == null) return new Error('bad input');
92+
return name.substring(0, 1).toUpperCase() + name.substring(1);
93+
};
94+
95+
exports.kebabCase = _.kebabCase;
96+
97+
/**
98+
* Extends conflicter so that it keeps track of conflict status
99+
*/
100+
exports.StatusConflicter = class StatusConflicter extends Conflicter {
101+
constructor(adapter, force) {
102+
super(adapter, force);
103+
this.generationStatus = {}; // keeps track of file conflict history
104+
}
105+
106+
checkForCollision(filepath, contents, callback) {
107+
super.checkForCollision(filepath, contents, (err, status) => {
108+
let filename = filepath.split('/').pop();
109+
this.generationStatus[filename] = status;
110+
callback(err, status);
111+
});
112+
}
58113
};

packages/cli/package.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,22 @@
2424
"yeoman-generator"
2525
],
2626
"devDependencies": {
27+
"@loopback/testlab": "^4.0.0-alpha.14",
28+
"mem-fs": "^1.1.3",
29+
"mem-fs-editor": "^3.0.2",
2730
"mocha": "^4.0.1",
2831
"nsp": "^3.1.0",
32+
"sinon": "^4.1.2",
2933
"yeoman-assert": "^3.1.0",
3034
"yeoman-test": "^1.7.0"
3135
},
3236
"dependencies": {
3337
"camelcase-keys": "^4.1.0",
3438
"debug": "^3.1.0",
39+
"lodash": "^4.17.4",
3540
"minimist": "^1.2.0",
41+
"regenerate": "^1.3.3",
42+
"unicode-10.0.0": "^0.7.4",
3643
"yeoman-generator": "^2.0.1"
3744
},
3845
"scripts": {

0 commit comments

Comments
 (0)