├── test
├── data.json
├── test-1.hbs
└── verify-1.html
├── .gitignore
├── .editorconfig
├── .travis.yml
├── README.md
├── package.json
└── src
└── index.js
/test/data.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Some Title",
3 | "body": "This is the body"
4 | }
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 | Thumbs.db
3 | node_modules/
4 | *.log
5 | *.log*
6 | *.pid
7 | *.seed
8 | lib/
9 | test/test*.html
10 |
--------------------------------------------------------------------------------
/test/test-1.hbs:
--------------------------------------------------------------------------------
1 |
2 |
{{title}}
3 |
4 | {{body}}
5 |
6 | {{br 2}}
7 |
8 |
--------------------------------------------------------------------------------
/test/verify-1.html:
--------------------------------------------------------------------------------
1 |
2 |
Some Title
3 |
4 | This is the body
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | end_of_line = lf
5 | insert_final_newline = true
6 | trim_trailing_whitespace = true
7 | charset = utf-8
8 | indent_style = space
9 | indent_size = 2
10 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: false
2 | language: node_js
3 | cache:
4 | directories:
5 | - node_modules
6 | node_js:
7 | - '0.12'
8 | - v4
9 | - stable
10 | after_success:
11 | - 'travis-after-all && npm run semantic-release'
12 | env:
13 | global:
14 | - secure: TuAEDPV6xmK6b0GF9oOTTdeLeA/2X3WDX3gfDAACnjKy3meADj0gIdZw1847jchLvKG5Hh+a3fpi8IA07yYqz/lqiNUbiuei18aScLd37Gq9U8ew0QoylS+c5j4KoiJLjzEHoz2t+yMUiDJbiYd69KHePfutFXvV2YbctL9PLWsJheDSpjqXdl2OTKTrfdvidOqy+U8H/1wuYozCJLw23NsLOafZPEGaQf5KCvdFUhGV5lbfVuB7JHhPvgDpKjw8+eHv6P6oUfiXbDybLlz0Taqp3LVb7B+9UzAzhIzPyNBSpmsT0tn7yfGeBTeRRXgN78S8kN0rtaSJFEuGJq0tr/d+1u3dUHLz4ny38kOx9Vj8vgbd3hvjJ5xdWr6un1cVTO7c5vmX1Rw3Ss6T3pMkdd66JNqktvF43EUMfGk6LoI0j7O/qjvELrxaPIwQfeDMkYukLrR7KW5endcY78NakIwPE9MYzEronn0Yt11vTyF3GBit+/hGUDS9zgxFBGsESDmF8S/ApsLn0jaRKUduV6nc2wnjd2Q0mejApcSnxFCQm8ii6scREifrHw2wS72uUae/cDVu2tKx/xZevcGIWEG0I/Hu+xczFIZ+iAXZOst2aKeaZw2HRku91SVX0VJICIgzQi9tvWAmVyeR5V0hpG0xSvg5t5tNN9bTJbXEoUw=
15 | - secure: fInupYDLX9WLepc+D6MXBwOlZEff8uXof8OL63RU4jphBmD9q5NZi55bcHYT44kpYXHQ1YsdXazbeS/3iKitK0rz2fCprWNcLRKYXNVTTetpLoWIgrn4+nVNbeXC5KTLooAmnQns5kJ2FGDHwptSUCSIdJctFFzzTb4ju2MqTJotLa851TcNQtgh/HTkrpIb25ytPTZeomVJproEuzYC6aUz09bMI5pnhsn7gshb5AcrBiOEKByQfhF1sxoC5ZYXZ479ZagGTzqFOgC1FxzEWrMzwcd2uQrwIUYoLrI9xZg89tJkf//BG8U+C0McJTL4kHXIxxw8jTHgxkbrtRD+4c8NzaLkGkCkuSb7llpDMMjDaPrlehvruczsh58EoL29XESgFl3lomdUC+jOgDfNLCpUDYjz0eBUjqOSaPNdlvOXiHsLzolwT8IVTKSTF/7Y4HeZN0WKh0rNxY6L4z0+6Gs1KhctAIoYprGnq52enu76cVShVTdN6gbxYCXsY4yyymxy0sPZHViZm6i7e+IJVEkVnOOZQ7p5f6LIAhUz9UOijHrx8YqY+a87w8CqjIEFSkn2ii0bHoC9jQAXvCjsFj0KxMz0RNiBQwTjVvuaxNq+HzXTNNL3IX7OHYO03F4mcABBc6Du3U3tvbp3mRpw9pRJS9FWJdy+Xd44ld+FJFY=
16 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # hbs-cli
2 |
3 | This is a tool to render [handlebars](http://handlebarsjs.com) templates, with the ability to require in Partials, Helpers and JSON Data.
4 |
5 | ## Installation
6 |
7 | ```sh
8 | $ npm install --save-dev hbs-cli
9 | ```
10 |
11 | ## Usage
12 |
13 | ```sh
14 | Usage:
15 | hbs --version
16 | hbs --help
17 | hbs [-P ]... [-H ]... [-D ]... [-o ] [--] ()
18 |
19 | -h, --help output usage information
20 | -v, --version output the version number
21 | -o, --output Directory to output rendered templates, defaults to cwd
22 | -e, --extension Output extension of generated files, defaults to html
23 | -s, --stdout Output to standard output
24 | -i, --stdin Receive data directly from stdin
25 | -P, --partial ... Register a partial (use as many of these as you want)
26 | -H, --helper ... Register a helper (use as many of these as you want)
27 |
28 | -D, --data ... Parse some data
29 |
30 | Examples:
31 |
32 | hbs --helper handlebars-layouts --partial ./templates/layout.hbs -- ./index.hbs
33 | hbs --data ./package.json --data ./extra.json ./homepage.hbs --output ./site/
34 | hbs --helper ./helpers/* --partial ./partials/* ./index.hbs # Supports globs!
35 | ```
36 |
37 | _* Yarn and NPM expand globs, so if you're using this in an NPM script make sure you wrap globs in quotes. For example:_
38 | ```sh
39 | hbs index.hbs --partial 'partials/*.hbs'
40 | ```
41 |
42 | ## Using Helpers
43 |
44 | In order to use Handlebar helpers you can simply create a folder with all your helpers in a js file each. These modules must export a register function which gets the Handlebars instance passed through its first parameter.
45 |
46 | ```js
47 | // src/template_helper/times.js
48 | var times = function () {};
49 |
50 | times.register = function (Handlebars) {
51 | Handlebars.registerHelper('times', function(n, block) {
52 | var accum = '';
53 | for(var i = 0; i < n; ++i)
54 | accum += block.fn(i);
55 | return accum;
56 | });
57 | };
58 |
59 | module.exports = times;
60 | ```
61 |
62 | Now you are able to use the `times` function within your Handlebars template such as this:
63 |
64 | ```
65 | {{#times 10}}
66 | {{this}}
67 | {{/times}}
68 | ```
69 |
70 | To compile this template you may run this command:
71 |
72 | ```bash
73 | hbs --helper ./src/template_helper/**/*.js --data src/data.json src/templates/**/*.hbs --output dist/
74 | ```
75 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "hbs-cli",
3 | "version": "1.4.1",
4 | "description": "A CLI tool for rendering Handlebars templates",
5 | "homepage": "http://github.com/keithamus/hbs-cli/issues",
6 | "bugs": "http://github.com/keithamus/hbs-cli/issues",
7 | "license": "MIT",
8 | "author": "Keith Cirkel (http://keithcirkel.co.uk)",
9 | "files": [
10 | "lib/*.js"
11 | ],
12 | "main": "lib/index.js",
13 | "bin": {
14 | "hbs": "lib/index.js"
15 | },
16 | "repository": {
17 | "type": "git",
18 | "url": "git+ssh://git@github.com/keithamus/hbs-cli.git"
19 | },
20 | "scripts": {
21 | "lint": "eslint src test --ignore-path .gitignore",
22 | "prepublish": "babel src -d lib",
23 | "pretest": "npm run lint",
24 | "semantic-release": "semantic-release pre && npm publish && semantic-release post",
25 | "test": "npm run test:run:1 && npm run test:verify:1 && npm run test:run:2 && npm run test:verify:2 && npm run test:run:3 && npm run test:verify:3",
26 | "test:run:1": "node . -H handlebars-helper-br -D ./test/data.json -o test test/test-1.hbs",
27 | "test:verify:1": "diff test/test-1.html test/verify-1.html",
28 | "test:run:2": "node . -s -H handlebars-helper-br -D ./test/data.json test/test-1.hbs > test/test-2.html",
29 | "test:verify:2": "diff test/test-2.html test/verify-1.html",
30 | "test:run:3": "cat ./test/data.json | node . -s -H handlebars-helper-br --stdin test/test-1.hbs > test/test-3.html",
31 | "test:verify:3": "diff test/test-3.html test/verify-1.html",
32 | "watch": "npm run prepublish -- -w"
33 | },
34 | "config": {
35 | "ghooks": {
36 | "commit-msg": "validate-commit-msg",
37 | "pre-commit": "npm test"
38 | }
39 | },
40 | "babel": {
41 | "compact": false,
42 | "ignore": "node_modules",
43 | "loose": "all",
44 | "optional": "runtime",
45 | "retainLines": true,
46 | "stage": 2
47 | },
48 | "eslintConfig": {
49 | "extends": "strict",
50 | "parser": "babel-eslint",
51 | "rules": {
52 | "no-console": 0,
53 | "no-process-exit": 0
54 | }
55 | },
56 | "dependencies": {
57 | "babel-runtime": "^5.8.34",
58 | "debug": "^2.2.0",
59 | "fs-promise": "^0.3.1",
60 | "get-stdin": "^8.0.0",
61 | "glob-promise": "^1.0.4",
62 | "handlebars": "^4.0.5",
63 | "lodash.merge": "^4.6.2",
64 | "minimist": "^1.2.0",
65 | "mkdirp-then": "^1.2.0",
66 | "resolve": "^1.1.6"
67 | },
68 | "devDependencies": {
69 | "babel": "^5.8.34",
70 | "babel-eslint": "^6.1.1",
71 | "eslint": "^1.10.3",
72 | "eslint-config-strict": "^7.0.4",
73 | "eslint-plugin-filenames": "^0.2.0",
74 | "ghooks": "^1.0.1",
75 | "handlebars-helper-br": "^0.1.0",
76 | "semantic-release": "^4.3.5",
77 | "travis-after-all": "^1.4.4",
78 | "validate-commit-msg": "^2.8.2"
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | import { resolve as resolvePath, basename, extname } from 'path';
3 | import Handlebars from 'handlebars';
4 | import minimist from 'minimist';
5 | import glob from 'glob-promise';
6 | import packageJson from '../package.json';
7 | import resolveNode from 'resolve';
8 | import { readFile, writeFile } from 'fs-promise';
9 | import merge from 'lodash.merge';
10 | import mkdirp from 'mkdirp-then';
11 | import getStdin from 'get-stdin';
12 | const debug = require('debug')('hbs');
13 | function resolve(file, options) {
14 | return new Promise((resolvePromise, reject) => resolveNode(file, options, (error, path) => {
15 | if (error) {
16 | reject(error);
17 | } else {
18 | resolvePromise(path);
19 | }
20 | }));
21 | }
22 | export async function resolveModuleOrGlob(path, cwd = process.cwd()) {
23 | try {
24 | debug(`Trying to require ${path} as a node_module`);
25 | return [ await resolve(path, { basedir: cwd }) ];
26 | } catch (error) {
27 | debug(`${path} is glob or actual file, expanding...`);
28 | return await glob(path, { cwd });
29 | }
30 | }
31 |
32 | export async function expandGlobList(globs) {
33 | if (typeof globs === 'string') {
34 | globs = [ globs ];
35 | }
36 | if (Array.isArray(globs) === false) {
37 | throw new Error(`expandGlobList expects Array or String, given ${typeof globs}`);
38 | }
39 | return (await Promise.all(
40 | globs.map((path) => resolveModuleOrGlob(path))
41 | )).reduce((total, current) => total.concat(current), []);
42 | }
43 |
44 | export function addHandlebarsHelpers(files) {
45 | files.forEach((file) => {
46 | debug(`Requiring ${file}`);
47 | const handlebarsHelper = require(file); // eslint-disable-line global-require
48 | if (handlebarsHelper && typeof handlebarsHelper.register === 'function') {
49 | debug(`${file} has a register function, registering with handlebars`);
50 | handlebarsHelper.register(Handlebars);
51 | } else {
52 | console.error(`WARNING: ${file} does not export a 'register' function, cannot import`);
53 | }
54 | });
55 | }
56 |
57 | export async function addHandlebarsPartials(files) {
58 | await Promise.all(files.map(async function registerPartial(file) {
59 | debug(`Registering partial ${file}`);
60 | Handlebars.registerPartial(basename(file, extname(file)), await readFile(file, 'utf8'));
61 | }));
62 | }
63 |
64 | export async function addObjectsToData(objects) {
65 | if (typeof objects === 'string') {
66 | objects = [ objects ];
67 | }
68 | if (Array.isArray(objects) === false) {
69 | throw new Error(`addObjectsToData expects Array or String, given ${typeof objects}`);
70 | }
71 | const dataSets = [];
72 | const files = await expandGlobList(objects.filter((object) => {
73 | try {
74 | debug(`Attempting to parse ${object} as JSON`);
75 | dataSets.push(JSON.parse(object));
76 | return false;
77 | } catch (error) {
78 | return true;
79 | }
80 | }));
81 | const fileContents = await Promise.all(
82 | files.map(async function registerPartial(file) {
83 | debug(`Loading JSON file ${file}`);
84 | return JSON.parse(await readFile(file, 'utf8'));
85 | })
86 | );
87 | return merge({}, ...dataSets.concat(fileContents));
88 | }
89 |
90 | export async function getStdinData() {
91 | const text = await getStdin();
92 | try {
93 | debug(`Attempting to parse ${text} as JSON`);
94 | return JSON.parse(text);
95 | } catch (error) {
96 | throw new Error(`stdin cannot be parsed as JSON`);
97 | }
98 | }
99 |
100 | export async function renderHandlebarsTemplate(
101 | files, outputDirectory = process.cwd(),
102 | outputExtension = 'html', data = {}, stdout = false) {
103 | await Promise.all(files.map(async function renderTemplate(file) {
104 | debug(`Rendering template ${file} with data`, data);
105 | const path = resolvePath(outputDirectory, `${basename(file, extname(file))}.${outputExtension}`);
106 | const htmlContents = Handlebars.compile(await readFile(file, 'utf8'))(data);
107 | if (stdout) {
108 | await process.stdout.write(htmlContents, 'utf8');
109 | } else {
110 | await mkdirp(outputDirectory);
111 | await writeFile(path, htmlContents, 'utf8');
112 | debug(`Wrote ${path}`);
113 | console.error(`Wrote ${path} from ${file}`);
114 | }
115 | }));
116 | }
117 |
118 | if (require.main === module) {
119 | const options = minimist(process.argv.slice(2), {
120 | string: [
121 | 'output',
122 | 'extension',
123 | 'partial',
124 | 'helper',
125 | 'data',
126 | ],
127 | boolean: [
128 | 'version',
129 | 'help',
130 | 'stdout',
131 | 'stdin',
132 | ],
133 | alias: {
134 | 'v': 'version',
135 | 'h': 'help',
136 | 'o': 'output',
137 | 'e': 'extension',
138 | 's': 'stdout',
139 | 'i': 'stdin',
140 | 'D': 'data',
141 | 'P': 'partial',
142 | 'H': 'helper',
143 | },
144 | });
145 | debug('Parsed argv', options);
146 | if (options.version) {
147 | console.error(packageJson.version);
148 | } else if (options.help || !options._ || !options._.length) {
149 | console.error(`
150 | Usage:
151 | hbs --version
152 | hbs --help
153 | hbs [-P ]... [-H ]... [-D ]... [-o ] [--] ()
154 |
155 | -h, --help output usage information
156 | -v, --version output the version number
157 | -o, --output Directory to output rendered templates, defaults to cwd
158 | -e, --extension Output extension of generated files, defaults to html
159 | -s, --stdout Output to standard output
160 | -i, --stdin Receive data directly from stdin
161 | -P, --partial ... Register a partial (use as many of these as you want)
162 | -H, --helper ... Register a helper (use as many of these as you want)
163 |
164 | -D, --data ... Parse some data
165 |
166 | Examples:
167 |
168 | hbs --helper handlebars-layouts --partial ./templates/layout.hbs -- ./index.hbs
169 | hbs --data ./package.json --data ./extra.json ./homepage.hbs --output ./site/
170 | hbs --helper ./helpers/* --partial ./partials/* ./index.hbs # Supports globs!
171 | `);
172 | } else {
173 | const setup = [];
174 | let data = {};
175 | if (options.helper) {
176 | debug('Setting up helpers', options.helper);
177 | setup.push(expandGlobList(options.helper).then(addHandlebarsHelpers));
178 | }
179 | if (options.partial) {
180 | debug('Setting up partials', options.partial);
181 | setup.push(expandGlobList(options.partial).then(addHandlebarsPartials));
182 | }
183 | if (options.data) {
184 | debug('Setting up data', options.data);
185 | setup.push(addObjectsToData(options.data).then((result) => data = result));
186 | }
187 | if (options.stdin) {
188 | debug('Setting up stdin', options.stdin);
189 | setup.push(getStdinData().then((stdinData) => data = stdinData));
190 | }
191 | Promise.all(setup)
192 | .then(() => expandGlobList(options._))
193 | .then((files) => renderHandlebarsTemplate(files, options.output, options.extension, data, options.stdout))
194 | .catch((error) => {
195 | console.error(error.stack || error);
196 | process.exit(1);
197 | });
198 | }
199 | }
200 |
--------------------------------------------------------------------------------