Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions docs/site/includes/CLI-std-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,21 @@

`--skip-install`
: Do not automatically install dependencies. Default is false.

`-c, --config`
: JSON file name or value to configure options

For example,
```sh
lb4 app --config config.json
lb4 app --config {"name":"my-app"}
cat config.json | lb4 app --config stdin
lb4 app --config stdin < config.json
lb4 app --config stdin << EOF
> {"name":"my-app"}
> EOF
```

`-y, --yes`
: Skip all confirmation prompts with default or provided value
<!-- prettier-ignore-end -->
5 changes: 4 additions & 1 deletion packages/cli/generators/controller/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ module.exports = class ControllerGenerator extends ArtifactGenerator {
}

_setupGenerator() {
super._setupGenerator();
this.artifactInfo = {
type: 'controller',
rootDir: 'src',
Expand All @@ -54,8 +55,10 @@ module.exports = class ControllerGenerator extends ArtifactGenerator {
required: false,
description: 'Type for the ' + this.artifactInfo.type,
});
}

return super._setupGenerator();
setOptions() {
return super.setOptions();
}

checkLoopBackProject() {
Expand Down
4 changes: 4 additions & 0 deletions packages/cli/generators/datasource/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ module.exports = class DataSourceGenerator extends ArtifactGenerator {
return super._setupGenerator();
}

setOptions() {
return super.setOptions();
}

/**
* Ensure CLI is being run in a LoopBack 4 project.
*/
Expand Down
15 changes: 10 additions & 5 deletions packages/cli/lib/artifact-generator.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,23 +19,27 @@ module.exports = class ArtifactGenerator extends BaseGenerator {

_setupGenerator() {
debug('Setting up generator');
super._setupGenerator();
this.argument('name', {
type: String,
required: false,
description: 'Name for the ' + this.artifactInfo.type,
});
}

setOptions() {
// argument validation
if (this.args.length) {
const validationMsg = utils.validateClassName(this.args[0]);
const name = this.options.name;
if (name) {
const validationMsg = utils.validateClassName(name);
if (typeof validationMsg === 'string') throw new Error(validationMsg);
}
this.artifactInfo.name = this.args[0];
this.artifactInfo.defaultName = 'new';
this.artifactInfo.name = name;
this.artifactInfo.relPath = path.relative(
this.destinationPath(),
this.artifactInfo.outDir,
);
super._setupGenerator();
return super.setOptions();
}

promptArtifactName() {
Expand All @@ -48,6 +52,7 @@ module.exports = class ArtifactGenerator extends BaseGenerator {
// capitalization
message: utils.toClassName(this.artifactInfo.type) + ' class name:',
when: this.artifactInfo.name === undefined,
default: this.artifactInfo.name,
validate: utils.validateClassName,
},
];
Expand Down
207 changes: 204 additions & 3 deletions packages/cli/lib/base-generator.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@

const Generator = require('yeoman-generator');
const chalk = require('chalk');
const debug = require('./debug')('artifact-generator');
const utils = require('./utils');
const StatusConflicter = utils.StatusConflicter;
const {StatusConflicter, readTextFromStdin} = require('./utils');
const path = require('path');
const fs = require('fs');
const readline = require('readline');
const debug = require('./debug')('base-generator');

/**
* Base Generator for LoopBack 4
*/
Expand All @@ -28,11 +31,206 @@ module.exports = class BaseGenerator extends Generator {
* Subclasses can extend _setupGenerator() to set up the generator
*/
_setupGenerator() {
this.option('config', {
type: String,
alias: 'c',
description: 'JSON file name or value to configure options',
});

this.option('yes', {
type: Boolean,
alias: 'y',
description:
'Skip all confirmation prompts with default or provided value',
});

this.artifactInfo = this.artifactInfo || {
rootDir: 'src',
};
}

/**
* Read a json document from stdin
*/
async _readJSONFromStdin() {
if (process.stdin.isTTY) {
this.log(
chalk.green(
'Please type in a json object line by line ' +
'(Press <ctrl>-D or type EOF to end):',
),
);
}

try {
const jsonStr = await readTextFromStdin();
return JSON.parse(jsonStr);
} catch (e) {
if (!process.stdin.isTTY) {
debug(e, jsonStr);
}
throw e;
}
}

async setOptions() {
let opts = {};
const jsonFileOrValue = this.options.config;
try {
if (jsonFileOrValue === 'stdin' || !process.stdin.isTTY) {
this.options['yes'] = true;
opts = await this._readJSONFromStdin();
} else if (typeof jsonFileOrValue === 'string') {
const jsonFile = path.resolve(process.cwd(), jsonFileOrValue);
if (fs.existsSync(jsonFile)) {
opts = this.fs.readJSON(jsonFile);
} else {
// Try parse the config as stringified json
opts = JSON.parse(jsonFileOrValue);
}
}
} catch (e) {
this.exit(e);
return;
}
if (typeof opts !== 'object') {
this.exit('Invalid config file or value: ' + jsonFileOrValue);
return;
}
for (const o in opts) {
if (this.options[o] == null) {
this.options[o] = opts[o];
}
}
}

/**
* Check if a question can be skipped in `express` mode
* @param {object} question A yeoman prompt
*/
_isQuestionOptional(question) {
return (
question.default != null || // Having a default value
this.options[question.name] != null || // Configured in options
question.type === 'list' || // A list
question.type === 'rawList' || // A raw list
question.type === 'checkbox' || // A checkbox
question.type === 'confirm'
); // A confirmation
}

/**
* Get the default answer for a question
* @param {*} question
*/
async _getDefaultAnswer(question, answers) {
let def = question.default;
if (typeof question.default === 'function') {
def = await question.default(answers);
}
let defaultVal = def;

if (def == null) {
// No `default` is set for the question, check existing answers
defaultVal = answers[question.name];
if (defaultVal != null) return defaultVal;
}

if (question.type === 'confirm') {
return defaultVal != null ? defaultVal : true;
}
if (question.type === 'list' || question.type === 'rawList') {
// Default to 1st item
if (def == null) def = 0;
if (typeof def === 'number') {
// The `default` is an index
const choice = question.choices[def];
if (choice) {
defaultVal = choice.value || choice.name;
}
} else {
// The default is a value
if (question.choices.map(c => c.value || c.name).includes(def)) {
defaultVal = def;
}
}
} else if (question.type === 'checkbox') {
if (def == null) {
defaultVal = question.choices
.filter(c => c.checked && !c.disabled)
.map(c => c.value || c.name);
} else {
defaultVal = def
.map(d => {
if (typeof d === 'number') {
const choice = question.choices[d];
if (choice && !choice.disabled) {
return choice.value || choice.name;
}
} else {
if (
question.choices.find(
c => !c.disabled && d === (c.value || c.name),
)
) {
return d;
}
}
return undefined;
})
.filter(v => v != null);
}
}
return defaultVal;
}

/**
* Override the base prompt to skip prompts with default answers
* @param questions One or more questions
*/
async prompt(questions) {
// Normalize the questions to be an array
if (!Array.isArray(questions)) {
questions = [questions];
}
if (!this.options['yes']) {
if (!process.stdin.isTTY) {
const msg = 'The stdin is not a terminal. No prompt is allowed.';
this.log(chalk.red(msg));
this.exit(new Error(msg));
return;
}
// Non-express mode, continue to prompt
return await super.prompt(questions);
}

const answers = Object.assign({}, this.options);

for (const q of questions) {
let when = q.when;
if (typeof when === 'function') {
when = await q.when(answers);
}
if (when === false) continue;
if (this._isQuestionOptional(q)) {
const answer = await this._getDefaultAnswer(q, answers);
debug('%s: %j', q.name, answer);
answers[q.name] = answer;
} else {
if (!process.stdin.isTTY) {
const msg = 'The stdin is not a terminal. No prompt is allowed.';
this.log(chalk.red(msg));
this.exit(new Error(msg));
return;
}
// Only prompt for non-skipped questions
const props = await super.prompt([q]);
Object.assign(answers, props);
}
}
return answers;
}

/**
* Override the usage text by replacing `yo loopback4:` with `lb4 `.
*/
Expand Down Expand Up @@ -96,7 +294,10 @@ module.exports = class BaseGenerator extends Generator {
*/
end() {
if (this.shouldExit()) {
debug(this.exitGeneration);
this.log(chalk.red('Generation is aborted:', this.exitGeneration));
// Fail the process
process.exitCode = 1;
return false;
}
return true;
Expand Down
6 changes: 4 additions & 2 deletions packages/cli/lib/project-generator.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,9 @@ module.exports = class ProjectGenerator extends BaseGenerator {
this.registerTransformStream(utils.renameEJS());
}

setOptions() {
async setOptions() {
await super.setOptions();
if (this.shouldExit()) return false;
if (this.options.name) {
const msg = utils.validate(this.options.name);
if (typeof msg === 'string') {
Expand Down Expand Up @@ -148,6 +150,7 @@ module.exports = class ProjectGenerator extends BaseGenerator {

return this.prompt(prompts).then(props => {
Object.assign(this.projectInfo, props);
this.destinationRoot(this.projectInfo.outdir);
});
}

Expand Down Expand Up @@ -187,7 +190,6 @@ module.exports = class ProjectGenerator extends BaseGenerator {

scaffold() {
if (this.shouldExit()) return false;
this.destinationRoot(this.projectInfo.outdir);

// First copy common files from ../../project/templates
this.fs.copyTpl(
Expand Down
Loading