Scaffolder
Copy pasting is prone to mistakes.
Keeping your project file structure consistent is annoying.
Sharing templates is too damn complicated!
This is where Scaffolder come in.
For a brief introduction and motivation for this tool, read this.
check out the vscode extension
TOC
Getting started
Setup
Install scaffolder globally
npm i -g scaffolder-cli
this will make the scaff
command available globally, you can now type scaff i
in the terminal, to enter the cli in interactive mode.
You can also use npx
for example npx scaffolder-cli i
will start scaffolder in interactive mode.
Usage
Create a templates folder in your project root directory
The templates folder should be named scaffolder and should contain folders where each folder represents a different template and inside of that folder, there is the template structure you wish to create.
The templates available are the templates defined in the scaffolder folder.
If you have more scaffolder folders in the current file system hierarchy then all of them will be included with precedence to the nearest scaffolder folder.
For example:
In our current project root
scaffolder
βββ component
βΒ Β βββ index.js
βΒ Β βββ {{componentName}}.js
βΒ Β βββ {{componentName}}.spec.js
βββ index
βββ index.js
In our desktop
scaffolder
βββ component
βΒ Β βββ index.js
βΒ Β βββ {{lol}}.js
βΒ Β βββ {{wattt}}.spec.js
βββ coolFile
βββ coolFile.sh
From the above structure, we will have three commands component (from the project scaffolder), index (from the project scaffolder) and coolFile (from the desktop scaffolder).
Lets look at the content of {{componentName}}.js and {{componentName}}.spec.js.
{{componentName}}.js from the current project scaffolder folder.
import React from 'react'
export const {{componentName}} = (props) => {
return (
<div>
Such a cool component
</div>
)
}
{{componentName}}.spec.js
import React from 'react';
import { mount } from 'enzyme';
import { {{componentName}} } from './{{componentName}}';
describe('{{componentName}}', () => {
it('should have a div', () => {
const wrapper = mount(
<{{componentName}} />
);
expect(wrapper.find('div').exists()).toBeTruthy()
});
});
Now let's run the following command somewhere in our project
scaff create component componentName=CoolAFComponent --folder MyCoolComp
A new folder will be created under our current working directory, let's look at what we got.
MyCoolComp
βββ CoolAFComponent.js
βββ CoolAFComponent.spec.js
βββ index.js
CoolAFComponent.js
import React from "react";
export const CoolAFComponent = (props) => {
return <div>Such a cool component</div>;
};
CoolAFComponent.spec.js
import React from "react";
import { mount } from "enzyme";
import { CoolAFComponent } from "./CoolAFComponent";
describe("CoolAFComponent", () => {
it("should have a div", () => {
const wrapper = mount(<CoolAFComponent />);
expect(wrapper.find("div").exists()).toBeTruthy();
});
});
This could also be achieved using the interactive mode!
How cool is this, right?
As you can see our params got injected to the right places and we created our template with little effort.
Hooray!!
API
interactive, i
Run Scaffolder in interactive mode, meaning, it will prompt the user to choose a template and a value for each parameter.
This command is the most recommended one as it simplifies the process for the user a lot.
options:
-
--from-github
Passing this flag will cause a prompt to appear, asking the user to enter a github repository (https/ssh) and consume the templates defined on that repository.
More info about sharing templates. -
--entry-point <absolutePath>
Generate the template to a specified location. -
--path-prefix <relativePath>
Path that will be appended the the location the template is generated into.If the path does not exists Scaffolder will automatically create it and notify the user what paths were created for him.
For example:
<entry-point>/<path-prefix>/<template-will-be-created-here>
-
--template <templateName>
Start the interactive mode with a preselected template. -
--values <commaSeparatedParametersValue>
Predefine values for specific parameters param1=val1,param2=val2...
create <templateName>
<templateName>: One of the templates defined in the scaffolder folder.
options:
-
--load-from <absolutePath>
Load the templates from a specific location. -
--entry-point <absolutePath>
Generate the template to a specified location. -
--path-prefix <relativePath>
Path that will be appended the the location the template is generated into.If the path does not exists Scaffolder will automatically create it and notify the user what paths were created for him.
For example:
<entry-point>/<path-prefix>/<template-will-be-created-here>
-
--folder, -f <folderName>
<folderName>: The name of the folder you want the template to be generated into. If none is supplied the template will be generated to the current working directory. -
<parameter>=<value>
<parameter>: One of the parameters for a specific template
<value>: The value you want the parameter to be replaced with.
save-remote <alias>
<alias>: The alias that will map to your remote templates.
options:
- --location <githubSrc>
The location of your remotes, e.g github ssh or https.
For example:[email protected]:galElmalah/scaffolder-templates-example.git
orhttps://github.com/galElmalah/scaffolder-templates-example.git
delete-remote <alias>
<alias>: The alias you wish to delete.
list, ls
Show the available templates from the current working directory.
show <templateName>
Show a specific template files
options:
- --show-content
Also show the full content of the template files.
Sharing templates
Often you find yourself wanting to share a template while not making every consumer of that template to save it on his machine.
In order to address that problem, Scaffolder lets you consume templates from Github repositories that have a scaffolder folder at their root.
For example, you can see this repository which contains 3 templates and a config file.
To generate one of those templates you can run npx scaffolder-cli i --from-github
and enter https://github.com/galElmalah/scaffolder-templates-example.git
and you'll be promoted to choose one of those templates.
You can now save remote templates repos under aliases!
For example, assuming you have scaffolder-cli installed globaly, you can run
scaff save-remote my-remote-templates --location https://github.com/galElmalah/scaffolder-templates-example.git
and from now on, you will have a new entry named (remote) my-remote-templates
when you run scffolder in interactive mode that will list all of the available templates in that repo.
Any improvement suggestions? go ahead and open an issue!
Scaffolder config file
Scaffolder lets you extend and define all sorts of things via a config file.
the config file should be placed inside the scaffolder folder that the template you are generating is defined in and named scaffolder.config.js
.
Through the scaffolder.config.js
file you can extend and customize scaffolder in several ways.
Example config file
module.exports = {
transformers: {
toLowerCase: (parameterValue, context) => parameterValue.toLowerCase(),
},
functions: {
date: (context) => Date.now(),
},
parametersOptions: {
someParameter: {
question:
"this text will be shown to the user in the interactive mode when he will be asked to enter the value for 'someParameter'",
},
},
templatesOptions: {
someTemplate: {
hooks: {
preTemplateGeneration: (context) => {
// do something before generating a template
},
postTemplateGeneration: (context) => {
// do something after generating a template
}
},
// transformers, functions, parametersOptions can be scoped to specific templates
transformers: {...},
functions: {...},
parametersOptions: {...}
}
}
};
transformers, functions, parametersOptions can be scoped to specific templates, and will have precedents over non scoped options.
transformers
Transformers can be used to transform a parameter value.
For example, you can write the following:
{{ someParameter | toLowerCase | someOtherTransformer }}
and the value that will be injected in your template will be the value after all of the transformations.
- Transformers can be chained together.
- Transformers are invoked with the value supplied for that parameter as the first argument and the context object as the second argument.
Default transformers
You can use the following transformers without defining anything in your config file
- toLowerCase
- toUpperCase
- capitalize - capitalize each word in your parameter value
- toCamelCase - takes Kebab or snake case and transform them to camelCase format
- camelCaseToSnakeCase
- camelCaseToKebabCase
functions
functions are very similar to transformations, but they are unary, meaning, they are invoked without any parameter value supplied to them.
For example, you can write the following:
{{date()}}
and the value returned from are date function (defined in our config file) will be injected to the template.
parametersOptions
parametersOptions is a map from parameters to their options.
For example, lets say we have a parameter named myReactComponentName and we want to show a custom question to the user when he is asked to enter a value for that parameter, we can add the following to our config file:
{
...
...
parametersOptions: {
myReactComponentName: {
question: "Enter a name for your react component:",
validation: (value) => value.length > 3 ? true : "The component name must be longer than 3 chars"
},
parameterTwo: {
question: "Enter a name for your react component:",
choices: {
values:["These", "values", "will", "be", "shown", "to the user to choose from" ]
}
}
}
}
parameter options object
property | type | description |
---|---|---|
question | string | The question that will be shown to the user when he will enter a value for the matching parameter |
validation | (value: string): string | true | this function will be invoked to validate the user input. Return a string if the value is invalid, this string will be shown to the user as an error message. Return true if the value is valid. |
choices | {values: string[]} | Object containing a values property from which the user will be asked to choose a value for that parameter. |
context object
property | type | description |
---|---|---|
parametersValues | Object<string, string> | Key value pairs containing each parameter and his associated value in the current template. |
templateName | string | The name of the template being generated. |
templateRoot | string | Absolute path to the template being generated. |
targetRoot | string | Absolute path to the location the template is being generated into. |
currentFilePath | string | The path to the file being created. |
type | string, one of: "FILE_NAME" , "FILE_CONTENT" , "FOLDER" |
The current type being operated upon - file/folder/content. |
fileName | string | The name of the file being operated upon. Available only if the type is "FILE_NAME" or "FILE_CONTENT". |
logger | Logger | A logger instance |
logger
property | type | description |
---|---|---|
info | (message: string): void | Output general info. |
warning | (message: string): void | Output warnings, meaning, it will be colored orange. |
error | (message: string): void | Output error, meaning, it will be colored red. |
In vscode context, the loggers will pass the message to "scaffolder" output channel prefixed with the log level used.
vscode logs example
[error][context-logger]:: some message
[warning][context-logger]:: some message
[info][context-logger]:: some message
templatesOptions
templatesOptions is a map from templates names to their options.
transformers, functions, parametersOptions can be scoped to specific templates, and will have precedents over non scoped options.
templates options object
property | type | description |
---|---|---|
hooks | hooks object | Hooks are functions to be executed at some point throughout the template |
parametersOptions | parameter options object | |
functions | functions | |
transformers | transformers |
hooks object
If an hook function returns a Promise then it will be awaited and only then the template generation process will continue.
property | type | description |
---|---|---|
preAskingQuestions | (context): any | Promise<any> | Executed before the user is prompted for the template questions. The context object will include an empty parametersValues object. |
preTemplateGeneration | (context): any | Promise<any> | Executed before the template is generated. |
postTemplateGeneration | (context): any | Promise<any> | Executed after the template is generated. |
By default all errors thrown inside of hooks are ignored. To stop execution inside a hook you can use
process.exit(1)
.
Motivation and goals
Why I wrote Scaffolder?
Working on several big projects, I noticed that there a few time-consuming tasks that keep popping up. One of those tasks is creating folders and filling in all the boilerplate code while keeping the project structure consistent. After realizing that this process needs to be automated, I set out to find a solution and ended up creating my own CLI tool
The first thing I had to do is to understand WHY itβs so annoying, and I realized that this happens for two reasons:
- Creating files and folders can be repetitive, annoying and a waste of time. Especially if some content repeats itself for every new file.
- Keeping a project file structure consistent is becoming more and more complex as the number of people working on that project increasesβββeach team member has his preference for naming files and exposing functionality.
Why I didnβt use any existing solutions?
First, came Yeoman. I gave yeoman a try but found it too complex. Furthermore, I want the templates to be a part of the project (in some cases), and committed to git alongside the code. Thus, supporting template generation offline and tight coupling between the project and the templates. All of the above seemed too complex or not possible at all with yeoman, so one hour after trying it out I moved on to other prospects.
Second, came boiler, I did not like this one for the same reasons I did not like Yeoman. Also, the fact that itβs not managed with npm is a bit annoying.
Third, came frustration
Both of these tools are AWESOME but for my needs, they werenβt right.
My goals while writing this tool
-
making this process as easy and seamless as possible.
-
Addressing the general problem. Meaning, it wonβt be language-specific e.g only React or Vue templates. I could potentially create templates in any shape, structure, and language I want.
-
Having the ability to create scoped templates. Meaning, creating project-specific templates that can be committed to git with the rest of the code.
-
Having the ability to create βglobalβ templates that will be available from anywhere.
-
Managed with npm.