File: README.md
Plop
Micro-generator framework that makes it easy for an entire team to create files with a level of uniformity.
Getting Started
What is Plop?
Plop is what I like to call a "micro-generator framework." Now, I call it that because it is a small tool that gives you a simple way to generate code or any other type of flat text files in a consistent way. You see, we all create structures and patterns in our code (routes, controllers, components, helpers, etc). These patterns change and improve over time so when you need to create a NEW insert-name-of-pattern-here, it's not always easy to locate the files in your codebase that represent the current "best practice." That's where plop saves you. With plop, you have your "best practice" method of creating any given pattern in CODE. Code that can easily be run from the terminal by typing plop. Not only does this save you from hunting around in your codebase for the right files to copy, but it also turns "the right way" into "the easiest way" to make new files.
If you boil plop down to its core, it is basically glue code between inquirer prompts and handlebar templates.
This documentation is a work in progress. If you have great ideas, I'd love to hear them.
Installation
1. Add plop to your project
$ npm install --save-dev plop2. Install plop globally (optional, but recommended for easy access)
$ npm install plop3. Create a plopfile.js at the root of your project
export default function (plop) {
// create your generators here
plop.setGenerator("basics", {
prompts: [], // array of inquirer prompts
actions: [], // array of actions
});
}
export defaultis only allowed in NodeJS inside "ESM" supported files.
To use this syntax, yourplopfilemust be either:
- An ESM .js file with type: "module" in package.json
- An ESM .mjs file with any type declared in package.json
Alternatively, you can have a
plopfilewithmodule.exports = function (plop)instead.
For this syntax, yourplopfilemust be either:
- A CommonJS .js file with type: "commonjs" in package.json
- A CommonJS .cjs file with any type declared in package.json
Your First Plopfile
A plopfile starts its life as a node module that exports a function which accepts the plop object as its first parameter.
export default function (plop) {}The plop object exposes the plop API object which contains the setGenerator(name, config) function. This is the function that you use to (wait for it) create a generator for this plopfile. When plop is run from the terminal in this directory (or any sub-directory), a list of these generators will be displayed.
Let's try setting up a basic generator to see how that looks.
export default function (plop) {
// controller generator
plop.setGenerator("controller", {
prompts: [
{
type: "input",
name: "name",
message: "controller name please",
},
],
actions: [
{
type: "add",
path: "src/{{name}}.js",
templateFile: "plop-templates/controller.hbs",
},
],
});
}The controller generator we created above will ask us 1 question, and create 1 file. This can be expanded to ask as many questions as needed, and create as many files as needed. There are also additional actions that can be used to alter our codebase in different ways.
Using Prompts
Plop uses the inquirer.js library to gather user data. A list of prompt types can be found on the inquirer official website.
CLI Usage
Once plop is installed, and you have created a generator, you are ready to run plop from the terminal. Running plop with no parameters will present you with a list of generators to pick from. You can also run plop [generatorName] to trigger a generator directly. If you did not install plop globally, you will need to setup an npm script to run plop for you.
// package.json
{
...,
"scripts": {
"plop": "plop"
},
...
}Bypassing Prompts
Once you get to know a project (and its generators) well, you may want to provide answers to the prompts when you run the generator. If I have (for instance) a component generator that has one prompt (name), I can run that generator using plop component "some component name" and it will immediately execute as though I had typed "some component name" into the prompt. If that same generator had a second prompt, the same input would have resulted in the user being prompted for the second value.
Prompts like confirm and list try to make sense of your input as best they can. For instance entering "y", "yes", "t", or "true" for a confirm prompt will result in a boolean true value. You can select items from a list using their value, index, key, or name. Checkbox prompts can accept a comma separated list of values in order to select multiples.
If you want to provide bypass input for the second prompt but not the first, you can use an underscore "_" to skip the bypass (ie
plop component _ "input for second prompt").
Plop comes with bypass logic built-in for standard inquirer prompts, but there are also ways to provide custom logic for how to handle user input for a specific prompt.
If you have published a 3rd party inquirer prompt plugin and would like to support bypass functionality for plop users out of the box, that is covered in another section of this documentation.
Bypassing Prompts (by Name)
You can also bypass prompts by name using -- and then providing arguments for each prompt that you'd like to bypass. Examples below.
Bypass Examples
## Bypassing both prompt 1 and 2
$ plop component "my component" react
$ plop component -- --name "my component" --type react
## Bypassing only prompt 2 (will be prompted for name)
$ plop component _ react
$ plop component -- --type reactRunning a Generator Forcefully
By default Plop actions keep your files safe by failing when things look fishy. The most obvious example of this is not allowing an add action to overwrite a file that already exists. Plop actions individually support the force property but you can also use the --force flag when running Plop from the terminal. Using the --force flag will tell every action to run forcefully. With great power...🕷
Using TypeScript plopfiles
Node.js v22.18+**
Modern Node.js supports TypeScript files out-of-the-box. No extra setup or CLI flags are required to use a TypeScript plopfile.
First, make a TypesScript plopfile using plop --init-ts or by hand:
// plopfile.ts
import type { NodePlopAPI } from "plop";
export default function (plop: NodePlopAPI) {
// plop generator code
}Second, add plop to scripts in package.json:
// package.json
"scripts": {
"plop" : "plop"
}Older Node.js Versions**
Plop bundles TypeScript declarations and supports TypeScript plopfiles via tsx loaders, a feature of NodeJS command line imports.
First, make a TypesScript plopfile using plop --init-ts or by hand:
// plopfile.ts
import { NodePlopAPI } from "plop";
export default function (plop: NodePlopAPI) {
// plop generator code
}Next, install tsx and optionally cross-env:
npm i -D tsx cross-envFinally, use NODE_OPTIONS to activate the tsx loader. Now Plop can import your plopfile.ts:
Node.js v20.6 and above
// package.json
"scripts": {
"plop" : "cross-env NODE_OPTIONS='--import tsx' plop --plopfile=plopfile.ts"
}Node.js v20.5.1 and below
// package.json
"scripts": {
"plop": "cross-env NODE_OPTIONS='--loader tsx' plop --plopfile=plopfile.ts"
}Why Generators?
Because when you create your boilerplate separate from your code, you naturally put more time and thought into it.
Because saving your team (or yourself) 5-15 minutes when creating every route, component, controller, helper, test, view, etc... really adds up.
Because context switching is expensive and saving time is not the only benefit to automating workflows
Plopfile API
The plopfile api is the collection of methods that are exposed by the plop object. Most of the work is done by setGenerator but this section documents the other methods that you may also find useful in your plopfile.
TypeScript Support
Plop bundles TypeScript declarations. See using TypeScript plopfiles for more details.
JSDoc Support
Whether or not you write your plopfile in TypeScript, many editors will offer code assistance via JSDoc declarations.
// plopfile.js
export default function (
/** @type {import('plop').NodePlopAPI} */
plop,
) {
// plop generator code
}Main Methods
These are the methods you will commonly use when creating a plopfile. Other methods that are mostly for internal use are list in the other methods section.
| Method | Parameters | Returns | Description |
|---|---|---|---|
| setGenerator | String, GeneratorConfig | PlopGenerator | setup a generator |
| setHelper | String, Function | setup handlebars helper | |
| setPartial | String, String | setup a handlebars partial | |
| setActionType | String, CustomAction | register a custom action type | |
| setPrompt | String, InquirerPrompt | registers a custom prompt type with inquirer | |
| load | Array[String], Object, Object | loads generators, helpers and/or partials from another plopfile or npm module |
setHelper
setHelper directly corresponds to the handlebars method registerHelper. So if you are familiar with handlebars helpers, then you already know how this works.
export default function (plop) {
plop.setHelper("upperCase", function (text) {
return text.toUpperCase();
});
// or in es6/es2015
plop.setHelper("upperCase", (txt) => txt.toUpperCase());
}setPartial
setPartial directly corresponds to the handlebars method registerPartial. So if you are familiar with handlebars partials, then you already know how this works.
export default function (plop) {
plop.setPartial("myTitlePartial", "<h1>{{titleCase name}}</h1>");
// used in template as {{> myTitlePartial }}
}setActionType
setActionType allows you to create your own actions (similar to add or modify) that can be used in your plopfiles. These are basically highly reusable custom action functions.
_FunctionSignature_ Custom Action
| Parameters | Type | Description |
|---|---|---|
| answers | Object | Answers to the generator prompts |
| config | ActionConfig | The object in the "actions" array for the generator |
| plop | PlopfileApi | The plop api for the plopfile where this action is being run |
export default function (plop) {
plop.setActionType("doTheThing", function (answers, config, plop) {
// do something
doSomething(config.configProp);
// if something went wrong
throw "error message";
// otherwise
return "success status message";
});
// or do async things inside of an action
plop.setActionType("doTheAsyncThing", function (answers, config, plop) {
// do something
return new Promise((resolve, reject) => {
if (success) {
resolve("success status message");
} else {
reject("error message");
}
});
});
// use the custom action
plop.setGenerator("test", {
prompts: [],
actions: [
{
type: "doTheThing",
configProp: "available from the config param",
},
{
type: "doTheAsyncThing",
speed: "slow",
},
],
});
}setPrompt
Inquirer provides many types of prompts out of the box, but it also allows developers to build prompt plugins. If you'd like to use a prompt plugin, you can register it with setPrompt. For more details see the Inquirer documentation for registering prompts. Also check out the plop community driven list of custom prompts.
import autocompletePrompt from 'inquirer-autocomplete-prompt';
export default function (plop) {
plop.setPrompt('autocomplete', autocompletePrompt);
plop.setGenerator('test', {
prompts: [{
type: 'autocomplete',
...
}]
});
};setGenerator
The config object needs to include prompts and actions (description is optional). The prompts array is passed to inquirer. The actions array is a list of actions to take (described in greater detail below)
_Interface_ `GeneratorConfig`
| Property | Type | Default | Description |
|---|---|---|---|
| description | [String] | short description of what this generator does | |
| prompts | Array[InquirerQuestion] | questions to ask the user | |
| actions | Array[ActionConfig] | actions to perform |
If your list of actions needs to be dynamic, take a look at using a dynamic actions array.
_Interface_ `PlopGenerator`
| Property | Type | Default | Description |
|---|---|---|---|
| runPrompts | Function | a function to run the prompts within a generator | |
| runActions | Function | a function to run the actions within a generator |
This interface also contains all properties from GeneratorConfig
_Interface_ `ActionConfig`
The following properties are the standard properties that plop handles internally. Other properties will be required depending on the type of action. Also take a look at the built-in actions.
| Property | Type | Default | Description |
|---|---|---|---|
| type | String | the type of action (add, modify, addMany, etc) |
|
| force | Boolean | false |
performs the action forcefully (means different things depending on the action) |
| data | Object / Function | {} |
specifies data that should be mixed with user prompt answers when running this action |
| abortOnFail | Boolean | true |
if this action fails for any reason abort all future actions |
| skip | Function | an optional function that specifies if the action should run |
The
dataproperty on anyActionConfigcan also be aFunctionthat returns anObjector aFunctionthat returns aPromisethat resolves with anObject.
The
skipfunction on anyActionConfigis optional and should return a string if the action should be skipped. The return value is the reason to skip the action.
Instead of an Action Object, a function can also be used
Other Methods
| Method | Parameters | Returns | Description |
|---|---|---|---|
| getHelper | String | Function | get the helper function |
| getHelperList | Array[String] | get a list of helper names | |
| getPartial | String | String | get a handlebars partial by name |
| getPartialList | Array[String] | get a list of partial names | |
| getActionType | String | CustomAction | get an actionType by name |
| getActionTypeList | Array[String] | get a list of actionType names | |
| setWelcomeMessage | String | Customizes the displayed message that asks you to choose a generator when you run plop. |
|
| getGenerator | String | GeneratorConfig | get the PlopGenerator by name |
| getGeneratorList | Array[Object] | gets an array of generator names and descriptions | |
| setPlopfilePath | String | set the plopfilePath value which is used internally to locate resources like template files |
|
| getPlopfilePath | String | returns the absolute path to the plopfile in use | |
| getDestBasePath | String | returns the base path that is used when creating files | |
| setDefaultInclude | Object | Object | sets the default config that will be used for this plopfile if it is consumed by another plopfile using plop.load() |
| getDefaultInclude | String | Object | gets the default config that will be used for this plopfile if it is consumed by another plopfile using plop.load() |
| renderString | String, Object | String | Runs the first parameter (String) through the handlebars template renderer using the second parameter (Object) as the data. Returns the rendered template. |
Built-In Actions
There are several types of built-in actions you can use in your GeneratorConfig. You specify which type of action (all paths are based on the location of the plopfile), and a template to use.
The
Add,AddManyandModifyactions have an optionaltransformmethod that can be used to transform the template result before it is written to disk. Thetransformfunction receives the template result or file contents as astringand the action data as arguments. It must return astringor aPromisethat resolves to astring.
Add
The add action is used to (you guessed it) add a file to your project. The path property is a handlebars template that will be used to create the file by name. The file contents will be determined by the template or templateFile property.
| Property | Type | Default | Description |
|---|---|---|---|
| path | String | a handlebars template that (when rendered) is the path of the new file | |
| template | String | a handlebars template that should be used to build the new file | |
| templateFile | String | a path a file containing the template |
|
| skipIfExists | Boolean | false |
skips a file if it already exists (instead of failing) |
| transform | Function | an optional function that can be used to transform the template result before writing the file to disk | |
| skip | Function | inherited from ActionConfig | |
| force | Boolean | false |
inherited from ActionConfig (overwrites files if they exist) |
| data | Object | {} |
inherited from ActionConfig |
| abortOnFail | Boolean | true |
inherited from ActionConfig |
AddMany
The addMany action can be used to add multiple files to your project with a single action. The destination property is a handlebars template that will be used to identify the folder that the generated files should go into. The base property can be used to alter what section of the template paths should be omitted when creating files. The paths located by the templateFiles glob can use handlebars syntax in their file/folder names if you'd like the added file names to be unique (example: {{ dashCase name }}.spec.js).
| Property | Type | Default | Description |
|---|---|---|---|
| destination | String | a handlebars template that (when rendered) is the destination folder for the new files | |
| base | String | the section of the path that should be excluded when adding files to the destination folder |
|
| templateFiles | Glob | glob pattern that matches multiple template files to be added | |
| stripExtensions | [String] | ['hbs'] |
file extensions that should be stripped from templateFiles files names while being added to the destination |
| globOptions | Object | glob options that change how to match to the template files to be added | |
| verbose | Boolean | true |
print each successfully added file path |
| transform | Function | an optional function that can be used to transform the template result before writing each file to disk | |
| skip | Function | inherited from ActionConfig | |
| skipIfExists | Boolean | false |
inherited from Add (skips a file if it already exists) |
| force | Boolean | false |
inherited from ActionConfig (overwrites files if they exist) |
| data | Object | {} |
inherited from ActionConfig |
| abortOnFail | Boolean | true |
inherited from ActionConfig |
Modify
The modify action can be used two ways. You can use a pattern property to find/replace text in the file located at the path specified, or you can use a transform function to transform the file contents. Both pattern and transform can be used at the same time (transform will happen last). More details on modify can be found in the example folder.
| Property | Type | Default | Description |
|---|---|---|---|
| path | String | handlebars template that (when rendered) is the path of the file to be modified | |
| pattern | RegExp | end‑of‑file | regular expression used to match text that should be replaced |
| template | String | handlebars template that should replace what was matched by the pattern. capture groups are available as $1, $2, etc |
|
| templateFile | String | path a file containing the template |
|
| transform | Function | an optional function that can be used to transform the file before writing it to disk | |
| skip | Function | inherited from ActionConfig | |
| data | Object | {} |
inherited from ActionConfig |
| abortOnFail | Boolean | true |
inherited from ActionConfig |
Append
The append action is a commonly used subset of modify. It is used to append data in a file at a particular location.
| Property | Type | Default | Description |
|---|---|---|---|
| path | String | handlebars template that (when rendered) is the path of the file to be modified | |
| pattern | RegExp, String | regular expression used to match text where the append should happen | |
| unique | Boolean | true |
whether identical entries should be removed |
| separator | String | new line |
the value that separates entries |
| template | String | handlebars template to be used for the entry | |
| templateFile | String | path a file containing the template |
|
| data | Object | {} |
inherited from ActionConfig |
| abortOnFail | Boolean | true |
inherited from ActionConfig |
Custom (Action Function)
The Add and Modify actions will take care of almost every case that plop is designed to handle. However, plop does offer custom action functions for the node/js guru. A custom action function is a function that is provided in the actions array.
- Custom action functions are executed by plop with the same CustomAction function signature.
- Plop will wait for the custom action to complete before executing the next action.
- The function must let plop known what’s happening through the return value. If you return a
Promise, we won’t start other actions until the promise resolves. If you return a message (String), we know that the action is done and we’ll report the message in the status of the action. - A custom action fails if the promise is rejected, or the function throws an
Exception
See the example plopfile for a sample synchronous custom action.
Comments
Comment lines can be added to the actions array by adding a string in place of an action config object. Comments are printed to the screen when plop comes to them and have no functionality of their own.
Built-In Helpers
There are a few helpers that I have found useful enough to include with plop. They are mostly case modifiers, but here is the complete list.
Case Modifiers
- camelCase: changeFormatToThis
- snakeCase: change_format_to_this
- dashCase/kebabCase: change-format-to-this
- dotCase: change.format.to.this
- pathCase: change/format/to/this
- properCase/pascalCase: ChangeFormatToThis
- lowerCase: change format to this
- sentenceCase: Change format to this,
- constantCase: CHANGE_FORMAT_TO_THIS
- titleCase: Change Format To This
Other Helpers
- pkg: look up a property from a package.json file in the same folder as the plopfile.
Taking it Further
There is not a lot needed to get up and running on some basic generators. However, if you want to take your plop-fu further, read on young padawan.
Using a Dynamic Actions Array
Alternatively, the actions property of the GeneratorConfig can itself be a function that takes the answers data as a parameter and returns the actions array.
This allows you to adapt the actions array based on provided answers:
export default function (plop) {
plop.setGenerator("test", {
prompts: [
{
type: "confirm",
name: "wantTacos",
message: "Do you want tacos?",
},
],
actions: function (data) {
var actions = [];
if (data.wantTacos) {
actions.push({
type: "add",
path: "folder/{{dashCase name}}.txt",
templateFile: "templates/tacos.txt",
});
} else {
actions.push({
type: "add",
path: "folder/{{dashCase name}}.txt",
templateFile: "templates/burritos.txt",
});
}
return actions;
},
});
}3rd Party Prompt Bypass
If you have written an inquirer prompt plugin and want to support plop's bypass functionality, the process is pretty simple. The plugin object that your prompt exports should have a bypass function. This bypass function will be run by plop with the user's input as the first parameter and the prompt config object as the second parameter. The value that this function returns will be added to the answer data object for that prompt.
// My confirmation inquirer plugin
export default MyConfirmPluginConstructor;
function MyConfirmPluginConstructor() {
// ...your main plugin code
this.bypass = (rawValue, promptConfig) => {
const lowerVal = rawValue.toString().toLowerCase();
const trueValues = ["t", "true", "y", "yes"];
const falseValues = ["f", "false", "n", "no"];
if (trueValues.includes(lowerVal)) return true;
if (falseValues.includes(lowerVal)) return false;
throw Error(`"${rawValue}" is not a valid ${promptConfig.type} value`);
};
return this;
}For the above example, the bypass function takes the user's text input and turns it into a
Booleanvalue that will be used as the prompt answer data.
Adding Bypass Support to Your Plopfile
If the 3rd party prompt plugin you are using does not support bypass by default, you can add the bypass function above to your prompt's config object and plop will use it for handling bypass data for that prompt.
Wrapping Plop
Plop provides a lot of powerful functionality "for free". This utility is so powerful, in fact, that you can even wrap plop
into your own CLI project. To do so, you only need a plopfile.js, a package.json, and a template to reference.
Your index.js file should look like the following:
#!/usr/bin/env node
import path from "node:path";
import minimist from "minimist";
import { Plop, run } from "plop";
const args = process.argv.slice(2);
const argv = minimist(args);
import { dirname } from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = dirname(fileURLToPath(import.meta.url));
Plop.prepare(
{
cwd: argv.cwd,
configPath: path.join(__dirname, "plopfile.js"),
preload: argv.preload || [],
completion: argv.completion,
},
(env) => Plop.execute(env, run),
);And your package.json should look like the following:
{
"name": "create-your-name-app",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"start": "plop"
},
"bin": {
"create-your-name-app": "./index.js"
},
"preferGlobal": true,
"dependencies": {
"plop": "^3.0.0"
}
}Setting the base destination path for the wrapper
When wrapping plop, you might want to have the destination path to be based on the cwd when running the wrapper. You can configure the dest base path like this:
Plop.prepare(
{
// config like above
},
(env) =>
Plop.execute(env, (env) => {
const options = {
...env,
dest: process.cwd(), // this will make the destination path to be based on the cwd when calling the wrapper
};
return run(options, undefined, true);
}),
);Adding General CLI Actions
Many CLI utilities handle some actions for you, such as running git init or npm install once the template is generated.
While we'd like to provide these actions, we also want to keep the core actions limited in scope. As such, we maintain a collection of libraries built to add these actions to Plop in our Awesome Plop list. There, you'll be able to find options for those actions, or even build your own and add it to the list!
Further Customization
While plop provides a great level of customization for CLI utility wrappers, there may be usecases where you simply
want more control over the CLI experience while also utilizing the template generation code.
Luckily, node-plop may be for you! It's what the plop CLI itself is built
upon and can be easily extended for other usage in the CLI. However, be warned, documentation is not quite as fleshed out
for integration with node-plop. That is to say Thar be dragons.
We note lackluster documentation on
node-plopintegration not as a point of pride, but rather a word of warning.
If you'd like to contribute documentation to the project, please do so! We always welcome and encourage contributions!
File: packages/plop/README.md
Plop
Micro-generator framework that makes it easy for an entire team to create files with a level of uniformity.
Getting Started
What is Plop?
Plop is what I like to call a "micro-generator framework." Now, I call it that because it is a small tool that gives you a simple way to generate code or any other type of flat text files in a consistent way. You see, we all create structures and patterns in our code (routes, controllers, components, helpers, etc). These patterns change and improve over time so when you need to create a NEW insert-name-of-pattern-here, it's not always easy to locate the files in your codebase that represent the current "best practice." That's where plop saves you. With plop, you have your "best practice" method of creating any given pattern in CODE. Code that can easily be run from the terminal by typing plop. Not only does this save you from hunting around in your codebase for the right files to copy, but it also turns "the right way" into "the easiest way" to make new files.
If you boil plop down to its core, it is basically glue code between inquirer prompts and handlebar templates.
This documentation is a work in progress. If you have great ideas, I'd love to hear them.
Installation
1. Add plop to your project
$ npm install --save-dev plop2. Install plop globally (optional, but recommended for easy access)
$ npm install plop3. Create a plopfile.js at the root of your project
export default function (plop) {
// create your generators here
plop.setGenerator("basics", {
prompts: [], // array of inquirer prompts
actions: [], // array of actions
});
}
export defaultis only allowed in NodeJS inside "ESM" supported files.
To use this syntax, yourplopfilemust be either:
- An ESM .js file with type: "module" in package.json
- An ESM .mjs file with any type declared in package.json
Alternatively, you can have a
plopfilewithmodule.exports = function (plop)instead.
For this syntax, yourplopfilemust be either:
- A CommonJS .js file with type: "commonjs" in package.json
- A CommonJS .cjs file with any type declared in package.json
Your First Plopfile
A plopfile starts its life as a node module that exports a function which accepts the plop object as its first parameter.
export default function (plop) {}The plop object exposes the plop API object which contains the setGenerator(name, config) function. This is the function that you use to (wait for it) create a generator for this plopfile. When plop is run from the terminal in this directory (or any sub-directory), a list of these generators will be displayed.
Let's try setting up a basic generator to see how that looks.
export default function (plop) {
// controller generator
plop.setGenerator("controller", {
prompts: [
{
type: "input",
name: "name",
message: "controller name please",
},
],
actions: [
{
type: "add",
path: "src/{{name}}.js",
templateFile: "plop-templates/controller.hbs",
},
],
});
}The controller generator we created above will ask us 1 question, and create 1 file. This can be expanded to ask as many questions as needed, and create as many files as needed. There are also additional actions that can be used to alter our codebase in different ways.
Using Prompts
Plop uses the inquirer.js library to gather user data. A list of prompt types can be found on the inquirer official website.
CLI Usage
Once plop is installed, and you have created a generator, you are ready to run plop from the terminal. Running plop with no parameters will present you with a list of generators to pick from. You can also run plop [generatorName] to trigger a generator directly. If you did not install plop globally, you will need to setup an npm script to run plop for you.
// package.json
{
...,
"scripts": {
"plop": "plop"
},
...
}Bypassing Prompts
Once you get to know a project (and its generators) well, you may want to provide answers to the prompts when you run the generator. If I have (for instance) a component generator that has one prompt (name), I can run that generator using plop component "some component name" and it will immediately execute as though I had typed "some component name" into the prompt. If that same generator had a second prompt, the same input would have resulted in the user being prompted for the second value.
Prompts like confirm and list try to make sense of your input as best they can. For instance entering "y", "yes", "t", or "true" for a confirm prompt will result in a boolean true value. You can select items from a list using their value, index, key, or name. Checkbox prompts can accept a comma separated list of values in order to select multiples.
If you want to provide bypass input for the second prompt but not the first, you can use an underscore "_" to skip the bypass (ie
plop component _ "input for second prompt").
Plop comes with bypass logic built-in for standard inquirer prompts, but there are also ways to provide custom logic for how to handle user input for a specific prompt.
If you have published a 3rd party inquirer prompt plugin and would like to support bypass functionality for plop users out of the box, that is covered in another section of this documentation.
Bypassing Prompts (by Name)
You can also bypass prompts by name using -- and then providing arguments for each prompt that you'd like to bypass. Examples below.
Bypass Examples
## Bypassing both prompt 1 and 2
$ plop component "my component" react
$ plop component -- --name "my component" --type react
## Bypassing only prompt 2 (will be prompted for name)
$ plop component _ react
$ plop component -- --type reactRunning a Generator Forcefully
By default Plop actions keep your files safe by failing when things look fishy. The most obvious example of this is not allowing an add action to overwrite a file that already exists. Plop actions individually support the force property but you can also use the --force flag when running Plop from the terminal. Using the --force flag will tell every action to run forcefully. With great power...🕷
Using TypeScript plopfiles
Node.js v22.18+**
Modern Node.js supports TypeScript files out-of-the-box. No extra setup or CLI flags are required to use a TypeScript plopfile.
First, make a TypesScript plopfile using plop --init-ts or by hand:
// plopfile.ts
import { NodePlopAPI } from "plop";
export default function (plop: NodePlopAPI) {
// plop generator code
}Second, add plop to scripts in package.json:
// package.json
"scripts": {
"plop" : "plop"
}Older Node.js Versions**
Plop bundles TypeScript declarations and supports TypeScript plopfiles via tsx loaders, a feature of NodeJS command line imports.
First, make a TypesScript plopfile using plop --init-ts or by hand:
// plopfile.ts
import { NodePlopAPI } from "plop";
export default function (plop: NodePlopAPI) {
// plop generator code
}Next, install tsx and optionally cross-env:
npm i -D tsx cross-envFinally, use NODE_OPTIONS to activate the tsx loader. Now Plop can import your plopfile.ts:
Node.js v20.6 and above
// package.json
"scripts": {
"plop" : "cross-env NODE_OPTIONS='--import tsx' plop --plopfile=plopfile.ts"
}Node.js v20.5.1 and below
// package.json
"scripts": {
"plop": "cross-env NODE_OPTIONS='--loader tsx' plop --plopfile=plopfile.ts"
}Why Generators?
Because when you create your boilerplate separate from your code, you naturally put more time and thought into it.
Because saving your team (or yourself) 5-15 minutes when creating every route, component, controller, helper, test, view, etc... really adds up.
Because context switching is expensive and saving time is not the only benefit to automating workflows
Plopfile API
The plopfile api is the collection of methods that are exposed by the plop object. Most of the work is done by setGenerator but this section documents the other methods that you may also find useful in your plopfile.
TypeScript Support
Plop bundles TypeScript declarations. See using TypeScript plopfiles for more details.
JSDoc Support
Whether or not you write your plopfile in TypeScript, many editors will offer code assistance via JSDoc declarations.
// plopfile.js
export default function (
/** @type {import('plop').NodePlopAPI} */
plop,
) {
// plop generator code
}Main Methods
These are the methods you will commonly use when creating a plopfile. Other methods that are mostly for internal use are list in the other methods section.
| Method | Parameters | Returns | Description |
|---|---|---|---|
| setGenerator | String, GeneratorConfig | PlopGenerator | setup a generator |
| setHelper | String, Function | setup handlebars helper | |
| setPartial | String, String | setup a handlebars partial | |
| setActionType | String, CustomAction | register a custom action type | |
| setPrompt | String, InquirerPrompt | registers a custom prompt type with inquirer | |
| load | Array[String], Object, Object | loads generators, helpers and/or partials from another plopfile or npm module |
setHelper
setHelper directly corresponds to the handlebars method registerHelper. So if you are familiar with handlebars helpers, then you already know how this works.
export default function (plop) {
plop.setHelper("upperCase", function (text) {
return text.toUpperCase();
});
// or in es6/es2015
plop.setHelper("upperCase", (txt) => txt.toUpperCase());
}setPartial
setPartial directly corresponds to the handlebars method registerPartial. So if you are familiar with handlebars partials, then you already know how this works.
export default function (plop) {
plop.setPartial("myTitlePartial", "<h1>{{titleCase name}}</h1>");
// used in template as {{> myTitlePartial }}
}setActionType
setActionType allows you to create your own actions (similar to add or modify) that can be used in your plopfiles. These are basically highly reusable custom action functions.
_FunctionSignature_ Custom Action
| Parameters | Type | Description |
|---|---|---|
| answers | Object | Answers to the generator prompts |
| config | ActionConfig | The object in the "actions" array for the generator |
| plop | PlopfileApi | The plop api for the plopfile where this action is being run |
export default function (plop) {
plop.setActionType("doTheThing", function (answers, config, plop) {
// do something
doSomething(config.configProp);
// if something went wrong
throw "error message";
// otherwise
return "success status message";
});
// or do async things inside of an action
plop.setActionType("doTheAsyncThing", function (answers, config, plop) {
// do something
return new Promise((resolve, reject) => {
if (success) {
resolve("success status message");
} else {
reject("error message");
}
});
});
// use the custom action
plop.setGenerator("test", {
prompts: [],
actions: [
{
type: "doTheThing",
configProp: "available from the config param",
},
{
type: "doTheAsyncThing",
speed: "slow",
},
],
});
}setPrompt
Inquirer provides many types of prompts out of the box, but it also allows developers to build prompt plugins. If you'd like to use a prompt plugin, you can register it with setPrompt. For more details see the Inquirer documentation for registering prompts. Also check out the plop community driven list of custom prompts.
import autocompletePrompt from 'inquirer-autocomplete-prompt';
export default function (plop) {
plop.setPrompt('autocomplete', autocompletePrompt);
plop.setGenerator('test', {
prompts: [{
type: 'autocomplete',
...
}]
});
};setGenerator
The config object needs to include prompts and actions (description is optional). The prompts array is passed to inquirer. The actions array is a list of actions to take (described in greater detail below)
_Interface_ `GeneratorConfig`
| Property | Type | Default | Description |
|---|---|---|---|
| description | [String] | short description of what this generator does | |
| prompts | Array[InquirerQuestion] | questions to ask the user | |
| actions | Array[ActionConfig] | actions to perform |
If your list of actions needs to be dynamic, take a look at using a dynamic actions array.
_Interface_ `PlopGenerator`
| Property | Type | Default | Description |
|---|---|---|---|
| runPrompts | Function | a function to run the prompts within a generator | |
| runActions | Function | a function to run the actions within a generator |
This interface also contains all properties from GeneratorConfig
_Interface_ `ActionConfig`
The following properties are the standard properties that plop handles internally. Other properties will be required depending on the type of action. Also take a look at the built-in actions.
| Property | Type | Default | Description |
|---|---|---|---|
| type | String | the type of action (add, modify, addMany, etc) |
|
| force | Boolean | false |
performs the action forcefully (means different things depending on the action) |
| data | Object / Function | {} |
specifies data that should be mixed with user prompt answers when running this action |
| abortOnFail | Boolean | true |
if this action fails for any reason abort all future actions |
| skip | Function | an optional function that specifies if the action should run |
The
dataproperty on anyActionConfigcan also be aFunctionthat returns anObjector aFunctionthat returns aPromisethat resolves with anObject.
The
skipfunction on anyActionConfigis optional and should return a string if the action should be skipped. The return value is the reason to skip the action.
Instead of an Action Object, a function can also be used
Other Methods
| Method | Parameters | Returns | Description |
|---|---|---|---|
| getHelper | String | Function | get the helper function |
| getHelperList | Array[String] | get a list of helper names | |
| getPartial | String | String | get a handlebars partial by name |
| getPartialList | Array[String] | get a list of partial names | |
| getActionType | String | CustomAction | get an actionType by name |
| getActionTypeList | Array[String] | get a list of actionType names | |
| setWelcomeMessage | String | Customizes the displayed message that asks you to choose a generator when you run plop. |
|
| getGenerator | String | GeneratorConfig | get the PlopGenerator by name |
| getGeneratorList | Array[Object] | gets an array of generator names and descriptions | |
| setPlopfilePath | String | set the plopfilePath value which is used internally to locate resources like template files |
|
| getPlopfilePath | String | returns the absolute path to the plopfile in use | |
| getDestBasePath | String | returns the base path that is used when creating files | |
| setDefaultInclude | Object | Object | sets the default config that will be used for this plopfile if it is consumed by another plopfile using plop.load() |
| getDefaultInclude | String | Object | gets the default config that will be used for this plopfile if it is consumed by another plopfile using plop.load() |
| renderString | String, Object | String | Runs the first parameter (String) through the handlebars template renderer using the second parameter (Object) as the data. Returns the rendered template. |
Built-In Actions
There are several types of built-in actions you can use in your GeneratorConfig. You specify which type of action (all paths are based on the location of the plopfile), and a template to use.
The
Add,AddManyandModifyactions have an optionaltransformmethod that can be used to transform the template result before it is written to disk. Thetransformfunction receives the template result or file contents as astringand the action data as arguments. It must return astringor aPromisethat resolves to astring.
Add
The add action is used to (you guessed it) add a file to your project. The path property is a handlebars template that will be used to create the file by name. The file contents will be determined by the template or templateFile property.
| Property | Type | Default | Description |
|---|---|---|---|
| path | String | a handlebars template that (when rendered) is the path of the new file | |
| template | String | a handlebars template that should be used to build the new file | |
| templateFile | String | a path a file containing the template |
|
| skipIfExists | Boolean | false |
skips a file if it already exists (instead of failing) |
| transform | Function | an optional function that can be used to transform the template result before writing the file to disk | |
| skip | Function | inherited from ActionConfig | |
| force | Boolean | false |
inherited from ActionConfig (overwrites files if they exist) |
| data | Object | {} |
inherited from ActionConfig |
| abortOnFail | Boolean | true |
inherited from ActionConfig |
AddMany
The addMany action can be used to add multiple files to your project with a single action. The destination property is a handlebars template that will be used to identify the folder that the generated files should go into. The base property can be used to alter what section of the template paths should be omitted when creating files. The paths located by the templateFiles glob can use handlebars syntax in their file/folder names if you'd like the added file names to be unique (example: {{ dashCase name }}.spec.js).
| Property | Type | Default | Description |
|---|---|---|---|
| destination | String | a handlebars template that (when rendered) is the destination folder for the new files | |
| base | String | the section of the path that should be excluded when adding files to the destination folder |
|
| templateFiles | Glob | glob pattern that matches multiple template files to be added | |
| stripExtensions | [String] | ['hbs'] |
file extensions that should be stripped from templateFiles files names while being added to the destination |
| globOptions | Object | glob options that change how to match to the template files to be added | |
| verbose | Boolean | true |
print each successfully added file path |
| transform | Function | an optional function that can be used to transform the template result before writing each file to disk | |
| skip | Function | inherited from ActionConfig | |
| skipIfExists | Boolean | false |
inherited from Add (skips a file if it already exists) |
| force | Boolean | false |
inherited from ActionConfig (overwrites files if they exist) |
| data | Object | {} |
inherited from ActionConfig |
| abortOnFail | Boolean | true |
inherited from ActionConfig |
Modify
The modify action can be used two ways. You can use a pattern property to find/replace text in the file located at the path specified, or you can use a transform function to transform the file contents. Both pattern and transform can be used at the same time (transform will happen last). More details on modify can be found in the example folder.
| Property | Type | Default | Description |
|---|---|---|---|
| path | String | handlebars template that (when rendered) is the path of the file to be modified | |
| pattern | RegExp | end‑of‑file | regular expression used to match text that should be replaced |
| template | String | handlebars template that should replace what was matched by the pattern. capture groups are available as $1, $2, etc |
|
| templateFile | String | path a file containing the template |
|
| transform | Function | an optional function that can be used to transform the file before writing it to disk | |
| skip | Function | inherited from ActionConfig | |
| data | Object | {} |
inherited from ActionConfig |
| abortOnFail | Boolean | true |
inherited from ActionConfig |
Append
The append action is a commonly used subset of modify. It is used to append data in a file at a particular location.
| Property | Type | Default | Description |
|---|---|---|---|
| path | String | handlebars template that (when rendered) is the path of the file to be modified | |
| pattern | RegExp, String | regular expression used to match text where the append should happen | |
| unique | Boolean | true |
whether identical entries should be removed |
| separator | String | new line |
the value that separates entries |
| template | String | handlebars template to be used for the entry | |
| templateFile | String | path a file containing the template |
|
| data | Object | {} |
inherited from ActionConfig |
| abortOnFail | Boolean | true |
inherited from ActionConfig |
Custom (Action Function)
The Add and Modify actions will take care of almost every case that plop is designed to handle. However, plop does offer custom action functions for the node/js guru. A custom action function is a function that is provided in the actions array.
- Custom action functions are executed by plop with the same CustomAction function signature.
- Plop will wait for the custom action to complete before executing the next action.
- The function must let plop known what’s happening through the return value. If you return a
Promise, we won’t start other actions until the promise resolves. If you return a message (String), we know that the action is done and we’ll report the message in the status of the action. - A custom action fails if the promise is rejected, or the function throws an
Exception
See the example plopfile for a sample synchronous custom action.
Comments
Comment lines can be added to the actions array by adding a string in place of an action config object. Comments are printed to the screen when plop comes to them and have no functionality of their own.
Built-In Helpers
There are a few helpers that I have found useful enough to include with plop. They are mostly case modifiers, but here is the complete list.
Case Modifiers
- camelCase: changeFormatToThis
- snakeCase: change_format_to_this
- dashCase/kebabCase: change-format-to-this
- dotCase: change.format.to.this
- pathCase: change/format/to/this
- properCase/pascalCase: ChangeFormatToThis
- lowerCase: change format to this
- sentenceCase: Change format to this,
- constantCase: CHANGE_FORMAT_TO_THIS
- titleCase: Change Format To This
Other Helpers
- pkg: look up a property from a package.json file in the same folder as the plopfile.
Taking it Further
There is not a lot needed to get up and running on some basic generators. However, if you want to take your plop-fu further, read on young padawan.
Using a Dynamic Actions Array
Alternatively, the actions property of the GeneratorConfig can itself be a function that takes the answers data as a parameter and returns the actions array.
This allows you to adapt the actions array based on provided answers:
export default function (plop) {
plop.setGenerator("test", {
prompts: [
{
type: "confirm",
name: "wantTacos",
message: "Do you want tacos?",
},
],
actions: function (data) {
var actions = [];
if (data.wantTacos) {
actions.push({
type: "add",
path: "folder/{{dashCase name}}.txt",
templateFile: "templates/tacos.txt",
});
} else {
actions.push({
type: "add",
path: "folder/{{dashCase name}}.txt",
templateFile: "templates/burritos.txt",
});
}
return actions;
},
});
}3rd Party Prompt Bypass
If you have written an inquirer prompt plugin and want to support plop's bypass functionality, the process is pretty simple. The plugin object that your prompt exports should have a bypass function. This bypass function will be run by plop with the user's input as the first parameter and the prompt config object as the second parameter. The value that this function returns will be added to the answer data object for that prompt.
// My confirmation inquirer plugin
export default MyConfirmPluginConstructor;
function MyConfirmPluginConstructor() {
// ...your main plugin code
this.bypass = (rawValue, promptConfig) => {
const lowerVal = rawValue.toString().toLowerCase();
const trueValues = ["t", "true", "y", "yes"];
const falseValues = ["f", "false", "n", "no"];
if (trueValues.includes(lowerVal)) return true;
if (falseValues.includes(lowerVal)) return false;
throw Error(`"${rawValue}" is not a valid ${promptConfig.type} value`);
};
return this;
}For the above example, the bypass function takes the user's text input and turns it into a
Booleanvalue that will be used as the prompt answer data.
Adding Bypass Support to Your Plopfile
If the 3rd party prompt plugin you are using does not support bypass by default, you can add the bypass function above to your prompt's config object and plop will use it for handling bypass data for that prompt.
Wrapping Plop
Plop provides a lot of powerful functionality "for free". This utility is so powerful, in fact, that you can even wrap plop
into your own CLI project. To do so, you only need a plopfile.js, a package.json, and a template to reference.
Your index.js file should look like the following:
#!/usr/bin/env node
import path from "node:path";
import minimist from "minimist";
import { Plop, run } from "plop";
const args = process.argv.slice(2);
const argv = minimist(args);
import { dirname } from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = dirname(fileURLToPath(import.meta.url));
Plop.prepare(
{
cwd: argv.cwd,
configPath: path.join(__dirname, "plopfile.js"),
preload: argv.preload || [],
completion: argv.completion,
},
(env) => Plop.execute(env, run),
);And your package.json should look like the following:
{
"name": "create-your-name-app",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"start": "plop"
},
"bin": {
"create-your-name-app": "./index.js"
},
"preferGlobal": true,
"dependencies": {
"plop": "^3.0.0"
}
}Setting the base destination path for the wrapper
When wrapping plop, you might want to have the destination path to be based on the cwd when running the wrapper. You can configure the dest base path like this:
Plop.prepare(
{
// config like above
},
(env) =>
Plop.execute(env, (env) => {
const options = {
...env,
dest: process.cwd(), // this will make the destination path to be based on the cwd when calling the wrapper
};
return run(options, undefined, true);
}),
);Adding General CLI Actions
Many CLI utilities handle some actions for you, such as running git init or npm install once the template is generated.
While we'd like to provide these actions, we also want to keep the core actions limited in scope. As such, we maintain a collection of libraries built to add these actions to Plop in our Awesome Plop list. There, you'll be able to find options for those actions, or even build your own and add it to the list!
Further Customization
While plop provides a great level of customization for CLI utility wrappers, there may be usecases where you simply
want more control over the CLI experience while also utilizing the template generation code.
Luckily, node-plop may be for you! It's what the plop CLI itself is built
upon and can be easily extended for other usage in the CLI. However, be warned, documentation is not quite as fleshed out
for integration with node-plop. That is to say Thar be dragons.
We note lackluster documentation on
node-plopintegration not as a point of pride, but rather a word of warning.
If you'd like to contribute documentation to the project, please do so! We always welcome and encourage contributions!
File: packages/node-plop/README.md
Node-Plop
This is an early publication of the plop core logic being removed from the CLI tool. Main purpose for this is to make it easier for others to automate code generation through processes and tools OTHER than the command line. This also makes it easier to test the code functionality of PLOP without needing to test via the CLI interface.
This is the backend code that drives the plop CLI tool using node-plop.
import nodePlop from "node-plop";
// load an instance of plop from a plopfile
const plop = await nodePlop(`./path/to/plopfile.js`);
// get a generator by name
const basicAdd = plop.getGenerator("basic-add");
// run all the generator actions using the data specified
basicAdd.runActions({ name: "this is a test" }).then(function (results) {
// do something after the actions have run
});File: README.zh.md
快速上手
文档版本:[email protected] - [email protected]
什么是 Plop?
Plop是一个“微生成器框架”——之所以这样定义它,是因为Plop是一个用于生成代码文件或其他纯文本文件的小工具。并且简单、高效、同时保证一致性。在开发中我们的代码往往基于“框架”和“关键字”的组合(路由、控制器、模板、等等)。这些组合往往随着时间的推移和开发的进行不断的会被完善和调整。当你需要创建一个同类型的文件时,你往往很难在浩如烟海的代码仓库中找到当初的那个“最佳实践”。而Plop专为解决此类问题而生,通过使用Plop,你只需要在控制台输入Plop命令,即可创建任何格式的、已经提前编写好的“最佳实现”代码。这不仅节约了你在搜寻合适代码来复制粘贴的时间,同时它也提供给你一种最“正确”也最“方便”的创建新文件的方法。
Plop的核心其实主要是inquirer命令行工具和handlebar模板引擎的整合。
此文档还在编写中,如果你有什么好想法请联系我们。
安装
1. 将plop添加到项目
$ npm install --save-dev plop2. 全局安装plop (可选项,推荐)
$ npm install plop3. 在项目的根目录创建plopfile.js
module.exports = function (plop) {
// 创建生成器
plop.setGenerator("basics", {
prompts: [], // 确认问题数组
actions: [], // 操作列表
});
};
export default只可以被用在NodeJS下的支持“ESM”标准的文件中。如果你希望使用这种语法,请确保你的使用环境属于以下两种之一:
- 文件后缀名为.js,package.json的“type”字段中定义为“module”
- 文件后缀名为.mjs,此情况不限制package.json的“type”字段
与此同时,你也可以创建一个内容为
module.exports = function (plop)的plopfile。如果使用这种方式引入,需要你的使用环境属于以下两种之一:
- 文件后缀名为.js,package.json的“type”字段中定义为“commonjs”
- 文件后缀名为.js,此情况不限制package.json的“type”字段
编写Plopfile
Plopfile是一个简单的Node module,并导出一个函数,它接受一个plop对象作为第一个传入参数。
module.exports = function (plop) {};plop对象暴露了一些plop的api,其中包括setGenerator(name, config)方法,使用此方法可以创建Plop生成器。当在工作目录(或工作目录的子目录)命令行中输入plop命令时,所有的生成器会以列表形式输出。
一个最简单的起始生成器例子可能类似这样:
module.exports = function (plop) {
// controller generator
plop.setGenerator("controller", {
prompts: [
{
type: "input",
name: "name",
message: "controller name please",
},
],
actions: [
{
type: "add",
path: "src/{{name}}.js",
templateFile: "plop-templates/controller.hbs",
},
],
});
};我们创建的 controller 生成器会询问我们一个问题,并创建一个文件。这一操作可以进一步扩展,比如询问更多的问题,创建更多的文件。也有一些附加的action可以通过其他的方式操作我们的代码仓库。
使用Prompts
Plop使用inquirer.js 库来接受用户输入。更多关于问题类型的信息可以查看inquirer官方文档.
通过CLI调用
Plop成功安装并且创建了generator之后,你就可以使用命令行运行plop了。直接调用plop命令将会输出所有可用的生成器。你呀可以直接使用plop [生成器名称]来直接调用某一生成器。如果你没有全局安装plop,则需要编写一个npm脚本来帮助运行plop。
// package.json
{
...,
"scripts": {
"plop": "plop"
},
...
}直接传入参数
如果你对一个项目(以及它的生成器)十分了解,你就可能希望在调用生成器时直接传递回答参数了。例如项目中有一个名为component的生成器,其接受一个名为name的参数,可以直接通过plop component "名字"直接传入参数。如果生成器接受更多参数,只要按照这种规则依次传入即可。
例如confirm和list类型的问题会最大程度尝试理解你的输入。比如为confirm类型问题传入参数"y","yes","t",或者"true"都会被解析成布尔值true。同时你可以使用选项的确切值,数组下标,键名,或者名称等等表示从列表中选中某选项。多选问题允许你传入以逗号分隔的值的列表,来表示多选项。
操作示例
如果你想直接传入参数的问题是第二个问题,第一个仍需手动填写或选择,可以使用“_“来跳过某个参数(例如
plop component _ "input for second prompt").
Plop已经内置常规问题的直接传入参数逻辑了,不过你也可以自定义一些特殊问题解析用户输入的逻辑。如果你发布了inquirer的第三方插件并想支持plop的直接传入参数逻辑,可以查看本文档的相关内容.
通过属性名直接传入参数
你也可以直接传入参数 -- 然后提供每个问题的参数来直接传入参数,例子如下.
直接传入参数示例
## 直接传入问题1和2的参数
$ plop component "my component" react
$ plop component -- --name "my component" --type react
## 直接传入问题2的参数 (name 属性依然会正常询问)
$ plop component _ react
$ plop component -- --type react强制执行生成器
通常情况下Plop的action在发现执行可以操作时会执行失败,以此来尽可能确保文件安全。最常见的情况可能是不允许add 操作覆盖一个已存在文件。但是Plop支持特殊的force属性,你也可以在命令行输入的指令后加上--force选项来开启强制模式,在这一模式下所有操作均会强制完成。不惜一切代价...🕷
为什么是“生成器”机制
这一机制可以使得你可以专注于编写模板,而不必同时考虑代码逻辑问题。这一机制可以为你和你的团队在面对重复逻辑时(路由,组件,辅助函数,测试,界面层,等等)节省大量的时间,这真的很重要!
与此同时,对于程序猿来说在不同任务之间反复横跳真的很浪费时间,很有可能还没等你进入编写新逻辑的状态你就忍不住去摸鱼了...所以一次专注一件事真的可以大大提高工作效率!而且工程化运作的优势远不止于此!
Plopfile API
Plopfile api是一系列plop对象暴露出的方法,其实setGenerator可以解决大部分的问题。但是在这部分文档中你可能会找到一些其他的有用信息以充实和完善你的plopfile。
使用TypeScript
plop内置了Typescript类型定义,无论你是否需要使用Typescript编写plopfile,这一特定都可以方便大部分的代码编辑器提供代码提示。
// plopfile.ts
import { NodePlopAPI } from 'plop';
export default function (plop: NodePlopAPI) {
// plop generator code
};// plopfile.js
module.exports = function (
/** @type {import('plop').NodePlopAPI} */
plop,
) {
// plop generator code
};常用方法
这些是创建plopfile时常用的方法。其他主要供内部使用的方法在其他方法部分列出。
| 方法名 | 入参 | 返回值 | 描述 |
|---|---|---|---|
| setGenerator | String, GeneratorConfig | GeneratorConfig | 创建一个生成器 |
| setHelper | String, Function | 创建一个helper | |
| setPartial | String, String | 创建一个Partial | |
| setActionType | String, CustomAction | 创建一个Action类型 | |
| setPrompt | String, InquirerPrompt | 使用inquirer注册自定义问题类型 | |
| load | Array[String], Object, Object | 从另一plopfile或者 npm module中载入生成器,helper,和/或 partials |
setHelper
setHelper 等同于 handlebars 的方法 registerHelper。 如果你熟悉 handlebars helpers,那你就已经对此很熟悉了。
export default function (plop) {
plop.setHelper("upperCase", function (text) {
return text.toUpperCase();
});
// or in es6/es2015
plop.setHelper("upperCase", (txt) => txt.toUpperCase());
}setPartial
setPartial 等同于 handlebars 的方法 registerPartial。如果你熟悉 handlebars partials,那你就已经对此很熟悉了。
export default function (plop) {
plop.setPartial("myTitlePartial", "<h1>{{titleCase name}}</h1>");
// used in template as {{> myTitlePartial }}
}setActionType
setActionType允许你创建自定义actions (类似 add 或 modify) 完善你的plopfile。这些方法通常都是高度可复用的的 自定义函数。
_函数定义_ 自定义Action
| 参数 | 类型 | 描述 |
|---|---|---|
| answers | Object | 生成器问题的回答 |
| config | ActionConfig | 生成器“actions”数组中的对象 |
| plop | PlopfileApi | Action运作时的plop api |
export default function (plop) {
plop.setActionType("doTheThing", function (answers, config, plop) {
// do something
doSomething(config.configProp);
// if something went wrong
throw "error message";
// otherwise
return "success status message";
});
// or do async things inside of an action
plop.setActionType("doTheAsyncThing", function (answers, config, plop) {
// do something
return new Promise((resolve, reject) => {
if (success) {
resolve("success status message");
} else {
reject("error message");
}
});
});
// use the custom action
plop.setGenerator("test", {
prompts: [],
actions: [
{
type: "doTheThing",
configProp: "available from the config param",
},
{
type: "doTheAsyncThing",
speed: "slow",
},
],
});
}setPrompt
Inquirer 提供了许多开箱即用的问题类型,但是其也允许开发者构建第三方插件丰富问题类型。如果你需要使用第三方问题差价插件,你可以使用setPrompt。更多细节可以查看Inquirer关于注册问题的官方文档。 你也可以查看plop社区提供的自定问题列表.
import autocompletePrompt from 'inquirer-autocomplete-prompt';
export default function (plop) {
plop.setPrompt('autocomplete', autocompletePrompt);
plop.setGenerator('test', {
prompts: [{
type: 'autocomplete',
...
}]
});
};setGenerator
这一部分的配置对象需要包含prompts 和 actions(description是可选项)。Prompts数组会被传递到inquirer,actions数组是一系列将要进行的操作的数组。(详细文档参阅下文)
_接口_ `GeneratorConfig`
| 参数 | 类型 | 默认值 | 描述 |
|---|---|---|---|
| description | [String] | 此生成器功能的简短描述 | |
| prompts | Array[InquirerQuestion] | 需要询问用户的问题 | |
| actions | Array[ActionConfig] | 需要执行的操作 |
>如果你的Action列表有动态需求,你可以查看使用动态action数组部分内容。
_接口_ `ActionConfig`
下列参数是plop内部需要使用的基本参数,其他参数的需求取决于action的类型,关于这部分可以查看内置actions。
| 参数 | 类型 | 默认值 | 描述 |
|---|---|---|---|
| type | String | action的类型 (add, modify, addMany, 等等) |
|
| force | Boolean | false |
强制执行forcefully (在不同action下有不同表现) |
| data | Object / Function | {} |
指定需要与用户输入参数进行合并的数据 |
| abortOnFail | Boolean | true |
如果本操作因任何原因失败则取消后续操作 |
| skip | Function | 可选项,标记这个action是否会被执行 |
ActionConfig的data属性也可以为一个返回对象的函数或者一个返回resolve内容为函数的Promise。
ActionConfig的skip属性方法是可选项,其应该返回一个字符串,内容是逃过action执行的原因。
Action除了可以使用对象写法,也可以使用函数写法
其他方法
| 方法 | 参数 | 返回 | 描述 |
|---|---|---|---|
| getHelper | String | Function | 获取helper函数 |
| getHelperList | Array[String] | 获取helper函数名称列表 | |
| getPartial | String | String | 通过名称获取handlebars partial |
| getPartialList | Array[String] | 获取partial名称列表 | |
| getActionType | String | CustomAction | 通过名称获取action 类型 |
| getActionTypeList | Array[String] | 获取actionType名称列表 | |
| setWelcomeMessage | String | 自定义运行plop时提示选择generator的提示标语 |
|
| getGenerator | String | GeneratorConfig | 根据名称获取GeneratorConfig |
| getGeneratorList | Array[Object] | 获取generator名称和描述的列表 | |
| setPlopfilePath | String | 更改内部用于定位资源和模板文件的plopfilePath值 |
|
| getPlopfilePath | String | 返回plopfile的绝对路径 | |
| getDestBasePath | String | 获取创建文件的基准路径 | |
| setDefaultInclude | Object | Object | 设置当被其他文件使用 plop.load()载入时的默认设置 |
| getDefaultInclude | String | Object | 获取当被其他文件使用 plop.load()载入时的默认设置 |
| renderString | String, Object | String | 使用handlebars渲染第一个参数(String)的模板,第二个参数(Object)作为渲染模板的数据 |
内置actions
你可以在GeneratorConfig中通过设置action的type属性以及模板名称(路径均为plopfile的相对路径)来使用内置的action。
Add,AddMany和Modifyactions有一个可选方法transform,它可以在模板渲染结果被写入文件之前对结果做出修改。transform方法接受字符串类型的文件内容或许安然结果作为参数,同时必须返回一个字符串或者字符串Promise。
Add
正如其字面意思,addaction会想你的项目中添加一个问题件。Path参数将会被传入handlebars作为渲染的目标文件名字。文件内容则取决于template或 templateFile 属性。
| 属性 | 类型 | 默认值 | 描述 |
|---|---|---|---|
| path | String | 渲染结果的目标位置 | |
| template | String | 渲染使用的模板名称 | |
| templateFile | String | 使用的模板路径 | |
| skipIfExists | Boolean | false |
当文件已经存在的时候直接跳过(而不是失败) |
| transform | Function | 可选方法,可以在内容被写入文件之前做出修改 | |
| skip | Function | 继承自 ActionConfig | |
| force | Boolean | false |
继承自 ActionConfig (如果文件存在将直接覆盖) |
| data | Object | {} |
继承自 ActionConfig |
| abortOnFail | Boolean | true |
继承自 ActionConfig |
AddMany
addManyaction可以在一个action中向项目添加多个文件。destination属性为生成文件的目标文件夹,base属性可以用于更改创建文件时需要忽略的文件路径。如果你需要生成特定文件名的文件,你可以在templateFiles属性处使用handlebars语法,例如“{ { dashCase name } }.spec.js”
| 属性 | 类型 | 默认值 | 描述 |
|---|---|---|---|
| destination | String | 渲染结果文件的目标文件夹 | |
| base | String | 向destination中写入文件时排除的文件路径 | |
| templateFiles | Glob | 用于匹配需要添加的模板 | |
| stripExtensions | [String] | ['hbs'] |
模板文件的后缀名 |
| globOptions | Object | 改变模板文件匹配方法的glob选项 | |
| verbose | Boolean | true |
输出成功添加的文件路径 |
| transform | Function | 可选方法,可以在内容被写入文件之前做出修改 | |
| skip | Function | 继承自 ActionConfig | |
| skipIfExists | Boolean | false |
继承自Add 当文件已经存在的时候直接跳过(而不是失败) |
| force | Boolean | false |
继承自 ActionConfig (如果文件存在将直接覆盖) |
| data | Object | {} |
继承自 ActionConfig |
| abortOnFail | Boolean | true |
继承自 ActionConfig |
Modify
modifyaction有两种使用方法,你可以使用pattern属性来匹配并替换在path中指定的文件,同时你可以使用transform方法来修改文件内容。pattern和 transform可以同时使用(transform后执行)。更多细节可以查看官方example
| 属性 | 类型 | 默认值 | 描述 |
|---|---|---|---|
| path | String | 需要被修改的文件渲染时使用的handlebars模板 | |
| pattern | RegExp | end‑of‑file | 用以匹配和替换的正则表达式 |
| template | String | 模板中需要被匹配替换的内容,可以使用$1, $2等等作为占位符 |
|
| templateFile | String | 包含template的文件路径 |
|
| transform | Function | 可选方法,可以在内容被写入文件之前做出修改 | |
| skip | Function | 继承自 ActionConfig | |
| data | Object | {} |
继承自 ActionConfig |
| abortOnFail | Boolean | true |
继承自 ActionConfig |
Append
append action 是更常用的modify的子集。 他可以在文件的特定位置插入内容。
| 属性 | 类型 | 默认值 | 描述 |
|---|---|---|---|
| path | String | 需要被修改的文件渲染时使用的handlebars模板 | |
| pattern | RegExp, String | 用以匹配和替换的正则表达式 | |
| unique | Boolean | true |
相同内容是否需要被移除 |
| separator | String | new line |
分隔条目的值 |
| template | String | 需要被修改的文件渲染时使用的handlebars模板 | |
| templateFile | String | 包含template的文件路径 |
|
| data | Object | {} |
继承自 ActionConfig |
| abortOnFail | Boolean | true |
继承自 ActionConfig |
此处文档翻译有待推敲,欢迎提出pr进行修正和补充
Custom (Action Function)
Add和Modify几乎可以完成plop设计的所有功能,不过plop也提供了更加进阶的自定义action方法。在actions数组中填入自定义的action函数即可使用这一功能。
- 自行以action在执行时与官方action使用相同的函数接口
- Plop会等待自定义action方法执行完毕后才会继续执行下一个action
- 函数必须返回一个Plop可以理解的确切值,如果返回了一个
promise,那么在这个promise完成之前plop不会进行任何操作。如果函数返回了一个字符串类型的消息(String), plop便可得知action已经成功执行,并将此信息输出到action的状态提示信息上。 - 当返回的promise被reject,会程序抛出了一个异常,plop会视为action执行失败
你也可以查看官方提供的 同步自定义action案例
Comments
通过添加一个字符串来代替 action config 对象,可以将注释行添加到 actions 数组中。当plop执行这一action时,注释就会被打印到屏幕上,这一action本身不会进行其他任何操作。
内置 Helper
这些Helper应该可以覆盖你使用plop的所有需要,其中大部分是格式转换器,以下是内置Helper完整列表
格式转换器
- camelCase: changeFormatToThis
- snakeCase: change_format_to_this
- dashCase/kebabCase: change-format-to-this
- dotCase: change.format.to.this
- pathCase: change/format/to/this
- properCase/pascalCase: ChangeFormatToThis
- lowerCase: change format to this
- sentenceCase: Change format to this
- constantCase: CHANGE_FORMAT_TO_THIS
- titleCase: Change Format To This
其他 Helper
- pkg: 在plopfile同文件夹下的packag.json中寻找某一属性
进阶使用
以下是一些常见的进阶用法
使用动态构建的actions数组
GeneratorConfig的actions属性可以是一个函数,他接受answers作为传入参数并且返回actions数组,这使你可以根据输入的答案动态调整actions数组
module.exports = function (plop) {
plop.setGenerator("test", {
prompts: [
{
type: "confirm",
name: "wantTacos",
message: "Do you want tacos?",
},
],
actions: function (data) {
var actions = [];
if (data.wantTacos) {
actions.push({
type: "add",
path: "folder/{{dashCase name}}.txt",
templateFile: "templates/tacos.txt",
});
} else {
actions.push({
type: "add",
path: "folder/{{dashCase name}}.txt",
templateFile: "templates/burritos.txt",
});
}
return actions;
},
});
};向社区第三方Prompt直接传入数据
如果你编写了一个inquirer问题插件,并想要提供plop支持,实现起来很简单。你只需要为你的问题插件导出对象添加一个bypass方法。这一方法会被plop执行,执行时传入的第一个参数为用户的输入,第二个参数为prompt设置对象。这个函数的返回值会被作为该问题的答案保存到data对象中。
// My confirmation inquirer plugin
module.exports = MyConfirmPluginConstructor;
function MyConfirmPluginConstructor() {
// ...your main plugin code
this.bypass = (rawValue, promptConfig) => {
const lowerVal = rawValue.toString().toLowerCase();
const trueValues = ["t", "true", "y", "yes"];
const falseValues = ["f", "false", "n", "no"];
if (trueValues.includes(lowerVal)) return true;
if (falseValues.includes(lowerVal)) return false;
throw Error(`"${rawValue}" is not a valid ${promptConfig.type} value`);
};
return this;
}在这个案例中,函数将用户输入解析为布尔值并存入data对象中
直接在plopfile中提供直接传入参数支持
如果你的第三方插件不支持直接传入参数,你可以直接在你的问题设置对象中加入bypass方法,plop会自动将其用于解析参数。
对Plop进行包装
Plop额外提供了很多非常强大的功能,你甚至可以基于这些功能对plop进行进一步的包装,将其打包成你自己的cli。你只需要一个plopfile.js,一个package.json,和一个用于参考的模板文件。
你可以这样编写你的index.js:
#!/usr/bin/env node
const path = require("path");
const args = process.argv.slice(2);
const { Plop, run } = require("plop");
const argv = require("minimist")(args);
Plop.launch(
{
cwd: argv.cwd,
// In order for `plop` to always pick up the `plopfile.js` despite the CWD, you must use `__dirname`
configPath: path.join(__dirname, "plopfile.js"),
require: argv.require,
completion: argv.completion,
// This will merge the `plop` argv and the generator argv.
// This means that you don't need to use `--` anymore
},
(env) => run(env, undefined, true),
);如果你选择了
env => run(env, undefined, true))写法,你可能会在直接传入参数时
遇到指令执行问题如果你想放弃使用这一特性而使用类似plop使用的方法 (需要在向generator传入参数之前加上
--)
只要将箭头函数env =>替换成run即可:javascriptPlop.launch({}, run);
你的 package.json 可能将会类似如下格式:
{
"name": "create-your-name-app",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"start": "plop"
},
"bin": {
"create-your-name-app": "./index.js"
},
"preferGlobal": true,
"dependencies": {
"plop": "^2.6.0"
}
}为你的自定义程序设定基准路径
当进一步打包plop时,你可能需要基于命令行执行cwd生成目标路径,你可以这样设置基准路径dest:
Plop.launch(
{
// config like above
},
(env) => {
const options = {
...env,
dest: process.cwd(), // this will make the destination path to be based on the cwd when calling the wrapper
};
return run(options, undefined, true);
},
);添加自动CLI Actions
许多CLI都会自动帮你进行某些行为,比如说在项目生成完成后自动运行 git init 或 npm install
这些行为非常普遍,但是我们出于保持核心功能简洁的目的并没有默认添加它们。因此,我们维护了一个 库集合,用于将这些行为添加到our Awesome Plop list中,在那里,您可以找到这些行为,甚至可以构建自己的行为并将其添加到列表中!。
更加进阶的自定义
虽然plop已经提供了高度可自定义的CLI包装支持,但也有可能某些场景下你需要在借助模板代码生成功能的基础上更进一步的掌控CLI的工作体验。
幸运的是, node-plop 可能更适合你! plop CLI本身就是基于此构建的,你可以基于此构建其他功能更加丰富的CLI。只是这部分的文档可能有待更进一步的完善,不过风险往往与机会并存 :)
我们注意到关于
node-plop集成的乏善可陈的文档可能是我们的一项短板。如果您希望为项目贡献文档,欢迎您积极参与进来!