faber-cli
v0.2.0
Published
A CLI to easily reuse your own configurable boilerplate projects.
Readme
Faber CLI
Faber is a CLI that helps you create/scaffold new projects using custom boilerplates.
You can prepare your own boilerplates to make them configurable for creating new projects with Faber, and pass custom parameters, data or actions to execute in the scaffolding of your new project.
Summary
Getting Started
Requirements
To use the Faber CLI, you will need Node.js and NPM on your machine.
Your project doesn't need to use Node.js for you to use Faber with it. You can use it on boilerplates with any kind of framework or stack. Once the project is created, Faber's work is done.
However, for being a JS library, Faber configurations are currently written with JavaScript.
Installation
Install the CLI globally on your machine with:
npm install -g faber-cliDemo
For having a quick demonstration of how Faber is used, try the faber-demo example repository.
Usage
Before diving into details, here is a quick overview of how you can use Faber on your projects:
If you want to prepare a boilerplate for using Faber:
- Create a faberconfig file at the root of your boilerplate repository;
- Write the actions to be executed on the boilerplate when creating a new project with it;
- Prepare the data you want to use in your actions;
- Test your actions with the
faber executeCLI command.
Make sure you stage your changes in Git before running your actions. Learn more.
If you want to use an existing boilerplate to create a new project:
- Prepare the data you want to use for your project as a minified JSON;
- Choose a repository of a boilerplate to use as a template;
- Use the
faber createCLI command to bootstrap a new project.
Configuring faberconfig
To use Faber CLI in a boilerplate, you need to create a faberconfig.js file at the root of your boilerplate project.
Usually, this file uses the .js extension, but depending on your preferences and your project settings (package.json, if existing on the project's root) you might want to use a different extension.
To use CommonJS, the file must be either:
.jswithtype: "commonjs"in yourpackage.json;.cjswith anytypein yourpackage.json..jswithout having apackage.jsonin the root;
To use ESM, the file must be either:
.jswithtype: "module"in yourpackage.json;.mjswith anytypein yourpackage.json.
See below a basic example using CommonJS and ESM.
CommonJS
module.exports = function (faber) {
faber.setActions((data) => {
return [
// Add your actions here
// ...
];
});
};ESM
export default (faber) => {
faber.setActions((data) => {
return [
// Add your actions here
// ...
];
});
};Passing Data
During the faber create and faber execute commands, the CLI asks for a minified (optionally encoded) JSON data. This data is passed to the setActions() function, allowing you to use it in the actions.
Here is an example of JSON data:
{
"name": "My Project",
"client": "The Client",
"isMultilanguage": false
}Due to the nature of how terminals (command prompts) work, there are some limitations on how to pass the data to the CLI. See below how to do it properly.
Encoded JSON (recommended)
The most reliable way of passing data is using a Base64 encoded JSON. Encoding guarantees the content consistency while working with any method of passing data to Faber.
You can encode the JSON to Base64 how you prefer, or use our online JSON encoder that works seamlessly with Faber.
Here is an example of a Base64 encoded JSON:
eyJuYW1lIjoiTXkgUHJvamVjdCIsImNsaWVudCI6IlRoZSBDbGllbnQiLCJpc011bHRpbGFuZ3VhZ2UiOmZhbHNlfQ==Tip: Minifying the JSON before encoding it helps generate a smaller string.
Minified JSON (less reliable)
Although we encourage using the encoding approach, you can also pass a minified JSON directly. It might be easier than encoding depending on your workflow, but has some caveats:
When asked by CLI
By default, the CLI will prompt you to paste the JSON data during its execution.
If not encoded, the JSON can be passed directly, as long as it does not contain line breaks, as in the example below:
$ Paste the project data: {"name":"My Project","client":"The Client","isMultilanguage":false}You can also use our online JSON encoder to minify it without encoding.
Using --data argument
If you prefer to pass the data directly in the terminal via command, you can use the --data argument, passing the JSON as value.
In this case, if not encoded, the JSON must be minified and then stringified.
However, some terminals might misinterpret JSONs that include spaces, or ignore escaped characters, which would break the JSON anyway. To avoid this, encoding would be a more reliable option.
Here is a usage example (this might or not work depending on your system):
faber create my-project --data "{\"name\":\"My Project\",\"client\":\"The Client\",\"isMultilanguage\":false}"You can use an online tool like jsonformatter.org to stringify the JSON after minifying it.
Reserved Properties
When using the create task, the name of the project passed as an argument for the command (i.e. faber create my-project) is added to the data object as the _dirName parameter. Similarly, when using the execute task, it gets the name of the folder where you are running the command.
{
_dirName: 'my-project';
}Actions
Actions are defined on the faberconfig file of the boilerplate using the faber.setActions() function.
You can use the project's data from the provided JSON (requested at faber create or faber execute commands) on any action.
See below the available actions that you can use:
Replace
Replaces text or patterns on files or glob patterns.
| Property | Type | Required | Description |
| -------- | --------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------- |
| type | String | Yes | Should be 'replace' for this action. |
| files | String,[String] | Yes | Path to the files where the replace should happen. It can be an array of paths, and can use glob patterns. |
| ignore | String,[String] | No | Path to the files where the replace shouldn't happen. It can be an array of paths, and can use glob patterns. |
| from | String,[String],RegExp,[RegExp] | Yes | Text(s) or pattern(s) to look for in the files. |
| to | String,[String],RegExp,[RegExp] | Yes | The replacement text(s). If array is provided, should match the same length as the from array. |
Usage examples
faber.setActions((data) => {
return [
// Replace all occurrences of a string in single file.
{
type: 'replace',
files: 'README.md',
from: 'PROJECT_NAME',
to: data.projectName,
},
// Replace occurrences of multiple strings in multiple files,
// using glob patterns, and defining paths to ignore.
{
type: 'replace',
files: ['README.md', 'package.json', 'src/*'],
ignore: ['src/node_modules', '.git'],
from: [/\bAUTHOR_NAME\b/g, /\bAUTHOR_URI\b/g],
to: [data.authorName, data.authorUri],
},
];
});Considerations
- When passing
stringin thefromproperty, it's automatically converted to a global regex to replace all occurrences instead of just the first one (JavaScript default). However, if aRegExpis passed, it is left unchanged, requiring thegflag to replace all occurrences (if wanted). - This action uses the replace-in-file package for the replacements. For more details about the
from,toandignoreparameters, please visit its documentation.
Move (or Rename)
Can be used to move or rename files and folders.
| Property | Type | Required | Description |
| -------- | ----------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| type | String | Yes | Should be 'move' for this action. |
| from | String,[String] | Yes | Path(s) to the source files or folders to move. |
| to | String,[String] | Yes | Destination path(s) to the files or folders to move or rename. If array is provided, should match the same length as the from array. |
Usage examples
faber.setActions((data) => {
return [
// Move a single file to another directory
{
type: 'move',
from: 'file.txt',
to: 'folder/file.txt',
},
// Rename a folder and move and rename a file
{
type: 'move',
from: ['folder', 'file.txt'],
to: [data.newFolderName, `dir/${data.newFileName}`],
},
];
});Considerations
- When moving a file to another directory, if the destination (
to) directory doesn't exist yet, it is created automatically. - If a file/folder with the destination (
to) name already exists, the existing one will be overriden. - If the source (
from) file/folder doesn't exist, an error is thrown. - This action uses the move-file package for moving/renaming files. Please visit its documentation if needed.
Delete
Deletes files or entire folders by defined paths or glob patterns.
| Property | Type | Required | Description |
| -------- | ----------------- | -------- | ----------------------------------------------------------------------------------------------- |
| type | String | Yes | Should be 'delete' for this action. |
| paths | String,[String] | Yes | Path to the files or folders to delete. It can be an array of paths, and can use glob patterns. |
Usage examples
faber.setActions((data) => {
return [
// Delete a single file
{
type: 'delete',
files: 'file.txt',
},
// Delete files and folders using glob pattern and variable
{
type: 'delete',
files: ['**/*.txt', data.folderToDelete],
},
];
});Considerations
- If the file/folder doesn't exist, it's just ignored.
- This action uses the del package for deleting files/folders. Please visit its documentation if needed.
Conditional
Update files' content based on conditional rules. Useful to keep/remove text according to conditions and the provided data.
| Property | Type | Required | Description |
| ------------ | ----------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| type | String | Yes | Should be 'conditional' for this action. |
| files | String,[String] | Yes | Path to the files where the conditional updates should happen. It can be an array of paths. |
| ignore | String,[String] | No | Path to the files where the replace shouldn't happen. It can be an array of paths, and can use glob pattern. |
| identifier | String | Yes | A token to identify the content to be kept/removed. |
| condition | Boolean | Yes | The condition to keep/remove the content. When the condition is true, the content is kept, and removed when false. However, when the block uses a negative token (!), the block is kept when the condition is false, and removed when true. |
Usage examples
faber.setActions((data) => {
return [
// Keep/Remove content on single file
{
type: 'conditional',
files: 'file.txt',
identifier: 'is-multilanguage',
condition: data.isMultiLanguage,
},
];
});On the file content, you can wrap the block of content to keep/remove with Faber conditional comments.
This is a file with instructions of the project.
<!-- @faber-if: is-multilanguage -->
This paragraph is only kept if the condition is truthy.
<!-- @faber-endif: is-multilanguage -->In the above example, the 2 lines between the @faber-if and @faber-endif HTML comments are kept when the condition to the is-multilanguage identifier is true, and deleted when the condition is false.
Negative match
This prints the /* @faber-if: !is-multisite */not /* @faber-endif: !is-multisite */word if the condition is falsy.In this example, by prefixing the identifier with an exclamation mark (!), we invert the rule, keeping the not word when the condition is false and deleting it when true.
Supported comments
Currently, you can use the following commenting styles for conditional replacements:
- Block comments:
<!---->/***//**/''''''""""""
- Line comments:
/////#
Considerations
- The conditional content is everything between the
@faber-if: identifierand@faber-endif: identifiercomments; - The identifier is required for both
@faber-ifand@faber-endiffor the action to work correctly; - There is no
@faber-elselogic yet; - The identifier is not a variable, it's just a string that links the conditional blocks to the configured action;
- You can have multiple conditional blocks using the same identifier;
- Prefix the identifier with an exclamation mark
!to keep the block when the condition isfalseinstead oftrue;
Run
Run shell commands.
| Property | Type | Required | Description |
| ---------- | ----------------- | -------- | -------------------------------------------------------------------------------------------- |
| type | String | Yes | Should be 'run' for this action. |
| commands | String,[String] | Yes | Command(s) to run sequentially. |
| silent | Boolean | No | If false, logs the command(s) output on the console. Default is true (omits the output). |
Usage examples
faber.setActions((data) => {
return [
// Runs a single command
{
type: 'run',
command: 'mkdir testing',
},
// Runs command without silent mode
{
type: 'run',
command: 'echo "Hello World!"',
silent: false,
},
// Runs multiple commands
{
type: 'run',
command: ['npm i', 'npm run start'],
},
// Same as above, but using command separators
{
type: 'run',
command: 'npm i && npm run start', // or 'npm i; npm run start'
},
];
});Changing directory
Each run action is executed at the initial directory, where faberconfig is found.
When a cd command is used to change the current directory, this navigation persists only within the current action, to the next commands from the same action.
When a run action completes, the cd navigation is restored to the initial directory for the next action.
See the example below:
[
// Creates a directory `foo` at ./subfolder
{
type: 'run',
command: ['cd subfolder', 'mkdir foo'], // or using && intead
},
// Creates a directory `bar` at ./
// ignoring navigation from previous actions
{
type: 'run',
command: 'mkdir bar',
},
];Considerations
- Using command separators (
&∨) has the same behavior as using an array with multiple commands. It's just a matter of preference; - When changing directories (i.e.
cd path/to/folder), the navigation persists for other commands in the current action; - By default, nothing is logged from the executed commands. To display the commands' output in the terminal, set the
silentoption asfalse; - This action uses the shelljs library for executing the commands, using the
exec()function for running the commands, and thecd()function forcdcommands (to change directory).
Commands (CLI)
The CLI has commands to create new projects, test boilerplates, and also manage available repository aliases on your local machine.
In a nutshell, you will use:
faber create– To create new projects from a pre-configured boilerplate. This command clones the repo and then executes thefaber executecommand inside of it.faber execute– To execute the configured actions on the current folder. When preparing a boilerplate, you can use this command to test the actions you are writing.faber ls|add|rm– To manage aliases to your boilerplates for ease of usage when using thefaber createcommand.
faber create
Creates a new project using a pre-configured boilerplate.
Usage example
$ faber create my-projectIncluding URL to clone repository and other flags:
$ faber create my-project https://github.com/path/example.git --branch main --use-existingArguments
faber create <name> [clone_url]
<name>– Name of the project's root folder.[clone_url]– The URL for cloning the repository (can be SSL or HTTPS, depending on your permissions and authentication).
Flags (optional)
--use-existing(bool) – If the folder already exists, skip the prompt and continue with the existing folder, without cloning any repository. In this case, make sure you back up before executing.--override-existing(bool) – If the folder already exists, skip the prompt and delete the existing folder before cloning the repository.--branch(string) – Name of the git branch to retrieve from the repository. If not defined, the default branch is used.--keep-git(bool) – Prevent deleting the existing Git history from the new cloned folder, removed by default.--keep-config(bool) – Prevent deleting thefaberconfigfile from the new cloned folder, removed by default.
Also includes all flags available to the faber execute command:
--data(string) – JSON data (preferrably encoded) to be passed to the actions.--no-preview(bool) – Does not show the JSON data preview.--deep-preview(bool) – Shows the JSON data preview with all the properties and array items expanded.--no-results(bool) – Does not show the actions results.
What does it do?
- Clones the boilerplate repository in the current directory into a new folder with the provided name.
- Run the steps from the
faber executecommand. - Deletes the
.gitfolder from the repository (when not using the--keep-gitflag); - Deletes the
faberconfig.*file (when not using the--keep-configflag);
Notice: You need to have permission to read from the boilerplate repository. When using private repositories, you need to authenticate via SSH or HTTPS as you normally would when cloning.
faber execute
Executes the configured actions on the current directory. Useful for configuring and testing actions.
A faberconfig file should be present on the directory.
Usage examples
$ faber executeIncluding JSON data and other flags:
$ faber execute --data "{title:\"Example\"}" --no-previewFlags (optional)
--data(string) – JSON data (preferrably encoded) to be passed to the actions.--no-preview(bool) – Does not show the JSON data preview.--deep-preview(bool) – Shows the JSON data preview with all the properties and array items expanded.--no-results(bool) – Does not show the actions results.
What does it do?
- Read the
faberconfigfile from the directory; - Ask for the JSON data to pass to the actions (when not provided with the
--dataflag); - Display a preview of the data (when not using the
--no-previewflag); - Execute the actions from
faberconfig(when not using the--dryflag);
Back up before executing
⚠️ To avoid losing your original code when running faber execute, make sure you have a backup with the current state of your codebase to go back to when testing your actions.
One way to do that is by staging your changes with Git before any test, which also allows you to easily diff the changes your actions made in the files.
Here is a quick example on how to do it in a repository with Git initialized:
- Make your changes to
faberconfigand other files; - Stage your changes with
git add ., or specifying which files to add; - Run
faber executeto run your configured actions; - Run
git diffto compare what your actions has changed (or use your favorite visual tool 😉); - Undo your actions with
git restore --worktree . && git clean -fd(which will revert, or delete, unstaged and untracked files and folders);
faber ls
List your registered repository aliases.
Usage example
$ faber lsfaber add
Adds a repository alias to your list of available boilerplates.
Usage example
$ faber add my-boilerplate https://github.com/path/example.git 'My Boilerplate'Arguments
faber add <alias> <repository> [name]
<alias>(mandatory) – Used to reference this boilerplate on other commands. It should consist only of letters, numbers, dashes and underscores.<repository>(mandatory) – URL to clone the repository. It usually ends with.git.[name](optional) – A name for this boilerplate to display when using thefaber createcommand.
faber rm
Removes a repository alias from your list of available boilerplates.
Usage example
$ faber rm my-boilerplateArguments
faber rm <alias>
<alias>(mandatory) – The reference to the boilerplate to remove from your list.
Mentions
This documentation was inspired by the Plop documentation.
Plop is an amazing micro-framework with a similar goal as Faber, however, while Plop is great for generating code inside your project using Handlebars as template engine, Faber is fully focused on starting new projects, with no defined template engine, so that you can run the boilerplate project on its own, for testing or development, while still making it a living boilerplate to clone with Faber.
P.S. You can use both together in your projects to make your life easier. 😉
