template-scaffolder
v2.3.8
Published
multi-file scaffolding with velocity template for any type of project
Downloads
94
Maintainers
Readme
Introduction
Scaffolding templates for your project. Make sure your team is using the same patterns by using scaffolding templates. Quickly create multiple files based on simple templates. Access to environment variables, users prompts, and scripting.
Release Notes
Version 2
- support for typescript configuration files (
scaffolding.config.ts
) - parallel file processing with the
--parallel
option. Testing shows up to 10x faster. - re-write of the prompt system, as the package we were using for this changed dramatically,
so the following question types are deprecated:
list
is nowselect
andfuzzypath
is now justpath
.
Getting Started
npm install template-scaffolder
- Create a
scaffolding
directory in the root of your project. - Create a sub-folder in the in your scaffolding folder for your (first) template.
- In the template folder add a
scaffolding.config.mjs
file with a simple default configuration ofexport default {}
as the contents. - Add 1 or more files to the template folder.
- (optional) Add a script to your
package.json
file to run the scaffolder:"scaffold": "npx scaffolder"
- Fine tune your configuration, templates, and npm scripts by reading below.
Table of Contents
Configuration Files
tsconfig.json
You can have a tsconfig.json
file in your scaffolding directory to control how your
scaffolding configuration files are compiled.
{
"compilerOptions": {
"strict": true, // can be true or false
"module": "CommonJS", // "NodeNext" is also supported
"target": "ESNext" // this option can not be changed, it helps with your linting if present.
// all other options are ignored, and only relevant to your IDE.
}
}
note: you will have to remove comments because this is a JSON file.
Scaffolding configuration files
Each template must have exactly one scaffolding.config
file with one of the following
extensions: .js, .mjs, .ts, .mts
in the root. The default export of this file must be an IConfigFile
(see below for schema).
The typescript schema for this file is:
export interface IConfigFile<TInput extends object> {
name?: string;
description?: string;
version?: string;
variables?:
| TemplateVariables
| ((
instanceName: string,
initialInputs: TInput
) => Promise<TemplateVariables>);
prompts?:
| Question<TInput>[]
| ((
instanceName: string
) => Promise<Question<TInput>[]> | Question<TInput>[]);
stripLines?: PatternList;
// eslint-disable-next-line @typescript-eslint/ban-types
macros?: MacroObject;
destinations?: Array<string> | string;
createNameDir?: boolean;
srcRoot?: string;
afterFileCreated?: (
createdFilePath: string,
dryRun: boolean,
variablesHash: TemplateVariables
) => string[];
}
Note: reference to 'instance name' is referring to the name that the user enters when running the scaffolder. This is not the same as the name of the template.
The defaults will work for most people, so you can have a template with the following default config:
scaffolding.config.js
;
export default {
name: 'js-template',
};
OR
'use strict';
import { IConfigFile } from 'template-scaffolder';
export default {
name: 'ts-template',
} as IConfigFile;
Because this is a regular javascript file, you have access to
process.env
, and any other javascript functions.
Configuration options
All fields are optional.
variables
A hash of keys (variable names) and values that you would like to use in your template files.
Simple example:
export default {
variables: {
CREATED_BY: process.env.USERNAME,
CREATED_ON: new Date().toString(),
SOMETHING: 'this is really something',
},
};
This can also be a function that returns the object giving
access to the instance name which the user has supplied.
In the function you will have access to the name
that the user
has provided, as well as values from prompts in the input
object parameter.
export default {
prompts: [{ name: 'FAV_COLOUR', message: 'Enter favourite color' }],
variables: (name, inputs) => ({
COMPONENT_NAME: name,
TEST_ID: name.replace(/([a-z])([A-Z])/, '$1-$2').toLowerCase(),
FAV_SHIRT_COLOUR: inputs.FAV_COLOUR,
}),
};
Built in Variables
The following variables are available without defining them. All built in variables except NAME can be overwritten using the variables section.
- NAME - the values for name as entered on command line or through prompts.
- TEMPLATE_NAME - the name of the template as it appears in the
name
fields of the config file. - TEMPLATE_VERSION - the version of the template of the as it appears in the
version
fields of the config file. - USERNAME - name of the user running the scaffolder.
(Uses git, and then falls back to the
USERNAME
environment variable).
destinations
The root folder where generated files will be created.
If no value is supplied, the user will be prompted to select a directory under
srcRoot
.
The value can be a list of strings, or a single string:
If a list of paths/string is supplied, the user will be prompted to select one of the options. They will be allowed to select Other, and enter their own directory.
If a single string is supplied, this value will be used for the destination, and the user will not be prompted for a value.
createNameDir
(defaults to true
)
By default, the scaffolder will create a directory at the root of the
destination folder named by the value you enter for the NAME
variable.
The template files will be placed in this created directory.
If you do not want this to happen, set this value to false
.
srcRoot
(defaults to ./src
or process.cwd()
)
When the user is being prompted to enter a destination directory,
only directories under the srcRoot directory will be available.
By default, the src
directory in the root of your project will be used
if one exists. If you do not have a src
directory, then the current
working directory will be used.
Use this value if you need something other than the defaults.
stripLines
Use for removing lines from the template before processing.
A list of strings or regex patterns used to test each line.
Lines that match any pattern will be removed before processing variables and macros.
If a pattern is a string, the test will determine if the line starts with that string (ignoring whitespace).
For regex patterns, test()
will be called on the line to determine a match.
afterFileCreated
If you need some special processing after a file has been scaffolded, you can use this async function. It will execute after each file is created.
You can optionally return a list of 1 or more commands to run as well.
An example would be adding the created file to git, running a formatter on the file, or adding the new file to your IDE's project file, etc.
In the case of a dry run, the file will not actually be created, so you may want to guard against this in your function if it expects it to exist.
The commands that run will not be interactive and should not expect user input.
export default {
afterFileCreated: async (path, dryRun, variables) => {
console.log(`${variables.NAME} adding ${path} to git`);
if (dryRun) return null;
return [`git add ${path}`, `npx prettier --write ${path}`];
},
};
You can return null if all the processing you require occurs in your function.
prompts
If your template requires variable values to be entered by the user, you may prompt the user. The prompts field is an array of Question objects, or a function that returns an list of Question objects, as defined by the Inquirer user prompter. The function form is handy if you need access to the instance name.
You can read more about what a DistinctQuestion
is by looking at the
(3rd party) inquirer documentation here.
Answers to prompts will be placed available in templates as variables
where the name
property will be the variable name,
and the user response will be the value.
For a simple question, you will just need two values:
export default {
prompts: [
{
name: 'MYVAR',
message: 'Enter a value for My Var:',
},
],
};
There are many types of questions you can prompt the user for:
All questions
All questions have the following fields:
interface IQuestionBase {
type?:
| 'fuzzypath'
| 'path'
| 'select'
| 'list'
| 'search'
| 'confirm'
| 'separator'
| 'number'
| 'input'
| undefined;
/**
* The name of the field to store the resulting answer in.
*/
name: string;
/**
* The message prompting the user.
*/
message: string;
/**
* The default value for the answer.
*/
default?: string | number | boolean;
/**
* True if the user must enter a value for the question.
*/
required?: boolean;
/**
* Can we skip answering this question?
* You can examine the previous answers and determine if you would like to answer this questions.
* If the question is skipped, the answer value will be undefined.
* @param previousAnswers
* @return false if the question should be skipped.
*/
when?: <TAnswerObject extends object>(
previousAnswers: TAnswerObject
) => boolean;
}
Simple Input
interface IInputQuestion extends IQuestionBase {
type?: 'input';
default?: string;
}
Confirmation questions
interface IConfirmQuestion extends IQuestionBase {
type: 'confirm';
default?: boolean;
}
Number questions
interface INumberQuestion extends IQuestionBase {
type: 'number';
default?: number;
}
Select questions
select from a list
interface ISelectQuestion extends IQuestionBase {
type: 'select' | 'list';
choices: IChoice[];
default?: string;
}
Search questions
search for a option to select, or through a set of given choices
export interface ISearchQuestion extends IQuestionBase {
type: 'search';
source?: (input: string | undefined) => Promise<IChoice[]>;
choices?: IChoice[];
default?: string;
}
Select a file or directory
A file selector with fuzzy search
export interface IPathSelectQuestion extends IQuestionBase {
type: 'fuzzypath' | 'path';
itemType?: 'file' | 'directory';
allowManualInput?: boolean;
rootPath?: string;
maxDepth?: number;
excludePath?: (pathInfo: IPathInfo) => boolean;
default?: string;
}
Supporting types
export interface IChoice {
value: string;
name?: string;
description?: string;
disabled?: boolean;
}
macros
An object containing 1 or more functions that return a string. These functions can be called as macros from your templates. They can take arguments as well. Config:
export default {
macros: {
repeat: str => `${str}-${str}`,
truncate: (str, len) => (str || '').substring(0, len),
},
};
In an HTML file template:
<p>
This is a paragraph. #repeat('this is a paragraph that is long', 11) Name 2x:
#repeat(${NAME})
</p>
If template is using with a value for NAME of 'TheName', the result would be:
<p>This is a paragraph. this is a p Name 2x: TheName-TheName</p>
Note: macros do not work when transforming file paths.
Command Line
The structure of running the scaffolder is as follows:
scaffolder [destinationDirectory] [--template=<templateName>] [--name=<NAME>] [--dryRun]
Arguments
All arguments to the command line are optional. If you do not supply the arguments on the command line, you will be prompted for their values.
Destination Directory
Directory which is the root of where generated files will be placed. If you specify this value on the command line, it must be the first argument. If it is not specified, the user will be prompted input a value.
template
Name of the template to use for generating files. This will be the name of a sub-folder in the scaffolding directory. If it is not specified, the user will be prompted input a value.
name
Value for the instance NAME variable. If it is not specified, the user will be prompted input a value.
dryRun
If this flag is specified, no files or directories will be created. The contents of what would have been written will be dumped to the console.
overwrite
By default, the scaffolder will skip the output file if it already exists. This flag will cause the file to be overwritten.
parallel
- Read config files in parallel for better startup times.
- Process templates in parallel for much faster processing times.
rerun
Run scaffolder with the exact same command line arguments and answers to questions as the previous run. This can be very useful for debugging your templates.
Making a Template
- Create a folder in the scaffolding directory.
- Create a
scaffolding.config.mjs
file, and configure it as above. - Add files and directories that you will want generated when executing the template.
Parts of the Template
Directories
The scaffolder will generate directories to match what is in the template dir. The name of the directories can contain variable names that will be substituted.
e.g.,
- a template file with the path
component/${SUB_COMPONENT}/index.tsx
- Enter 'MySubComponent' when prompted for SUB_COMPONENT
- Enter 'MyComponent' when prompted for NAME
- Generated file will have path:
MyComponent/MySubComponent/index.tsx
Template Files
File names may contain variable names that will be substituted.
e.g.,
- a template file with the path
component/${NAME}.tsx
- Enter 'MyComponent' when prompted for NAME
- Generated file will have path:
src/MyComponent/MyComponent.tsx
File Contents
Files can be of any type, and have any content. Variable substitution, and calling macros is all done via the Apache 'Velocity Template Language' (VTL) syntax.
VTL Tips
- In for a simple variable replacement use
${NAME}
or$NAME
. - Use backslash
\
to escape the transformation:\${NAME}
will not get transformed. - Conditionals
such as
if/else
can be handy. - Macros are called as
#myMacro()
. You can define your own macros. - VTL is very powerful. To fully take advantage of it, the Velocity Template User Guide.
- The
#include()
macro is a custom implementation. The line which this appears in will be replaced by the contents of the referenced file. The referenced file must reside in the _includes folder. The includes are recursive.
Sample Project
The following is a directory structure for a React project. It supplies two templates: component and page.
- projectRoot
- scaffolding
- _templateHelpers
- index.mjs
- _includes
- fileHeader.txt
- component
- __tests__
- ${NAME}.test.tsx
${NAME}.tsx
styles.ts
scaffolding.config.mjs
- page
${NAME}.tsx
scaffolding.config.mjs
- src
- common
- components
- pages
Component template
Let's look at the component template.
scaffolding/component/scaffolding.config.mjs
import { capitalize } from '../_templateHelpers/index.mjs';
export default {
name: 'React Component',
description: 'for common components',
variables: name => ({
TEST_ID: name.replace(/([a-z])([A-Z])/, '$1-$2').toLowerCase(),
PROPS_INTERFACE: `I${name}Props`,
}),
destinations: ['src/components'], //suggest most common name first
srcRoot: './src', //default value,
afterFileCreated: (path, variables) => {
console.log(`adding ${path} to git`);
return [`git add ${path}`];
},
macros: {
capitalize,
},
};
scaffolding/component/${NAME}.tsx
There are two things to note about this file.
First, it is including a file using the #include()
directive.
Second, it is using a macro (#capitalize()
).
// #include(fileHeader.txt)
import * as React from 'react';
import {ReactElement} from 'react';
import {use${NAME}Styles} from './styles';
// testing macro #capitalize('word')
interface $PROPS_INTERFACE
{
}
export const $NAME = ({}: $PROPS_INTERFACE): ReactElement => {
use${NAME}Styles();
const someVar = 'Need to escape this';
return (
<div data-testid="${TEST_ID}">
{`The name variable is \${someVar}`}
</div>
);
};
scaffolding/component/tests/${NAME}.test.tsx
import * as React from 'react';
import {render} from '@testing-library/react';
import '@testing-library/jest-dom';
import {${NAME}} from '../${NAME}';
describe('<${NAME} />', () => {
it('should render without blowing up', () => {
const result = render(<${NAME} />);
expect(result.getByTestId('${TEST_ID}')).toBeInTheDocument();
});
});
scaffolding/component/styles.ts
import {createUseStyles} from 'react-jss';
import {ITheme} from '@models';
export const use${NAME}Styles = (createUseStyles((theme: ITheme) => {
return ({});
}));
Page Template
This is the page template.
scaffolding/page/scaffolding.config.mjs
import { capitalize } from '../_templateHelpers/index.mjs';
export default {
prompts: () => [
{
name: 'TITLE',
message: 'Enter page heading',
},
],
macros: {
truncate: (str, n) => (str || '').substring(0, n),
},
};
scaffolding/page/${NAME}.tsx
import * as React from 'react';
import {ReactElement} from 'react';
export const ${NAME} = (): ReactElement => {
return (
<div>
<h1>
Title: #truncate(${TITLE}, 40)
</h1>
<p>
This is page content.
</p>
</div>
);
};
scaffolding/_includes/fileHeader.txt
Files in the _includes
folder can be references made by other templates using the
#include()
directive in the template.
/**
* This is a standar file header that I will include.
*/
scaffolding/_templateHelpers/index.mjs
You may find that a number of your templates have duplicated sections. A tip is to create one (or more) helper files to eliminate the duplicate config in your templates.
export const capitalize = str => str.charAt(0).toUpperCase() + str.slice(1);