-
Notifications
You must be signed in to change notification settings - Fork 1.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Enable json config options and non-interactive mode #1384
Conversation
packages/cli/lib/base-generator.js
Outdated
@@ -21,11 +22,113 @@ module.exports = class BaseGenerator extends Generator { | |||
* Subclasses can extend _setupGenerator() to set up the generator | |||
*/ | |||
_setupGenerator() { | |||
this.option('json', { | |||
type: String, | |||
description: 'Stringified JSON object to configure options', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It makes me wonder how practical it is to pass JSON strings via exec arguments. IIRC, there are limitations on the maximum supported length - e.g. Linux limits the length to ~100KB according to this post, Windows has a limit of 32KB according to this post.
In my experience, CLI programs usually accept large content from stdin stream.
I am proposing to slightly different set of CLI options and behavior:
# Read JSON instructions from the given file
$ lb4 app --json commands.json
# No file was provided, read instruction from stdin
$ cat commands.json | lb4 app --json
Thoughts?
packages/cli/lib/base-generator.js
Outdated
this.option('express', { | ||
type: Boolean, | ||
description: | ||
'Run the generator in express mode to skip optional questions', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When I first saw the name of the options, I assumed express
would be related to Express.js framework.
Can we find a better option name please, for example --non-interactive
or --skip-prompts
?
9a48834
to
d08ae6f
Compare
274cf05
to
c96b219
Compare
db3600e
to
f88f1d9
Compare
f88f1d9
to
ce31b72
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Besides my comments below, please add documentation explaining how to use the new express/non-interactive mode. Ideally, the pull request description should be updated to match the actual implementation too.
* keyword 'loopback' under 'keywords' attribute in package.json. | ||
* 'keywords' is an array | ||
*/ | ||
checkLoopBackProject() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't we already have another implementation of checkLoopBackProject
? I think @virkt25 was using one in his datasource work. Let's create a single implementation of this check and move it to a place where all generators can import it from.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, all the artifact generators call super.checkLoopBackProject()
which is implemented in BaseGenerator
which is what this generator (ArtifactGenerator
) extends. This implementation seems to be the same as the one found in the base generator so not sure why it's duplicated.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@raymondfeng PTAL ☝️
packages/cli/lib/base-generator.js
Outdated
if (process.stdin.isTTY) { | ||
this.log( | ||
chalk.green( | ||
'Please type in a json object line by line ' + |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Uh oh, do you really expect people to type JSON by hand? What if they make a typo, e.g. forget to wrap a key in double quotes - how are they going to fix the type? Will they have to re-type everything again?
In my opinion, adding readline support is overkill. The JSON mode is intended for machines, humans should use interactive prompt mode.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@raymondfeng PTAL ☝️
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm thinking about copy-paste
of json objects into stdin
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please also note the readline
support is needed for unix pipe or redirection, for example:
cat config.json | lb4 app --config stdin
lb4 app --config stdin < config.json
lb4 app --config stdin <<EOF
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The generator needs to read from stdin
- which is the source of input from unix pipes. The readline
module is used to enable that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In Node.js, process.stdin
is a regular stream that you can read the input from. No need to wrap it in readline
, unless I am missing something specific to how Yeoman modifies stdin.
A typical solution for reading all input from stdin into a string is to pipe stdin to concat-stream.
A mock-up:
const concat = require('concat-stream');
// NOTE: returns Promise<Buffer>, not Promise<string>!
function readStream(src) {
return new Promise((resolve, reject) => {
const concatStream = concat(resolve);
src('error', reject);
src.pipe(concatStream);
});
}
// usage
const rawJsonBuffer = await readStream(stdin);
const data = JSON.parse(rawJsonBuffer);
// fortunately, JSON.parse works with buffers too
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, that's just a different way of implementing readJSONFromStdin. Is leveraging readline
to read from stdin a bad idea?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The other option is the use the following code:
function readBufferFromStdin() {
const stdin = process.stdin;
const data = [];
let len = 0;
return new Promise((resolve, reject) => {
stdin.on('readable', () => {
let chunk;
while ((chunk = stdin.read())) {
data.push(chunk);
len += chunk.length;
}
});
stdin.on('end', () => {
resolve(Buffer.concat(data, len));
});
stdin.on('error', err => {
reject(err);
});
});
};
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, that's just a different way of implementing readJSONFromStdin. Is leveraging readline to read from stdin a bad idea?
I suppose using readline
is not a bad idea per se. My main objection is against the complexity of the current implementation of _readJSONFromStdin
. To me, the currently proposed code requires maintainers to know a lot about readline, TTY, SIGINT, etc. and the way how all things are wired together.
Compare it with my proposed solution that's just 5 lines of relatively straightforward code.
return new Promise((resolve, reject) => {
const concatStream = concat(resolve);
src.on('error', reject);
src.pipe(concatStream);
});
Your second example, using ReadableStream directly, looks better to me than the original readline-based one. But why to re-implement reading a stream into a buffer when concat-stream
module already does it well? Dealing with streams is tricky and it's very easy to introduce subtle bugs that are difficult to spot by casual readers.
Not a big deal though. If you prefer a hand-written readBufferFromStdin
then I am ok with that too.
packages/cli/lib/base-generator.js
Outdated
q.type === 'confirm'; // A confirmation | ||
|
||
// Get the default answer for a question | ||
const defaultAnswer = async q => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
async function defaultAnswer(q) {
// ...
}
The implementation of this function is way too long, please refactor it by extracting bits and pieces into standalone functions.
packages/cli/lib/base-generator.js
Outdated
return defaultVal != null ? defaultVal : true; | ||
} | ||
if (q.type === 'list' || q.type === 'rawList') { | ||
// Default to 1st item |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Handling of list
/rawList
is a prime candidate to be extracted into a new function.
packages/cli/lib/base-generator.js
Outdated
} | ||
} | ||
} else if (q.type === 'checkbox') { | ||
if (def == null) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ditto here - please extract checkbox
case into a function.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great PR 👏 ! I'm excited to see this PR land in the CLI :D -- Just a few quick comments but looks great otherwise.
* keyword 'loopback' under 'keywords' attribute in package.json. | ||
* 'keywords' is an array | ||
*/ | ||
checkLoopBackProject() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, all the artifact generators call super.checkLoopBackProject()
which is implemented in BaseGenerator
which is what this generator (ArtifactGenerator
) extends. This implementation seems to be the same as the one found in the base generator so not sure why it's duplicated.
packages/cli/lib/base-generator.js
Outdated
if (process.stdin.isTTY) { | ||
this.log( | ||
chalk.green( | ||
'Please type in a json object line by line ' + |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1
packages/cli/lib/base-generator.js
Outdated
|
||
this.option('skip-optional-prompts', { | ||
type: Boolean, | ||
alias: 'b', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is the alias for this prompt b
? I'd image maybe s
for skip?
packages/cli/lib/base-generator.js
Outdated
description: 'JSON file to configure options', | ||
}); | ||
|
||
this.option('skip-optional-prompts', { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure how I feel about a such a long flag name from a UX perspective. Can we consider a simpler / easier to type / use / remember flag such as --skip
or --skip-optional
or --express
(but may be confusing with Express framework) or --quick
or --defaults
... anything smaller than the current flag.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was proposing --non-interactive
, but that name has a slightly different meaning.
To be honest, I don't see much value in --skip-default-prompts
unless it's used with the JSON mode. I am proposing to merge these two modes together and use the --config
option to drive the behavior.
- Answers are read from JSON - use non-interactive mode (skip default prompts)
- Answers are read from UI prompts - use interactive mode (ask all prompts)
That way there is only one new CLI option to add (--config
).
9b49a49
to
041b5d2
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FWIW, there are few older comments waiting to be addressed or discussed.
* keyword 'loopback' under 'keywords' attribute in package.json. | ||
* 'keywords' is an array | ||
*/ | ||
checkLoopBackProject() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@raymondfeng PTAL ☝️
packages/cli/lib/base-generator.js
Outdated
if (process.stdin.isTTY) { | ||
this.log( | ||
chalk.green( | ||
'Please type in a json object line by line ' + |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@raymondfeng PTAL ☝️
The |
041b5d2
to
af291b3
Compare
4718144
to
aaab42d
Compare
After switching to stream-based implementation of reading stdin, the following error is thrown:
I'm not sure how to fix it at the moment. BTW, the stream based approach needs to deal with |
I was able to get around the issue using the following code after reading /**
* Read string from stdin
*/
exports.readStringFromStdin = function(log) {
const stdin = process.stdin;
if (stdin.isTTY && log) {
log(
chalk.green('Please type in a json object ' + '(Press <ctrl>-D to end):'),
);
}
const data = [];
let len = 0;
function onData(chunk) {
data.push(chunk);
len += chunk.length;
}
// Restore stdin
function close() {
stdin.removeListener('data', onData);
stdin.pause(); // Pause the stdin so that other prompts can happen
}
return new Promise((resolve, reject) => {
// Set up `data` handler to make it compatible with mock-stdin which is
// an old style stream which does not emit `readable` events
stdin.on('data', onData);
stdin.once('end', () => {
const buf = Buffer.concat(data, len);
close();
resolve(buf.toString('utf-8'));
});
stdin.once('error', err => {
close();
reject(err);
});
});
}; @bajtos Do you really think this is better/simpler than the current code using |
Bummer
Yeah, I agree with you that it's more robust to keep the readline-based implementation then. Thank you for looking under the hood and helping us both to better understand design constraints. To me, this is another argument for not using yeoman (see #844). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks mostly good.
Please address my comments below. Feel free to get somebody else (@virkt25?) to give a final approval for this pull request, so that you don't have to wait for me in case I am not around.
packages/cli/lib/base-generator.js
Outdated
if (err) return; | ||
const jsonStr = lines.join('\n'); | ||
try { | ||
const json = JSON.parse(jsonStr); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please move handling of the input outside of the code dealing with stdin
.
Ideally, I'd like to see something like the following in _readJSONFromStdin
:
async _readJSONFromStdin() {
const input = await readFromStdin();
try {
return JSON.parse(input);
} catch (err) {
if (!process.stdin.isTTY) {
debug(err, jsonStr);
}
throw err;
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed. Refactor the function into utils.
packages/cli/lib/base-generator.js
Outdated
this.log( | ||
chalk.red('The stdin is not a terminal. No prompt is allowed.'), | ||
); | ||
process.exit(1); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't exit the process please, it disrupts mocha
tests. Call this.exit
or throw new Error
instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
packages/cli/lib/base-generator.js
Outdated
this.log( | ||
chalk.red('The stdin is not a terminal. No prompt is allowed.'), | ||
); | ||
process.exit(1); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ditto. Don't exit the process, end the generator execution only.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
aaab42d
to
e73a949
Compare
packages/cli/lib/utils.js
Outdated
if (process.stdin.isTTY && log) { | ||
log( | ||
chalk.green( | ||
'Please type in a json object line by line ' + |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In my view, this message does not belong to readTextFromStdin
as readTextFromStdin
can be used to read arbitrary text, not only JSON.
I am proposing to move it back to _readJSONFromStdin
and also remove the no-longer used log
argument.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Apart from the one comment left by @bajtos, the changes look good to me. Excited to see this PR land. :D
- Allow generator options to be passed in json format from a file, stdin, or stringified value - Allow prompts to be skipped if a prompt has default value or the corresponding anwser exists in options
e73a949
to
662696c
Compare
The PR adds the following features:
--config
from a json file, stdin, or stringified value--yes
option so that optional questions will be skipped with default valuesFor example,
Checklist
npm test
passes on your machinepackages/cli
were updatedexamples/*
were updated