EJS and HJSON based scaffolding. A simple tool and library to automate the process of setting up new projects or project parts with templates.
Summary:
- Works similar to
cp -r
- No overwrite by default
- Files suffixed with
.hop.ejs
parsed by EJS - Directly access JSON/HJSON Data in the files
- No cli modal boilerplate
- Customize with own javascript
Options:
--help Show help [boolean]
--version Show version number [boolean]
-t, --template Path to template folder [string] [required]
-d, --destination Path to destination folder [string] [default: "."]
-i, --input HJSON input data [string]
-f, --force Overwrites existing files [boolean]
--ed, --ejs-delimiter Which EJS delimiter to use [string] [default: "%"]
npm install hoppla
Hoppla is a tool to quickly set up new folder structures in your projects. The templates can be automated with EJS and JS. Instead of using complicated cli dialogs to provide data for your templates, the data can be directly provided by HJSON or JSON.
The core of hoppla works similar to cp -r
. It merges the template contents into the destination, will however by default not overwrite files.
- Template
templates/example
content:helloworld.txt
- Example cli execution:
npx hoppla -t ./templates/example -d .
- Result:
./helloworld.txt
By default all files suffixed with .hop.ejs
will be parsed with EJS. Other files will be copied as is.
The data specified in the input cli option will be used as EJS data and therefore can be accessed in the template files.
Example content of templates/example/helloworld.txt.hop.ejs
:
Hello <%= input.userName %>
<% if (input.userAge < 18) { %>
Sorry but you are too young.
<% } %>
This file will be scaffolded with the name helloworld.txt
(hoppla removes the .hop.ejs
suffix).
A template is just a folder with content that will be copied recursively to a destination.
There are three places to configure your template:
You can add a file with the name hopplaconfig
to the root of your template. This file will not be copied to the destination. The contents of the file is a HSJON or JSON object which can have the following options:
{
// Default input data used for EJS. Input data provided with the cli option will be merged over the input data specified here.
input: {
aVariable: 'defaultValue'
}
// Files and folders matching these globs will not be copied to the destination
// Keep in mind: this gets checked before the files will be renamed (in case there is a hopplaconfig fileName).
// Think it like this: if you have an excluded file, its hopplaconfig is ignored too!
excludeGlobs: [ '**/tmp', 'TODOS.md' ]
// Files matching these globs will not be parsed with EJS
// By default only files suffixed with ".hop.ejs" are parsed with EJS!
rawGlobs: [ '**/*.png', '**/*.zip' ]
// Custom javascript which will be executed at the start of hoppla, after the tmp directory is created and still empty
init: 'console.log("Hello world")'
// Custom javascript which will be executed, before the template files are copied from the tmp directory to the destination
prepare: 'console.log("How are you?")'
// Custom javascript which will be executed at the end of hoppla, after the template files are copied to the destination
finalize: 'console.log("Goodby")'
}
Coming back to the helloworld.txt.hop.ejs file from the Basics chapter. Its content can look like this:
###hopplaconfig {
// Create the file in the destination with a custom filename.
// The string will be joined with the parent folder name before interpretation, making it viable
// for various tricks like:
// '.' (a folder would in this case copy its contents into its parent)
// 'newFolderX/newFolderY/hello.txt' (the file would be copied into the completely new folders newFolderX/newFolderY)
fileName: 'hello.<%= input.userName %>.txt',
// Can be set to "true" and the file will not be copied to the destination
exclude: false,
// Copy file as is / not EJS output
raw: false,
// Custom javascript hook (have a look at the specific chapter)
generate: 'console.log(hoppla.input.userName); return hoppla.generate(hoppla.input);'
} hopplaconfig###
Hello <%= input.userName %>
The ###hopplaconfig hopplaconfig### block is not included in the destination output.
Every file in the template accepts a second file with the name filename.hopplaconfig
. filename
is the filename of the file to configure (without the .hop.ejs
suffix). So the hopplaconfig filename for our example helloworld.txt.hop.ejs
is helloworld.txt.hopplaconfig
This is espacially useful for folders and binary files which cannot be configured inline.
The content of the separate config file is JSON or HJSON. The options are the same as if you use the inline configuration.
Inside of your custom javascript you have access to a hoppla
variable which is an object with several properties:
Customize the template with javascript at the start and end of the hoppla process.
{
input: { hello: 'world' }
prepare: 'console.log("prepareHopplaObj", hoppla)'
/*
* Output:
* 'prepareHopplaObj' {
* input: { hello: 'world' },
* template: '/home/ubuntu/projects/templates/helloworld',
* tmp: '/home/ubuntu/projects/templates/new-helloworld/tmp-hoppla/helloworld'
* destination: '/home/ubuntu/projects/new-helloworld'
* require: Function
* }
*/
finalize: 'console.log("finalizeHopplaObj", hoppla)'
/*
* Output:
* 'finalizeHopplaObj' {
* error: Error
* input: { hello: 'world' },
* template: '/home/ubuntu/projects/templates/helloworld',
* destination: '/home/ubuntu/projects/new-helloworld'
* require: Function
* }
*/
}
This is a hook where you can add file specific javascript. In the js context is a hoppla object with a generate function. Everytime this generate function is called, it will create a copy of the file (counter.txt in the example) and interpret the new copy with the input you specified as the first argument of hoppla.generate. Only the generated copies will be finally copied to the destination!
This allows you to not only add extra input variables from js for a single file but also to generate multiple copies of the file with new file names.
Example:
counter.txt:
###hopplaconfig {
fileName: 'count.<%= input.count %>.txt'
// Always return the promise from hoppla.generate!
generate: 'return hoppla.require('tpl-helpers/hello.js')(hoppla)'
} ###hopplaconfig
Counting <%= input.count %>
tpl-helpers/hello.js:
module.exports = function(hoppla) {
console.log('generateHopplaObj', hoppla);
/*
* Output:
* 'generateHopplaObj' {
* generate: Function,
* input: { userName: 'john' }
* require: Function
* }
*/
var promise = Promise.resolve();
for(var i = 0; i < 3; i++) {
promise = promise.then(() => {
input.count = i;
// Creates a temporary copy of counter.txt which will be parsed with the new input
// The copy will NOT recursively interpret the generate =)
// hoppla.generate is asynchronous and returns a Promise!
return hoppla.generate(input);
})
}
return promise;
}
Destination result:
- count.0.txt
- count.1.txt
- count.2.txt
If you use the generate option, you also have to use the hoppla.generate function atleast once. Otherwise the file would not be copied to the destination.
You can just return a promise in custom-js options:
{
prepare: 'return Promise.resolve().then(() => { console.log("async") })'
}
Use this to require other javascript files from your template. The path is relative to the template directory.
{
excludeGlobs: [ 'tpl-helpers' ]
prepare: 'hoppla.require("tpl-helpers/prepare.js")()'
}
Only exists in the hoppla object of finalize. If hoppla somewhere throwed an error, it will be accessible in hoppla.error
. This allows you to add sensible logic to your custom javascript:
{
finalize: 'if (hoppla.error) console.log("Please restart windows")'
}
Only exists in the hoppla object of prepare/finalize. Use this to call separate hoppla-js processes. Check out the "Call hoppla from javascript" chapter for more details.
Instead of using the cli for hoppla, you also can instead require and call it with javascript like in this example:
const { hoppla } = require('hoppla');
Promise.resolve()
.then(() => {
return hoppla({
input: {
userName: 'john'
},
template: 'folder/to/template',
destination: 'folder/to/destination'
// force: true,
// ejsOptions: { customEjsOptions... }
})
})
.then(() => {
// hoppla is done
})
You can use copyRecursive, to recursive merge two directories together. Files only existing in the destination will be kept there.
const path = require('path');
const { copyRecursive } = require('hoppla');
// Merges proj/your-src-dir into proj/your-dest-dir
copyRecursive({
source: path.join(__dirname, 'your-src-dir'),
destination: path.join(__dirname, 'your-dest-dir'),
force: true, // overwrite files, default: false
silent: true, // only output errors, default: false
baseDir: '/', // logs are relative to this dir, default: your-dest-dir/..
exclude: [
'**/.git'
]
})