├── test ├── dependencies │ ├── dependency3.pug │ ├── include2.pug │ ├── dependency2.pug │ └── dependency1.pug └── index.js ├── .gitignore ├── .travis.yml ├── package.json ├── LICENSE.md ├── README.md ├── HISTORY.md └── index.js /test/dependencies/dependency3.pug: -------------------------------------------------------------------------------- 1 | strong dependency3 2 | -------------------------------------------------------------------------------- /test/dependencies/include2.pug: -------------------------------------------------------------------------------- 1 | include dependency2.pug 2 | -------------------------------------------------------------------------------- /test/dependencies/dependency2.pug: -------------------------------------------------------------------------------- 1 | include dependency3.pug 2 | -------------------------------------------------------------------------------- /test/dependencies/dependency1.pug: -------------------------------------------------------------------------------- 1 | html 2 | body 3 | block container 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.seed 2 | *.log 3 | *.csv 4 | *.dat 5 | *.out 6 | *.pid 7 | *.gz 8 | pids 9 | logs 10 | results 11 | npm-debug.log 12 | node_modules 13 | coverage 14 | cov-pt* 15 | test/temp 16 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: false 3 | 4 | node_js: 5 | - "0.10" 6 | - "0.12" 7 | - "4" 8 | - "6" 9 | 10 | after_success: 11 | - npm run coverage 12 | - npm i codecov 13 | - codecov -f ./coverage/lcov.info 14 | 15 | notifications: 16 | email: 17 | on_success: never 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pug-cli", 3 | "version": "1.0.0-alpha6", 4 | "description": "Pug's CLI interface", 5 | "bin": { 6 | "pug": "./index.js", 7 | "pug-cli": "./index.js" 8 | }, 9 | "preferGlobal": true, 10 | "keywords": [], 11 | "dependencies": { 12 | "chalk": "^1.0.0", 13 | "commander": "^2.8.1", 14 | "pug": "^2.0.0-alpha7", 15 | "mkdirp": "^0.5.1" 16 | }, 17 | "devDependencies": { 18 | "istanbul": "*", 19 | "mocha": "*", 20 | "rimraf": "^2.3.4" 21 | }, 22 | "scripts": { 23 | "test": "mocha -R spec --bail", 24 | "precoverage": "rimraf coverage && rimraf cov-pt*", 25 | "coverage": "istanbul cover --report none --dir cov-pt0 node_modules/mocha/bin/_mocha -- -R dot", 26 | "postcoverage": "istanbul report --include cov-pt\\*/coverage.json && rimraf cov-pt*" 27 | }, 28 | "repository": { 29 | "type": "git", 30 | "url": "https://github.com/pugjs/pug-cli.git" 31 | }, 32 | "author": "TJ Holowaychuk ", 33 | "maintainers": [ 34 | "Timothy Gu ", 35 | "Forbes Lindesay " 36 | ], 37 | "license": "MIT" 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010-2014 TJ Holowaychuk 2 | Copyright (c) 2013-2015 Forbes Lindesay 3 | Copyright (c) 2015 Tiancheng "Timothy" Gu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pug-cli 2 | 3 | Pug's CLI interface 4 | 5 | [![Build Status](https://img.shields.io/travis/pugjs/pug-cli/master.svg)](https://travis-ci.org/pugjs/pug-cli) 6 | [![Dependency Status](https://img.shields.io/david/pugjs/pug-cli.svg)](https://david-dm.org/pugjs/pug-cli) 7 | [![NPM version](https://img.shields.io/npm/v/pug-cli.svg)](https://www.npmjs.org/package/pug-cli) 8 | [![Coverage Status](https://img.shields.io/codecov/c/github/pugjs/pug-cli.svg)](https://codecov.io/gh/pugjs/pug-cli) 9 | 10 | ## Usage 11 | 12 | ``` 13 | $ pug [options] [dir|file ...] 14 | ``` 15 | 16 | Render ``s and all files in ``s. If no files are specified, 17 | input is taken from standard input and output to standard output. 18 | 19 | ### Options 20 | 21 | ``` 22 | -h, --help output usage information 23 | -V, --version output the version number 24 | -O, --obj JSON/JavaScript options object or file 25 | -o, --out output the rendered HTML or compiled JavaScript to 26 | 27 | -p, --path filename used to resolve includes 28 | -b, --basedir path used as root directory to resolve absolute includes 29 | -P, --pretty compile pretty HTML output 30 | -c, --client compile function for client-side runtime.js 31 | -n, --name the name of the compiled template (requires --client) 32 | -D, --no-debug compile without debugging (smaller functions) 33 | -w, --watch watch files for changes and automatically re-render 34 | -E, --extension specify the output file extension 35 | -s, --silent do not output logs 36 | --name-after-file name the template after the last section of the file 37 | path (requires --client and overriden by --name) 38 | --doctype specify the doctype on the command line (useful if it 39 | is not specified by the template) 40 | ``` 41 | 42 | ### Examples 43 | 44 | Render all files in the `templates` directory: 45 | 46 | ``` 47 | $ pug templates 48 | ``` 49 | 50 | Create `{foo,bar}.html`: 51 | 52 | ``` 53 | $ pug {foo,bar}.pug 54 | ``` 55 | 56 | Using `pug` over standard input and output streams: 57 | 58 | ``` 59 | $ pug < my.pug > my.html 60 | $ echo "h1 Pug!" | pug 61 | ``` 62 | 63 | Render all files in `foo` and `bar` directories to `/tmp`: 64 | 65 | ``` 66 | $ pug foo bar --out /tmp 67 | ``` 68 | 69 | Specify options through a string: 70 | 71 | ``` 72 | $ pug -O '{"doctype": "html"}' foo.pug 73 | # or, using JavaScript instead of JSON 74 | $ pug -O "{doctype: 'html'}" foo.pug 75 | ``` 76 | 77 | Specify options through a file: 78 | 79 | ``` 80 | $ echo "exports.doctype = 'html';" > options.js 81 | $ pug -O options.js foo.pug 82 | # or, JSON works too 83 | $ echo '{"doctype": "html"}' > options.json 84 | $ pug -O options.json foo.pug 85 | ``` 86 | 87 | ## Installation 88 | 89 | npm install pug-cli -g 90 | 91 | ## License 92 | 93 | MIT 94 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | As with most npm modules, this project adheres to 3 | [Semantic Versioning](http://semver.org/). 4 | 5 | ## [1.0.0-alpha6] - 2016-06-01 6 | 7 | ### Added 8 | - `--basedir` option is added for easier specification of that Pug option. 9 | - Node.js v6 is now tested. 10 | 11 | ## [1.0.0-alpha5] - 2016-05-18 12 | 13 | ### Changed 14 | - Files ending `.jade` are now recognized as Pug templates when a directory is provided as input. 15 | 16 | ## [1.0.0-alpha4] - 2016-05-18 17 | 18 | ### Changed 19 | - When `--watch` is specified, `watch` is no longer passed as an option to Pug. This should have no effect on users. 20 | 21 | ### Fixed 22 | - Fixed `--no-debug` option ([#23]) 23 | 24 | ## [1.0.0-alpha3] - 2016-05-18 25 | 26 | ### Added 27 | - Node.js module as option file is supported as well. 28 | - Some examples have been added to the documentation on how to use `-O`. 29 | 30 | ## [1.0.0-alpha2] - 2016-05-18 31 | 32 | ### Changed 33 | - Pug has been updated to the latest alpha. 34 | - Unused dependencies have been removed. 35 | 36 | ## [1.0.0-alpha1] - 2016-03-23 37 | 38 | ### Removed 39 | - `-H` option, deprecated in 0.1.0, has been removed. 40 | - Support for `SIGINT` as signal for EOF, deprecated in 0.1.1, has been removed. 41 | 42 | ### Changed 43 | - The package is renamed to `pug-cli`. 44 | 45 | ### Fixed 46 | - Support for Windows has been fixed. 47 | 48 | ## [0.1.1] - 2015-09-29 49 | ### Deprecated 50 | - Using `SIGINT` (`^C`) to signify end of input in standard input mode is deprecated, and will be removed in 1.0.0. Instead, use `^D` which means "end of file." 51 | 52 | ### Fixed 53 | - Fallback on options specified with `-O` if the corresponding CLI option is not specified. 54 | - Mark this module as preferred to be installed globally. 55 | - Fix copyright and update maintainers in package.json. 56 | - Fix links in HISTORY.md. 57 | - Fix compiling directories whose paths contain backslashes (`\`) (#11). 58 | 59 | ## [0.1.0] - 2015-07-24 60 | ### Added 61 | - Silent mode (`-s`, `--silent`) which disables printing unimportant messages (#3, pugjs/pug#1905). 62 | 63 | ### Changed 64 | - Hierarchy mode (`-H`, `--hierarchy`) is made the default. 65 | - Both versions of Pug and the CLI are printed with `-V` or `--version`. 66 | - Unescaped Unicode line and paragraph separators (`U+2028` and `U+2029`) is now allowed in the `-O` option only when the input is considered to be JSON (#5, pugjs/pug#1949). 67 | - Non-JSON object files are allowed for the `-O` option as long as it can be parsed with the `eval()` function. 68 | 69 | ### Deprecated 70 | - Since the hierarchy mode (`-H`, `--hierarchy`) is made the default, the 71 | option is now redundant and will be removed in 1.0.0. 72 | 73 | ### Fixed 74 | - Capitalization in help message is kept consistent. 75 | - Fix grammar error in the help message (by @didoarellano). 76 | - Fix watch mode in more than one level of dependency hierarchy (pugjs/pug#1888). 77 | 78 | ## 0.0.1 - 2015-06-02 79 | ### Added 80 | - Initial release. 81 | 82 | [unreleased]: https://github.com/pugjs/pug-cli/compare/1.0.0-alpha5...master 83 | [1.0.0-alpha5]: https://github.com/pugjs/pug-cli/compare/1.0.0-alpha4...1.0.0-alpha5 84 | [1.0.0-alpha4]: https://github.com/pugjs/pug-cli/compare/1.0.0-alpha3...1.0.0-alpha4 85 | [1.0.0-alpha3]: https://github.com/pugjs/pug-cli/compare/1.0.0-alpha2...1.0.0-alpha3 86 | [1.0.0-alpha2]: https://github.com/pugjs/pug-cli/compare/1.0.0-alpha1...1.0.0-alpha2 87 | [1.0.0-alpha1]: https://github.com/pugjs/pug-cli/compare/0.1.1...1.0.0-alpha1 88 | [0.1.1]: https://github.com/pugjs/pug-cli/compare/0.1.0...0.1.1 89 | [0.1.0]: https://github.com/pugjs/pug-cli/compare/0.0.1...0.1.0 90 | 91 | [#23]: https://github.com/pugjs/pug-cli/issues/23 92 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | var fs = require('fs'); 6 | var path = require('path'); 7 | var program = require('commander'); 8 | var mkdirp = require('mkdirp'); 9 | var chalk = require('chalk'); 10 | var pug = require('pug'); 11 | 12 | var basename = path.basename; 13 | var dirname = path.dirname; 14 | var resolve = path.resolve; 15 | var normalize = path.normalize; 16 | var join = path.join; 17 | var relative = path.relative; 18 | 19 | // Pug options 20 | 21 | var options = {}; 22 | 23 | // options 24 | 25 | program 26 | .version( 27 | 'pug version: ' + require('pug/package.json').version + '\n' + 28 | 'pug-cli version: ' + require( './package.json').version 29 | ) 30 | .usage('[options] [dir|file ...]') 31 | .option('-O, --obj ', 'JSON/JavaScript options object or file') 32 | .option('-o, --out ', 'output the rendered HTML or compiled JavaScript to ') 33 | .option('-p, --path ', 'filename used to resolve includes') 34 | .option('-b, --basedir ', 'path used as root directory to resolve absolute includes') 35 | .option('-P, --pretty', 'compile pretty HTML output') 36 | .option('-c, --client', 'compile function for client-side') 37 | .option('-n, --name ', 'the name of the compiled template (requires --client)') 38 | .option('-D, --no-debug', 'compile without debugging (smaller functions)') 39 | .option('-w, --watch', 'watch files for changes and automatically re-render') 40 | .option('-E, --extension ', 'specify the output file extension') 41 | .option('-s, --silent', 'do not output logs') 42 | .option('--name-after-file', 'name the template after the last section of the file path (requires --client and overriden by --name)') 43 | .option('--doctype ', 'specify the doctype on the command line (useful if it is not specified by the template)') 44 | 45 | 46 | program.on('--help', function(){ 47 | console.log(' Examples:'); 48 | console.log(''); 49 | console.log(' # Render all files in the `templates` directory:'); 50 | console.log(' $ pug templates'); 51 | console.log(''); 52 | console.log(' # Create {foo,bar}.html:'); 53 | console.log(' $ pug {foo,bar}.pug'); 54 | console.log(''); 55 | console.log(' # Using `pug` over standard input and output streams'); 56 | console.log(' $ pug < my.pug > my.html'); 57 | console.log(' $ echo \'h1 Pug!\' | pug'); 58 | console.log(''); 59 | console.log(' # Render all files in `foo` and `bar` directories to `/tmp`:'); 60 | console.log(' $ pug foo bar --out /tmp'); 61 | console.log(''); 62 | console.log(' # Specify options through a string:'); 63 | console.log(' $ pug -O \'{"doctype": "html"}\' foo.pug'); 64 | console.log(' # or, using JavaScript instead of JSON'); 65 | console.log(' $ pug -O "{doctype: \'html\'}" foo.pug'); 66 | console.log(''); 67 | console.log(' # Specify options through a file:'); 68 | console.log(' $ echo "exports.doctype = \'html\';" > options.js'); 69 | console.log(' $ pug -O options.js foo.pug'); 70 | console.log(' # or, JSON works too'); 71 | console.log(' $ echo \'{"doctype": "html"}\' > options.json'); 72 | console.log(' $ pug -O options.json foo.pug'); 73 | console.log(''); 74 | }); 75 | 76 | program.parse(process.argv); 77 | 78 | // options given, parse them 79 | 80 | if (program.obj) { 81 | options = parseObj(program.obj); 82 | } 83 | 84 | /** 85 | * Parse object either in `input` or in the file called `input`. The latter is 86 | * searched first. 87 | */ 88 | function parseObj (input) { 89 | try { 90 | return require(path.resolve(input)); 91 | } catch (e) { 92 | var str; 93 | try { 94 | str = fs.readFileSync(program.obj, 'utf8'); 95 | } catch (e) { 96 | str = program.obj; 97 | } 98 | try { 99 | return JSON.parse(str); 100 | } catch (e) { 101 | return eval('(' + str + ')'); 102 | } 103 | } 104 | } 105 | 106 | [ 107 | ['path', 'filename'], // --path 108 | ['debug', 'compileDebug'], // --no-debug 109 | ['client', 'client'], // --client 110 | ['pretty', 'pretty'], // --pretty 111 | ['basedir', 'basedir'], // --basedir 112 | ['doctype', 'doctype'], // --doctype 113 | ].forEach(function (o) { 114 | options[o[1]] = program[o[0]] !== undefined ? program[o[0]] : options[o[1]]; 115 | }); 116 | 117 | // --name 118 | 119 | if (typeof program.name === 'string') { 120 | options.name = program.name; 121 | } 122 | 123 | // --silent 124 | 125 | var consoleLog = program.silent ? function() {} : console.log; 126 | 127 | // left-over args are file paths 128 | 129 | var files = program.args; 130 | 131 | // object of reverse dependencies of a watched file, including itself if 132 | // applicable 133 | 134 | var watchList = {}; 135 | 136 | // function for rendering 137 | var render = program.watch ? tryRender : renderFile; 138 | 139 | // compile files 140 | 141 | if (files.length) { 142 | consoleLog(); 143 | if (program.watch) { 144 | process.on('SIGINT', function() { 145 | process.exit(1); 146 | }); 147 | } 148 | files.forEach(function (file) { 149 | render(file); 150 | }); 151 | // stdio 152 | } else { 153 | stdin(); 154 | } 155 | 156 | /** 157 | * Watch for changes on path 158 | * 159 | * Renders `base` if specified, otherwise renders `path`. 160 | */ 161 | function watchFile(path, base, rootPath) { 162 | path = normalize(path); 163 | 164 | var log = ' ' + chalk.gray('watching') + ' ' + chalk.cyan(path); 165 | if (!base) { 166 | base = path; 167 | } else { 168 | base = normalize(base); 169 | log += '\n ' + chalk.gray('as a dependency of') + ' '; 170 | log += chalk.cyan(base); 171 | } 172 | 173 | if (watchList[path]) { 174 | if (watchList[path].indexOf(base) !== -1) return; 175 | consoleLog(log); 176 | watchList[path].push(base); 177 | return; 178 | } 179 | 180 | consoleLog(log); 181 | watchList[path] = [base]; 182 | fs.watchFile(path, {persistent: true, interval: 200}, 183 | function (curr, prev) { 184 | // File doesn't exist anymore. Keep watching. 185 | if (curr.mtime.getTime() === 0) return; 186 | // istanbul ignore if 187 | if (curr.mtime.getTime() === prev.mtime.getTime()) return; 188 | watchList[path].forEach(function(file) { 189 | tryRender(file, rootPath); 190 | }); 191 | }); 192 | } 193 | 194 | /** 195 | * Convert error to string 196 | */ 197 | function errorToString(e) { 198 | return e.stack || /* istanbul ignore next */ (e.message || e); 199 | } 200 | 201 | /** 202 | * Try to render `path`; if an exception is thrown it is printed to stderr and 203 | * otherwise ignored. 204 | * 205 | * This is used in watch mode. 206 | */ 207 | function tryRender(path, rootPath) { 208 | try { 209 | renderFile(path, rootPath); 210 | } catch (e) { 211 | // keep watching when error occured. 212 | console.error(errorToString(e) + '\x07'); 213 | } 214 | } 215 | 216 | /** 217 | * Compile from stdin. 218 | */ 219 | 220 | function stdin() { 221 | var buf = ''; 222 | process.stdin.setEncoding('utf8'); 223 | process.stdin.on('data', function(chunk){ buf += chunk; }); 224 | process.stdin.on('end', function(){ 225 | var output; 226 | if (options.client) { 227 | output = pug.compileClient(buf, options); 228 | } else { 229 | var fn = pug.compile(buf, options); 230 | var output = fn(options); 231 | } 232 | process.stdout.write(output); 233 | }).resume(); 234 | } 235 | 236 | /** 237 | * Process the given path, compiling the pug files found. 238 | * Always walk the subdirectories. 239 | * 240 | * @param path path of the file, might be relative 241 | * @param rootPath path relative to the directory specified in the command 242 | */ 243 | 244 | function renderFile(path, rootPath) { 245 | var isPug = /\.(?:pug|jade)$/; 246 | var isIgnored = /([\/\\]_)|(^_)/; 247 | 248 | var stat = fs.lstatSync(path); 249 | // Found pug file 250 | if (stat.isFile() && isPug.test(path) && !isIgnored.test(path)) { 251 | // Try to watch the file if needed. watchFile takes care of duplicates. 252 | if (program.watch) watchFile(path, null, rootPath); 253 | if (program.nameAfterFile) { 254 | options.name = getNameFromFileName(path); 255 | } 256 | var fn = options.client 257 | ? pug.compileFileClient(path, options) 258 | : pug.compileFile(path, options); 259 | if (program.watch && fn.dependencies) { 260 | // watch dependencies, and recompile the base 261 | fn.dependencies.forEach(function (dep) { 262 | watchFile(dep, path, rootPath); 263 | }); 264 | } 265 | 266 | // --extension 267 | var extname; 268 | if (program.extension) extname = '.' + program.extension; 269 | else if (options.client) extname = '.js'; 270 | else if (program.extension === '') extname = ''; 271 | else extname = '.html'; 272 | 273 | // path: foo.pug -> foo. 274 | path = path.replace(isPug, extname); 275 | if (program.out) { 276 | // prepend output directory 277 | if (rootPath) { 278 | // replace the rootPath of the resolved path with output directory 279 | path = relative(rootPath, path); 280 | } else { 281 | // if no rootPath handling is needed 282 | path = basename(path); 283 | } 284 | path = resolve(program.out, path); 285 | } 286 | var dir = resolve(dirname(path)); 287 | mkdirp.sync(dir); 288 | var output = options.client ? fn : fn(options); 289 | fs.writeFileSync(path, output); 290 | consoleLog(' ' + chalk.gray('rendered') + ' ' + chalk.cyan('%s'), normalize(path)); 291 | // Found directory 292 | } else if (stat.isDirectory()) { 293 | var files = fs.readdirSync(path); 294 | files.map(function(filename) { 295 | return path + '/' + filename; 296 | }).forEach(function (file) { 297 | render(file, rootPath || path); 298 | }); 299 | } 300 | } 301 | 302 | /** 303 | * Get a sensible name for a template function from a file path 304 | * 305 | * @param {String} filename 306 | * @returns {String} 307 | */ 308 | function getNameFromFileName(filename) { 309 | var file = basename(filename).replace(/\.(?:pug|jade)$/, ''); 310 | return file.toLowerCase().replace(/[^a-z0-9]+([a-z])/g, function (_, character) { 311 | return character.toUpperCase(); 312 | }) + 'Template'; 313 | } 314 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fs = require('fs'); 4 | var path = require('path'); 5 | var assert = require('assert'); 6 | var cp = require('child_process'); 7 | var mkdirp = require('mkdirp'); 8 | var rimraf = require('rimraf'); 9 | 10 | // Sets directory to output coverage data to 11 | // Incremented every time getRunner() is called. 12 | var covCount = 1; 13 | var isIstanbul = process.env.running_under_istanbul; 14 | 15 | /* 16 | * I/O utilities for temporary directory. 17 | */ 18 | function j(paths) { 19 | return path.join.apply(path, paths); 20 | } 21 | 22 | function t(paths) { 23 | paths = Array.isArray(paths) ? paths : [paths]; 24 | var args = [__dirname, 'temp'].concat(paths); 25 | return j(args); 26 | } 27 | 28 | function r(paths) { 29 | return fs.readFileSync(t(paths), 'utf8'); 30 | } 31 | 32 | function rs(paths) { 33 | return fs.createReadStream(t(paths)); 34 | } 35 | 36 | function w(paths, content) { 37 | return fs.writeFileSync(t(paths), content); 38 | } 39 | 40 | function a(paths, content) { 41 | return fs.appendFileSync(t(paths), content); 42 | } 43 | 44 | function u(paths) { 45 | return fs.unlinkSync(t(paths)); 46 | } 47 | 48 | /** 49 | * Gets an array containing the routine to run the pug CLI. If this file is 50 | * being processed with istanbul then this function will return a routine 51 | * asking istanbul to store coverage data to a unique directory 52 | * (cov-pt/). 53 | */ 54 | function getRunner() { 55 | var pugExe = j([__dirname, '..', 'index.js']); 56 | 57 | if (!isIstanbul) return [process.argv[0], [pugExe]]; 58 | else { 59 | return [ 'istanbul', 60 | [ 'cover', 61 | '--print', 'none', 62 | '--report', 'none', 63 | '--root', process.cwd(), 64 | '--dir', process.cwd() + '/cov-pt' + (covCount++), 65 | pugExe, 66 | '--' ] ]; 67 | } 68 | } 69 | 70 | /* 71 | * Run Pug CLI. 72 | * 73 | * @param args Array of arguments 74 | * @param [stdin] Stream of standard input 75 | * @param callback Function to call when the process finishes 76 | */ 77 | function run(args, stdin, callback) { 78 | if (arguments.length === 2) { 79 | callback = stdin; 80 | stdin = null; 81 | } 82 | var runner = getRunner(); 83 | var proc = cp.execFile(runner[0], runner[1].concat(args), { 84 | cwd: t([]) 85 | }, callback); 86 | if (stdin) stdin.pipe(proc.stdin); 87 | } 88 | 89 | /** 90 | * Set timing limits for a test case 91 | */ 92 | function timing(testCase) { 93 | if (isIstanbul) { 94 | testCase.timeout(20000); 95 | testCase.slow(3000); 96 | } else { 97 | testCase.timeout(12500); 98 | testCase.slow(2000); 99 | } 100 | } 101 | 102 | /* 103 | * Make temporary directories 104 | */ 105 | rimraf.sync(t([])); 106 | mkdirp.sync(t(['_omittedDir'])); 107 | mkdirp.sync(t(['depwatch'])); 108 | mkdirp.sync(t(['inputs', 'level-1-1'])); 109 | mkdirp.sync(t(['inputs', 'level-1-2'])); 110 | mkdirp.sync(t(['outputs', 'level-1-1'])); 111 | mkdirp.sync(t(['outputs', 'level-1-2'])); 112 | 113 | /* 114 | * CLI utilities 115 | */ 116 | describe('miscellanea', function () { 117 | timing(this); 118 | it('--version', function (done) { 119 | run(['-V'], function (err, stdout) { 120 | if (err) done(err); 121 | assert.equal(stdout.trim(), 'pug version: ' + require('pug/package.json').version + '\npug-cli version: ' + require('../package.json').version); 122 | run(['--version'], function (err, stdout) { 123 | if (err) done(err); 124 | assert.equal(stdout.trim(), 'pug version: ' + require('pug/package.json').version + '\npug-cli version: ' + require('../package.json').version); 125 | done() 126 | }); 127 | }); 128 | }); 129 | it('--help', function (done) { 130 | // only check that it doesn't crash 131 | run(['-h'], function (err, stdout) { 132 | if (err) done(err); 133 | run(['--help'], function (err, stdout) { 134 | if (err) done(err); 135 | done() 136 | }); 137 | }); 138 | }); 139 | it('Omits files starting with an underscore', function (done) { 140 | w('_omitted.pug', '.foo bar'); 141 | w('_omitted.html', '

output not written

'); 142 | 143 | run(['_omitted.pug'], function (err) { 144 | if (err) return done(err); 145 | var html = r('_omitted.html'); 146 | assert(html === '

output not written

'); 147 | done(); 148 | }); 149 | }); 150 | it('Omits directories starting with an underscore', function (done) { 151 | w('_omittedDir/file.pug', '.foo bar'); 152 | w('_omittedDir/file.html', '

output not written

'); 153 | 154 | run(['--no-debug', '_omittedDir/file.pug'], function (err, stdout) { 155 | if (err) return done(err); 156 | var html = r('_omittedDir/file.html'); 157 | assert.equal(html, '

output not written

'); 158 | done(); 159 | }); 160 | }); 161 | }); 162 | 163 | describe('HTML output', function () { 164 | timing(this); 165 | it('works', function (done) { 166 | w('input.pug', '.foo bar'); 167 | w('input.html', '

output not written

'); 168 | 169 | run(['--no-debug', 'input.pug'], function (err) { 170 | if (err) return done(err); 171 | var html = r('input.html'); 172 | assert(html === '
bar
'); 173 | done(); 174 | }); 175 | }); 176 | it('--extension', function (done) { 177 | w('input.pug', '.foo bar'); 178 | w('input.special-html', '

output not written

'); 179 | 180 | run(['--no-debug', '-E', 'special-html', 'input.pug'], function (err) { 181 | if (err) return done(err); 182 | var html = r('input.special-html'); 183 | assert(html === '
bar
'); 184 | done(); 185 | }); 186 | }); 187 | it('--basedir', function (done) { 188 | w('input.pug', 'extends /dependency1.pug'); 189 | w('input.html', '

output not written

'); 190 | run(['--no-debug', '-b', j([__dirname, 'dependencies']), 'input.pug'], function (err, stdout) { 191 | if (err) return done(err); 192 | var html = r('input.html'); 193 | assert.equal(html, ''); 194 | done(); 195 | }); 196 | }); 197 | context('--obj', function () { 198 | it('JavaScript syntax works', function (done) { 199 | w('input.pug', '.foo= loc'); 200 | w('input.html', '

output not written

'); 201 | run(['--no-debug', '--obj', "{'loc':'str'}", 'input.pug'], function (err) { 202 | if (err) return done(err); 203 | var html = r('input.html'); 204 | assert(html === '
str
'); 205 | done(); 206 | }); 207 | }); 208 | it('JavaScript syntax does not accept UTF newlines', function (done) { 209 | w('input.pug', '.foo= loc'); 210 | w('input.html', '

output not written

'); 211 | run(['--no-debug', '--obj', "{'loc':'st\u2028r'}", 'input.pug'], function (err) { 212 | if (!err) return done(new Error('expecting error')); 213 | done(); 214 | }); 215 | }); 216 | it('JSON syntax accept UTF newlines', function (done) { 217 | w('input.pug', '.foo= loc'); 218 | w('input.html', '

output not written

'); 219 | run(['--no-debug', '--obj', '{"loc":"st\u2028r"}', 'input.pug'], function (err) { 220 | if (err) return done(err); 221 | var html = r('input.html'); 222 | assert.equal(html, '
st\u2028r
'); 223 | done(); 224 | }); 225 | }); 226 | it('JSON file', function (done) { 227 | w('obj.json', '{"loc":"str"}'); 228 | w('input.pug', '.foo= loc'); 229 | w('input.html', '

output not written

'); 230 | run(['--no-debug', '--obj', 'obj.json', 'input.pug'], function (err) { 231 | if (err) return done(err); 232 | var html = r('input.html'); 233 | assert(html === '
str
'); 234 | done(); 235 | }); 236 | }); 237 | it('JavaScript module', function (done) { 238 | w('obj.js', 'module.exports = {loc: "str"};'); 239 | w('input.pug', '.foo= loc'); 240 | w('input.html', '

output not written

'); 241 | run(['--no-debug', '--obj', 'obj.js', 'input.pug'], function (err) { 242 | if (err) return done(err); 243 | var html = r('input.html'); 244 | assert(html === '
str
'); 245 | done(); 246 | }); 247 | }); 248 | }); 249 | it('stdio', function (done) { 250 | w('input.pug', '.foo bar'); 251 | run(['--no-debug'], rs('input.pug'), function (err, stdout, stderr) { 252 | if (err) return done(err); 253 | assert(stdout === '
bar
'); 254 | done(); 255 | }); 256 | }); 257 | context('--out', function () { 258 | it('works', function (done) { 259 | w('input.pug', '.foo bar'); 260 | w('input.html', '

output not written

'); 261 | run(['--no-debug', '--out', 'outputs', 'input.pug'], function (err) { 262 | if (err) return done(err); 263 | var html = r(['outputs', 'input.html']); 264 | assert(html === '
bar
'); 265 | done(); 266 | }); 267 | }); 268 | it('works when input is a directory', function (done) { 269 | w(['inputs', 'input.pug'], '.foo bar 1'); 270 | w(['inputs', 'level-1-1', 'input.pug'], '.foo bar 1-1'); 271 | w(['inputs', 'level-1-2', 'input.pug'], '.foo bar 1-2'); 272 | w(['outputs', 'input.html'], 'BIG FAT HEN 1'); 273 | w(['outputs', 'level-1-1', 'input.html'], 'BIG FAT HEN 1-1'); 274 | w(['outputs', 'level-1-2', 'input.html'], 'BIG FAT HEN 1-2'); 275 | 276 | run(['--no-debug', '--hierarchy', '--out', 'outputs', 'inputs'], function (err) { 277 | if (err) return done(err); 278 | var html = r(['outputs', 'input.html']); 279 | assert(html === '
bar 1
'); 280 | var html = r(['outputs', 'level-1-1', 'input.html']); 281 | assert(html === '
bar 1-1
'); 282 | var html = r(['outputs', 'level-1-2', 'input.html']); 283 | assert(html === '
bar 1-2
'); 284 | done(); 285 | }); 286 | }); 287 | }); 288 | it('--silent', function (done) { 289 | w('input.pug', '.foo bar'); 290 | w('input.html', '

output not written

'); 291 | run(['--no-debug', '-s', 'input.pug'], function (err, stdout) { 292 | if (err) return done(err); 293 | var html = r('input.html'); 294 | assert.equal(html, '
bar
'); 295 | assert.equal(stdout, ''); 296 | 297 | w('input.html', '

output not written

'); 298 | run(['--no-debug', '--silent', 'input.pug'], function (err, stdout) { 299 | if (err) return done(err); 300 | var html = r('input.html'); 301 | assert.equal(html, '
bar
'); 302 | assert.equal(stdout, ''); 303 | done(); 304 | }); 305 | }); 306 | }); 307 | }); 308 | 309 | describe('client JavaScript output', function () { 310 | timing(this); 311 | it('works', function (done) { 312 | w('input.pug', '.foo bar'); 313 | w('input.js', 'throw new Error("output not written");'); 314 | run(['--no-debug', '--client', 'input.pug'], function (err) { 315 | if (err) return done(err); 316 | var template = Function('', r('input.js') + ';return template;')(); 317 | assert(template() === '
bar
'); 318 | done(); 319 | }); 320 | }); 321 | it('--name', function (done) { 322 | w('input.pug', '.foo bar'); 323 | w('input.js', 'throw new Error("output not written");'); 324 | run(['--no-debug', '--client', '--name', 'myTemplate', 'input.pug'], function (err) { 325 | if (err) return done(err); 326 | var template = Function('', r('input.js') + ';return myTemplate;')(); 327 | assert(template() === '
bar
'); 328 | done(); 329 | }); 330 | }); 331 | it('--name --extension', function (done) { 332 | w('input.pug', '.foo bar'); 333 | w('input.special-js', 'throw new Error("output not written");'); 334 | run(['--no-debug', '--client', '-E', 'special-js', 'input.pug'], function (err) { 335 | if (err) return done(err); 336 | var template = Function('', r('input.special-js') + ';return template;')(); 337 | assert(template() === '
bar
'); 338 | done(); 339 | }); 340 | }); 341 | it('stdio', function (done) { 342 | w('input.pug', '.foo bar'); 343 | w('input.js', 'throw new Error("output not written");'); 344 | run(['--no-debug', '--client'], rs('input.pug'), function (err, stdout) { 345 | if (err) return done(err); 346 | var template = Function('', stdout + ';return template;')(); 347 | assert(template() === '
bar
'); 348 | done(); 349 | }); 350 | }); 351 | it('--name-after-file', function (done) { 352 | w('input-file.pug', '.foo bar'); 353 | w('input-file.js', 'throw new Error("output not written");'); 354 | run(['--no-debug', '--client', '--name-after-file', 'input-file.pug'], function (err, stdout, stderr) { 355 | if (err) return done(err); 356 | var template = Function('', r('input-file.js') + ';return inputFileTemplate;')(); 357 | assert(template() === '
bar
'); 358 | return done(); 359 | }); 360 | }); 361 | it('--name-after-file ·InPuTwIthWEiRdNaMME.pug', function (done) { 362 | w('·InPuTwIthWEiRdNaMME.pug', '.foo bar'); 363 | w('·InPuTwIthWEiRdNaMME.js', 'throw new Error("output not written");'); 364 | run(['--no-debug', '--client', '--name-after-file', '·InPuTwIthWEiRdNaMME.pug'], function (err, stdout, stderr) { 365 | if (err) return done(err); 366 | var template = Function('', r('·InPuTwIthWEiRdNaMME.js') + ';return InputwithweirdnammeTemplate;')(); 367 | assert(template() === '
bar
'); 368 | return done(); 369 | }); 370 | }); 371 | }); 372 | 373 | describe('--watch', function () { 374 | var watchProc; 375 | var stdout = ''; 376 | 377 | function cleanup() { 378 | stdout = ''; 379 | if (!watchProc) return; 380 | 381 | watchProc.stderr.removeAllListeners('data'); 382 | watchProc.stdout.removeAllListeners('data'); 383 | watchProc.removeAllListeners('error'); 384 | watchProc.removeAllListeners('close'); 385 | } 386 | 387 | after(function () { 388 | cleanup(); 389 | watchProc.kill('SIGINT'); 390 | watchProc = null; 391 | }); 392 | 393 | beforeEach(cleanup); 394 | 395 | afterEach(function (done) { 396 | // pug --watch can only detect changes that are at least 1 second apart 397 | setTimeout(done, 1000); 398 | }); 399 | 400 | it('pass 1: initial compilation', function (done) { 401 | timing(this); 402 | 403 | w('input-file.pug', '.foo bar'); 404 | w('input-file.js', 'throw new Error("output not written (pass 1)");'); 405 | var cmd = getRunner(); 406 | cmd[1].push('--no-debug', '--client', '--name-after-file', '--watch', 'input-file.pug'); 407 | watchProc = cp.spawn(cmd[0], cmd[1], { 408 | cwd: t([]) 409 | }); 410 | 411 | watchProc.stdout.setEncoding('utf8'); 412 | watchProc.stderr.setEncoding('utf8'); 413 | watchProc.on('error', done); 414 | watchProc.stdout.on('data', function(buf) { 415 | stdout += buf; 416 | if (/rendered/.test(stdout)) { 417 | cleanup(); 418 | 419 | var template = Function('', r('input-file.js') + ';return inputFileTemplate;')(); 420 | assert(template() === '
bar
'); 421 | 422 | return done(); 423 | } 424 | }); 425 | }); 426 | it('pass 2: change the file', function (done) { 427 | w('input-file.js', 'throw new Error("output not written (pass 2)");'); 428 | 429 | watchProc.on('error', done); 430 | watchProc.stdout.on('data', function(buf) { 431 | stdout += buf; 432 | if (/rendered/.test(stdout)) { 433 | cleanup(); 434 | 435 | var template = Function('', r('input-file.js') + ';return inputFileTemplate;')(); 436 | assert(template() === '
baz
'); 437 | 438 | return done(); 439 | } 440 | }); 441 | 442 | w('input-file.pug', '.foo baz'); 443 | }); 444 | it('pass 3: remove the file then add it back', function (done) { 445 | w('input-file.js', 'throw new Error("output not written (pass 3)");'); 446 | 447 | watchProc.on('error', done) 448 | watchProc.stdout.on('data', function(buf) { 449 | stdout += buf; 450 | if (/rendered/.test(stdout)) { 451 | cleanup(); 452 | 453 | var template = Function('', r('input-file.js') + ';return inputFileTemplate;')(); 454 | assert(template() === '
bat
'); 455 | 456 | return done(); 457 | } 458 | }); 459 | 460 | u('input-file.pug'); 461 | setTimeout(function () { 462 | w('input-file.pug', '.foo bat'); 463 | }, 250); 464 | }); 465 | it('pass 4: intentional errors in the pug file', function (done) { 466 | var stderr = ''; 467 | var errored = false; 468 | 469 | watchProc.on('error', done); 470 | watchProc.on('close', function() { 471 | errored = true; 472 | return done(new Error('Pug should not terminate in watch mode')); 473 | }); 474 | watchProc.stdout.on('data', function(buf) { 475 | stdout += buf; 476 | if (/rendered/.test(stdout)) { 477 | stdout = ''; 478 | return done(new Error('Pug compiles an erroneous file w/o error')); 479 | } 480 | }); 481 | watchProc.stderr.on('data', function(buf) { 482 | stderr += buf; 483 | if (!/Invalid indentation/.test(stderr)) return; 484 | stderr = ''; 485 | var template = Function('', r('input-file.js') + ';return inputFileTemplate;')(); 486 | assert(template() === '
bat
'); 487 | 488 | watchProc.stderr.removeAllListeners('data'); 489 | watchProc.stdout.removeAllListeners('data'); 490 | watchProc.removeAllListeners('error'); 491 | watchProc.removeAllListeners('exit'); 492 | // The stderr event will always fire sooner than the close event. 493 | // Wait for it. 494 | setTimeout(function() { 495 | if (!errored) done(); 496 | }, 100); 497 | }); 498 | 499 | w('input-file.pug', [ 500 | 'div', 501 | ' div', 502 | '\tarticle' 503 | ].join('\n')); 504 | }); 505 | }); 506 | 507 | describe('--watch with dependencies', function () { 508 | var watchProc; 509 | var stdout = ''; 510 | 511 | before(function () { 512 | function copy(file) { 513 | w(['depwatch', file], 514 | fs.readFileSync(j([__dirname, 'dependencies', file]))); 515 | } 516 | copy('include2.pug'); 517 | copy('dependency2.pug'); 518 | copy('dependency3.pug'); 519 | }); 520 | 521 | function cleanup() { 522 | stdout = ''; 523 | 524 | if (!watchProc) return; 525 | 526 | watchProc.stderr.removeAllListeners('data'); 527 | watchProc.stdout.removeAllListeners('data'); 528 | watchProc.removeAllListeners('error'); 529 | watchProc.removeAllListeners('close'); 530 | } 531 | 532 | after(function () { 533 | cleanup(); 534 | watchProc.kill('SIGINT'); 535 | watchProc = null; 536 | }); 537 | 538 | beforeEach(cleanup); 539 | 540 | afterEach(function (done) { 541 | // pug --watch can only detect changes that are at least 1 second apart 542 | setTimeout(done, 1000); 543 | }); 544 | 545 | it('pass 1: initial compilation', function (done) { 546 | timing(this); 547 | 548 | w(['depwatch', 'include2.html'], 'output not written (pass 1)'); 549 | w(['depwatch', 'dependency2.html'], 'output not written (pass 1)'); 550 | var cmd = getRunner(); 551 | cmd[1].push('--watch', 'include2.pug', 'dependency2.pug'); 552 | watchProc = cp.spawn(cmd[0], cmd[1], { 553 | cwd: t('depwatch') 554 | }); 555 | 556 | watchProc.stdout.setEncoding('utf8'); 557 | watchProc.stderr.setEncoding('utf8'); 558 | watchProc.on('error', done); 559 | watchProc.stdout.on('data', function(buf) { 560 | stdout += buf; 561 | if ((stdout.match(/rendered/g) || []).length === 2) { 562 | cleanup(); 563 | 564 | var output = r(['depwatch', 'include2.html']); 565 | assert.equal(output.trim(), 'dependency3'); 566 | output = r(['depwatch', 'dependency2.html']); 567 | assert.equal(output.trim(), 'dependency3'); 568 | 569 | return done(); 570 | } 571 | }); 572 | }); 573 | it('pass 2: change a dependency', function (done) { 574 | timing(this); 575 | 576 | w(['depwatch', 'include2.html'], 'output not written (pass 2)'); 577 | w(['depwatch', 'dependency2.html'], 'output not written (pass 2)'); 578 | 579 | watchProc.on('error', done); 580 | watchProc.stdout.on('data', function(buf) { 581 | stdout += buf; 582 | if ((stdout.match(/rendered/g) || []).length === 2) { 583 | cleanup(); 584 | 585 | var output = r(['depwatch', 'include2.html']); 586 | assert.equal(output.trim(), 'dependency3

Hey

'); 587 | output = r(['depwatch', 'dependency2.html']); 588 | assert.equal(output.trim(), 'dependency3

Hey

'); 589 | 590 | return done(); 591 | } 592 | }); 593 | 594 | a(['depwatch', 'dependency2.pug'], '\np Hey\n'); 595 | }); 596 | it('pass 3: change a deeper dependency', function (done) { 597 | timing(this); 598 | 599 | w(['depwatch', 'include2.html'], 'output not written (pass 3)'); 600 | w(['depwatch', 'dependency2.html'], 'output not written (pass 3)'); 601 | 602 | watchProc.on('error', done) 603 | watchProc.stdout.on('data', function(buf) { 604 | stdout += buf; 605 | if ((stdout.match(/rendered/g) || []).length === 2) { 606 | cleanup(); 607 | 608 | var output = r(['depwatch', 'include2.html']); 609 | assert.equal(output.trim(), 'dependency3

Foo

Hey

'); 610 | output = r(['depwatch', 'dependency2.html']); 611 | assert.equal(output.trim(), 'dependency3

Foo

Hey

'); 612 | 613 | return done(); 614 | } 615 | }); 616 | 617 | a(['depwatch', 'dependency3.pug'], '\np Foo\n'); 618 | }); 619 | it('pass 4: change main file', function (done) { 620 | timing(this); 621 | 622 | w(['depwatch', 'include2.html'], 'output not written (pass 4)'); 623 | w(['depwatch', 'dependency2.html'], 'output not written (pass 4)'); 624 | 625 | watchProc.on('error', done); 626 | watchProc.stdout.on('data', function(buf) { 627 | stdout += buf; 628 | if ((stdout.match(/rendered/g) || []).length === 1) { 629 | cleanup(); 630 | 631 | var output = r(['depwatch', 'include2.html']); 632 | assert.equal(output.trim(), 'dependency3

Foo

Hey

Baz

'); 633 | output = r(['depwatch', 'dependency2.html']); 634 | assert.equal(output.trim(), 'output not written (pass 4)'); 635 | 636 | return done(); 637 | } 638 | }); 639 | 640 | a(['depwatch', 'include2.pug'], '\np Baz\n'); 641 | }); 642 | }); 643 | --------------------------------------------------------------------------------