- Introduction
- Development installation
- Running tests
- Code organization
- Commands
- Logging
- Okapi Client
- Plugins
- Documentation
- Debugging
- Releasing
The Stripes CLI is a command-line interface that runs using Node. It enhances the default build
and serve
operations found within stripes-core's Node API. It does this by modifying the Webpack configuration as needed.
Stripes CLI uses the Yargs framework for defining commands and Inquirer for accepting interactive input. In addition to providing a convention for defining commands and options, Yargs offers great built-in help.
To develop Stripes CLI, first clone the repo and then yarn install
its dependencies:
$ git clone https://github.com/folio-org/stripes-cli.git
$ cd stripes-cli
$ yarn install
Then create a link to lib/stripes-cli.js
in your path so stripes can easily be run from anywhere.
$ ln -s ./lib/stripes-cli.js /usr/local/bin/stripes
The CLI's tests use Mocha, Chai, and Sinon. Run the test with the test
script:
$ yarn test
The main CLI directories:
stripes-cli
├─doc Documentation
├─resources Workspace template files
├─test CLI tests
└─lib
├─cli CLI context, middleware, and common logic
├─commands Command handlers
├─okapi Okapi services and http client
└─platform Platform generation logic
NOTE: Template files for creating new UI Modules via app create
and setting up BigTest via app bigtest
are retrieved from https://github.com/folio-org/ui-app-template
All commands are organized in the lib/commands
directory. A command consists of Yargs command module that exports:
command
- String of the command name and any positional argumentsdescribe
- Description of the commandbuilder
- Function accepting and returning a Yargs instance for defining options, examples, and applying middlewarehandler
- Function invoked with a parsedargv
to perform the command
Example command:
// The command itself
function myCommand(argv) {
console.log(`Hello ${argv.name}!`);
}
// Yargs command module with a builder function
module.exports = {
command: 'hello',
describe: 'A very basic command',
builder: (yargs) => {
yargs
.option('name', {
describe: 'A name to say hello to',
type: 'string',
})
.example('$0 hello --name folio', 'Say hello to "folio".');
},
handler: myCommand,
};
Complex logic or logic consumed by more than one command should be kept in separate modules. Although not a strict requirement, try to limit user input and output to the command handler itself. This allows the work to be shared in different contexts where the messaging may differ across commands or use-cases.
Options are defined using Yargs option syntax.
Example:
port: {
type: 'number',
describe: 'Development server port',
default: 3000,
group: 'Server Options:',
},
Useful settings include:
type
- option type (string, boolean, number, array)describe
- description for helpdefault
- value when the option is not providedgroup
- grouping in the help outputchoices
- limit validation to predefined valuesconflicts
- options that must not be set with this one
At minimum, include type
and describe
properties for all options help populate the CLI's built-in help output and command refrence. See the Yargs .options API documentation for all available settings.
In the command's builder, apply options with .option()
:
command: 'hello',
builder: (yargs) => {
yargs
.option('name', {
describe: 'A name to say hello to',
type: 'string',
});
},
handler: myCommand,
Options used in more than one command should be kept in lib/commands/common-options
. Organize and export them in logical groupings, then import the desired options in each command. Doing so consolidates the option metadata, so option descriptions and types remain consistent across the application. When assigning multiple options at a time, pass a single object to .options()
with Object.assign()
.
builder: (yargs) => {
.options(Object.assign({}, okapiOptions, serverOptions);
},
Positional arguments are defined similar to options via the command builder. However, they should also include a reference within the command:
value to define their order. Required positionals are in the form <name>
while optional positionals use [name]
. See the Yargs positional documentation for more information.
command: 'hello <name>',
builder: (yargs) => {
yargs
.positional('name', {
describe: 'A name to say hello to',
type: 'string',
});
},
handler: myCommand,
The CLI supports middleware for additional handling of argv
prior to invoking a command. One or more middleware functions can applied to the Yargs builder. See the Yargs middleware documentation for more details.
Several useful middleware functions are included with the CLI for loading context, parsing standard input, and prompting the user for input.
The CLI can provide a context for each command which denotes whether the command has been run from a UI module, platform, or workspace directory. This is helpful for performing operations specific to specific contexts. To access to this information in your command, apply the contextMiddleware
to your command builder. The result will be applied to argv.context
.
// Lazy load to improve startup time
const importLazy = require('import-lazy')(require);
const { contextMiddleware } = importLazy('../cli/context-middleware');
// The command itself
function myCommand(argv) {
if(argv.context.isUiModule) {
console.log(`Hello from the module ${argv.context.moduleName}!`)
} else {
console.log(`Hello from somewhere else!`);
}
}
// Yargs command module with a builder function
module.exports = {
command: 'hello',
describe: 'A very basic command',
builder: (yargs) => {
yargs
.middleware([
contextMiddleware(), // <--- middleware
])
.example('$0 hello', 'Say hello to from context.');
},
handler: myCommand,
};
Use the stripesConfigMiddleware
when a Stripes tenant configuration needs to be accessed within a command. This middleware will load the configuration from file (typically stripes.config.js
, but .json
is also supported) when --configFile
is specified on the command-line. Alternatively, the stripes configuration can be read from stdin if no --configFile
is specified and a JSON string is piped into the command. The stripes configuration, whether by file or stdin, is made available to the command as argv.stripesConfig
.
When using stripesConfigMiddleware
, apply stripesConfigFile
and/or stripesConfigStdin
options from common-options
. This will ensure both configFile
and stripesConfig
are reported consistently in the help. Commands consuming a config file, typically accept configFile
as a positional option.
// Lazy load to improve startup time
const importLazy = require('import-lazy')(require);
const { stripesConfigMiddleware } = importLazy('../cli/stripes-config-middleware');
const { stripesConfigFile, stripesConfigStdin } = importLazy('./common-options');
// The command itself
function myCommand(argv) {
console.log(`Hello ${argv.configFile}`); // <--- filename
console.log(argv.stripesConfig); // <--- config object
}
// Yargs command module with a builder function
module.exports = {
command: 'hello [configFile]', // <---- indicate placement of positional
describe: 'A very basic command',
builder: (yargs) => {
yargs
.middleware([
stripesConfigMiddleware(), // <--- middleware
])
.positional('configFile', stripesConfigFile.configFile) // .positional() does not accept an object
.options(stripesConfigStdin); // .options() will accept an object
.example('$0 hello stripes.config.js', 'Say hello to a stripes configuration.');
},
handler: myCommand,
};
To accept standard input (stdin) within a command, apply one of the CLI's stdin middleware handlers from lib/cli/stdin-middleware.js
. Available stdin middleware include stdinStringMiddleware
, stdinArrayMiddleware
, and stdinJsonMiddleware
for parsing string, array, and JSON input. The stdinArrayMiddleware
splits on whitespace, including line breaks, to make accepting multi-line input easy.
Each of the CLI's stdin middleware accept a key and return the middleware function for use by Yargs. When the middleware is invoked, stdin will be parsed and, if available, assigned to the specified option key. From within the command, simply access the value as you would any other option.
For example, the following will assign stdin
, parsed as an string, to the name
option. For consistency, include "(stdin)" in your option's description to surface this consistently in the CLI's generated documentation.
// Lazy load to improve startup time
const importLazy = require('import-lazy')(require);
const { stdinStringMiddleware } = importLazy('../cli/stdin-middleware');
// The command itself
function myCommand(argv) {
console.log(`Hello ${argv.name}!`);
}
// Yargs command module with a builder function
module.exports = {
command: 'hello',
describe: 'A very basic command',
builder: (yargs) => {
yargs
.middleware([
stdinStringMiddleware('name'), // <--- provide the option key to assign stdin to
])
.option('name', {
describe: 'A name to say hello to (stdin)', // <--- include "(stdin)" in the description for the doc generator
type: 'string',
})
.example('$0 hello --name folio', 'Say hello to "folio".');
.example('echo folio | $0 hello', 'Say hello to "folio" with stdin.');
},
handler: myCommand,
};
When answers to questions can be acquired up front, the simplest way to ask for them is to apply the CLI's promptMiddleware
from lib/cli/prompt-middleware
. When invoked, this middleware will check the incoming argv
prompt the user for any options which were not provided on the command line. Internally the middleware uses Inquirer to prompt the user with questions.
This example will prompt for a name before running command's hander:
// Lazy load to improve startup time
const importLazy = require('import-lazy')(require);
const { promptMiddleware } = importLazy('../cli/prompt-middleware');
// The command itself
function myCommand(argv) {
console.log(`Hello ${argv.name}!`);
}
// Used to share option details between promptMiddleware and yargs builder
const myOptions = {
name: {
describe: 'A name to say hello to',
type: 'string',
}
}
// Yargs command module with a builder function
module.exports = {
command: 'hello',
describe: 'A very basic command',
builder: (yargs) => {
yargs
.middleware([
promptMiddleware(myOptions), // <--- provide object of option(s) for user prompts
])
.option('name', myOptions.name)
.example('$0 hello', 'Prompt for name and then say hello.');
},
handler: myCommand,
};
Yargs options and Inquirer questions do not have fully compatible structures. When a CLI option is also used as an interactive question, avoid duplication by using the CLI's yargsToInquirer()
helper. This is automatically invoked by promptMiddleware
.
Any Inquirer question settings that do not have a Yargs option equivalent can be defined in an inquirer
property. In the following example, Yargs has no equivalent for the password
type or mask
setting. The yargsToInquirer()
helper will apply any inquirer-specific options after conversion.
password: {
type: 'string',
describe: 'Okapi tenant password',
group: 'Okapi Options:',
inquirer: {
type: 'password',
mask: '*',
},
},
Related commands can be grouped together using directories. To do this create a directory to contain the related commands and create a command to reference the directory.
Here we have mod.js
the command, and mod
the directory:
stripes-cli
└─lib
└─commands
├─mod.js
└─mod
├─add.js
├─remove.js
├─update.js
├─enable.js
└─disable.js
Using Yarg's .commandDir()
, the command instructs Yargs to retrieve all commands found in the mod
directory. No handler is necessary if mod
does nothing on its own.
module.exports = {
command: 'mod <command>',
describe: 'Commands to manage UI module descriptors',
builder: yargs => yargs.commandDir('mod'),
handler: () => {},
};
The resulting commands from above are all accessible by mod
followed by the command name. This gives the appearance of sub-commands under mod
. For example:
$ stripes mod add
$ stripes mod remove
Yargs will surface descriptions for each command in the mod
directory with the help output for stripes mod --help
.
Logging is instrumented with the debug utility. All logs within the CLI pass through lib/cli/logger.js
, a wrapper around debug
, to ensure proper namespace assignment.
To add a logger to code, require and invoke it:
const logger = require('./cli/logger')();
logger.log('a message');
Optionally, pass the name of a feature or category when invoking the logger. This is useful for filtering log output.
const okapiLogger = require('./cli/logger')('okapi');
okapiLogger.log('a message about Okapi');
See debugging below for details on viewing log output.
TODO: Document
The CLI can be extended with plugins. Plugins provide a means for the user to perform custom logic, possibly altering the Webpack configuration prior to invoking a Webpack build. They are defined in a .stripesclirc.js
configuration file.
To create a plugin, define a plugins
object in .stripesclirc.js
which contains keys representing each command that is receiving a plugin. In this example, a plugin has been defined for serve
:
module.exports = {
port: 8080,
plugins: {
serve: servePlugin,
},
};
The value should be an object containing beforeBuild
and, optionally, options
.
beforeBuild
is a function that will be passed the command's parsedargv
. It should return a function that will be passed Webpack config processed by the CLI. This gives the opportunity for the plugin to inspect or modify the config prior to running Webpack.options
define additional Yargs options for the command. When provided, options will be validated and included in the command help along the CLI's built-in options.
const servePlugin = {
options: {
example: {
describe: 'This will show up in the help',
type: 'string',
},
},
beforeBuild: (argv) => {
return (config) => {
// Chance to inspect or modify the config based on argv...
return config;
};
},
}
The best way to document the CLI is within each Yargs command module. Be sure to include a description for the command, options, and positionals. Include type
for options and positionals.
Group options where it make sense using the group
property. This breaks out options in the help for readability. Custom option groups should end with the word "Options:", such as "Server Options:", in order to be picked up by the CLI document generator.
module.exports.serverOptions = {
port: {
type: 'number',
describe: 'Development server port',
default: 3000,
group: 'Server Options:',
},
// ...
}
Note: If your command is a work in progress, experimental, or has an interface that is likely to change, include "(work in progress)" in the description. This will be highlighted in the command documentation and TOC.
Add one or more examples on how to use the command by calling .example()
in the Yargs builder. $0
within the example string is replaced by the script name (stripes) in the help output:
builder: (yargs) => {
yargs
.example('$0 hello', 'Say hello')
.example('$0 hello --name folio', 'Say hello to "folio".');
// ...
},
After creating a new command or updating an existing one, be sure to update docs/commands.md
, the CLI's command reference. This process is automated by the lib/doc/generator.js
script. To update it, run:
$ yarn docs
This will traverse the CLI's commands gathering all the --help
text and parsing to write as markdown. The generated markdown help is then applied to the docs/commands-template.md
. If changes are needed to the introduction or footer, update docs/commands-template.md
before running yarn docs
.
Note: Review the generated changes with the actual help output checking for unexpected additions or omissions. These may be a sign that the command reference was not updated recently or that Yargs has changed its help text formatting. If it appears to be the later, review docs/yargs-help-parser.js
for corrections.
When updating documentation like this dev-guide.md or the user-guide.md, keep the table of contents (TOC) updated as well. The TOC will need updating anytime a heading is added, removed, or changed. When modifying an existing heading, kee.
The Okapi repository has a handy script, md2toc, to help with maintaining the TOC. In most cases the -l 2
option will apply. For example, the following will generate a TOC for which can then be applied to this document.
$ perl ../okapi/doc/md2toc -l 2 ./doc/dev-guide.md
Stripes-CLI implements debug for diagnostic logging. This can be a useful starting point to diagnose errors.
Debug output is enabled by setting the DEBUG
environment variable. The value of DEBUG
is a comma-separated list of namespaces you wish to view debug output for. By convention, namespaces match the supporting package name. Features within a namespace may be separated by a colon. The wildcard *
is supported. For Windows, replace export
with set
in the examples below.
For example, to view all stripes-cli debug logs:
$ export DEBUG=stripes-cli*
To view only the cli's calls to Okapi:
$ export DEBUG=stripes-cli:okapi
To view all stripes-cli and stripes-core debug logs:
$ export DEBUG=stripes-cli*,stripes-core*
Alternatively set the wildcard on stripes:
$ export DEBUG=stripes*
It is also possible set the wildcard for all namespaces:
$ export DEBUG=*
Note: The above will enable logging for all packages that happen to be instrumented with debug
, including express
and babel
.
Some of the available diagnostic output can be lengthy. The debug
utility writes to stderr, so if you would like to send this content in a file, you can do so with:
$ export DEBUG=stripes*
$ stripes serve 2> file.log
Included in the Stripes-CLI repository is a Visual Studio Code launch.json
configuration which makes debugging a command or Stripes build easy. This file contains the debug configuration of several sample CLI commands as well as the CLI's own unit tests.
Example configuration:
{
"type": "node",
"request": "launch",
"name": "Serve from PLATFORM",
"program": "${workspaceFolder}/lib/stripes-cli.js",
"args": [ "serve", "stripes.config.js"],
"cwd": "${workspaceFolder}/../stripes-sample-platform"
},
Pay careful attention to the current working directory, cwd
, defined for each configuration as this may not match an app or platform on your current system. Modify the cwd
to a suitable (and often temporary) path. This will be the path in which the CLI is invoked from via VSCode. It is necessary for determining proper context.
Modify the args
property to include the command name and any command options desired. For options, separate out the key from the value. For example, --user diku_admin
will have two entries in the array, --user
and diku_admin
.
"args": ["perm", "create", "module.hello-world.enabled", "--push", "--user", "diku_admin"],
To debug with VSCode, set a breakpoint on the desired command or unit test. For CLI commands, it is often best to start at the top of the handler, for example, in lib/commands/serve.js
. Next, from the debug menu, select the appropriate configuration and click play.
In situations where the handler is not invoked as expected, check your input in args
. Also, try adding --no-interactive
to ensure the debugger is not improperly handling interactive input. You can always set the breakpoint in lib/stripes-cli.js
as the very first point of entry.
The version of stripes-core in use by the CLI could vary depending on your CLI install, app, platform, or workspace configuration. The easiest way to ensure your stripes-core breakpoints will be hit properly is to initiate debugging in the CLI using the Stripes Serve from PLATFORM
or Stripes Serve from APP
configuration. Set your breakpoint at the end of the serve
command handler where the stripes-core API, stripes.api.serve(...)
, is invoked.
From there, simply step into the stripes-core code. VSCode will open the version of stripes-core in use. Once a stripes-core file is open, inspect its path, then open and set breakpoints on any other desired files found within the stripes-core's webpack
directory.
To release Stripes-CLI, follow the general Stripes release procedure. The only CLI-specific addition is to make sure the command reference has been regenerated before tagging. Do this after bumping the version number so the correct version is reflected in the generated documentation.