├── 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 | --------------------------------------------------------------------------------