UNPKG

7.89 kBJavaScriptView Raw
1#!/usr/bin/env node
2
3/**
4 * Module dependencies.
5 */
6
7var fs = require('fs')
8 , program = require('commander')
9 , path = require('path')
10 , basename = path.basename
11 , dirname = path.dirname
12 , resolve = path.resolve
13 , normalize = path.normalize
14 , join = path.join
15 , mkdirp = require('mkdirp')
16 , jade = require('../');
17
18// jade options
19
20var options = {};
21
22// options
23
24program
25 .version(require('../package.json').version)
26 .usage('[options] [dir|file ...]')
27 .option('-O, --obj <str|path>', 'JavaScript options object or JSON file containing it')
28 .option('-o, --out <dir>', 'output the compiled html to <dir>')
29 .option('-p, --path <path>', 'filename used to resolve includes')
30 .option('-P, --pretty', 'compile pretty html output')
31 .option('-c, --client', 'compile function for client-side runtime.js')
32 .option('-n, --name <str>', 'The name of the compiled template (requires --client)')
33 .option('-D, --no-debug', 'compile without debugging (smaller functions)')
34 .option('-w, --watch', 'watch files for changes and automatically re-render')
35 .option('-E, --extension <ext>', 'specify the output file extension')
36 .option('-H, --hierarchy', 'keep directory hierarchy when a directory is specified')
37 .option('--name-after-file', 'Name the template after the last section of the file path (requires --client and overriden by --name)')
38 .option('--doctype <str>', 'Specify the doctype on the command line (useful if it is not specified by the template)')
39
40
41program.on('--help', function(){
42 console.log(' Examples:');
43 console.log('');
44 console.log(' # translate jade the templates dir');
45 console.log(' $ jade templates');
46 console.log('');
47 console.log(' # create {foo,bar}.html');
48 console.log(' $ jade {foo,bar}.jade');
49 console.log('');
50 console.log(' # jade over stdio');
51 console.log(' $ jade < my.jade > my.html');
52 console.log('');
53 console.log(' # jade over stdio');
54 console.log(' $ echo \'h1 Jade!\' | jade');
55 console.log('');
56 console.log(' # foo, bar dirs rendering to /tmp');
57 console.log(' $ jade foo bar --out /tmp ');
58 console.log('');
59});
60
61program.parse(process.argv);
62
63// options given, parse them
64
65if (program.obj) {
66 options = parseObj(program.obj);
67}
68
69/**
70 * Parse object either in `input` or in the file called `input`. The latter is
71 * searched first.
72 */
73function parseObj (input) {
74 var str, out;
75 try {
76 str = fs.readFileSync(program.obj);
77 } catch (e) {
78 return eval('(' + program.obj + ')');
79 }
80 // We don't want to catch exceptions thrown in JSON.parse() so have to
81 // use this two-step approach.
82 return JSON.parse(str);
83}
84
85// --path
86
87if (program.path) options.filename = program.path;
88
89// --no-debug
90
91options.compileDebug = program.debug;
92
93// --client
94
95options.client = program.client;
96
97// --pretty
98
99options.pretty = program.pretty;
100
101// --watch
102
103options.watch = program.watch;
104
105// --name
106
107if (typeof program.name === 'string') {
108 options.name = program.name;
109}
110
111// --doctype
112
113options.doctype = program.doctype;
114
115// left-over args are file paths
116
117var files = program.args;
118
119// array of paths that are being watched
120
121var watchList = [];
122
123// function for rendering
124var render = program.watch ? tryRender : renderFile;
125
126// compile files
127
128if (files.length) {
129 console.log();
130 if (options.watch) {
131 process.on('SIGINT', function() {
132 process.exit(1);
133 });
134 }
135 files.forEach(function (file) {
136 render(file);
137 });
138 process.on('exit', function () {
139 console.log();
140 });
141// stdio
142} else {
143 stdin();
144}
145
146/**
147 * Watch for changes on path
148 *
149 * Renders `base` if specified, otherwise renders `path`.
150 */
151function watchFile(path, base, rootPath) {
152 path = normalize(path);
153 if (watchList.indexOf(path) !== -1) return;
154 console.log(" \033[90mwatching \033[36m%s\033[0m", path);
155 fs.watchFile(path, {persistent: true, interval: 200},
156 function (curr, prev) {
157 // File doesn't exist anymore. Keep watching.
158 if (curr.mtime.getTime() === 0) return;
159 // istanbul ignore if
160 if (curr.mtime.getTime() === prev.mtime.getTime()) return;
161 tryRender(base || path, rootPath);
162 });
163 watchList.push(path);
164}
165
166/**
167 * Convert error to string
168 */
169function errorToString(e) {
170 return e.stack || /* istanbul ignore next */ (e.message || e);
171}
172
173/**
174 * Try to render `path`; if an exception is thrown it is printed to stderr and
175 * otherwise ignored.
176 *
177 * This is used in watch mode.
178 */
179function tryRender(path, rootPath) {
180 try {
181 renderFile(path, rootPath);
182 } catch (e) {
183 // keep watching when error occured.
184 console.error(errorToString(e));
185 }
186}
187
188/**
189 * Compile from stdin.
190 */
191
192function stdin() {
193 var buf = '';
194 process.stdin.setEncoding('utf8');
195 process.stdin.on('data', function(chunk){ buf += chunk; });
196 process.stdin.on('end', function(){
197 var output;
198 if (options.client) {
199 output = jade.compileClient(buf, options);
200 } else {
201 var fn = jade.compile(buf, options);
202 var output = fn(options);
203 }
204 process.stdout.write(output);
205 }).resume();
206
207 process.on('SIGINT', function() {
208 process.stdout.write('\n');
209 process.stdin.emit('end');
210 process.stdout.write('\n');
211 process.exit();
212 })
213}
214
215var hierarchyWarned = false;
216
217/**
218 * Process the given path, compiling the jade files found.
219 * Always walk the subdirectories.
220 *
221 * @param path path of the file, might be relative
222 * @param rootPath path relative to the directory specified in the command
223 */
224
225function renderFile(path, rootPath) {
226 var re = /\.jade$/;
227 var stat = fs.lstatSync(path);
228 // Found jade file/\.jade$/
229 if (stat.isFile() && re.test(path)) {
230 // Try to watch the file if needed. watchFile takes care of duplicates.
231 if (options.watch) watchFile(path, null, rootPath);
232 if (program.nameAfterFile) {
233 options.name = getNameFromFileName(path);
234 }
235 var fn = options.client
236 ? jade.compileFileClient(path, options)
237 : jade.compileFile(path, options);
238 if (options.watch && fn.dependencies) {
239 // watch dependencies, and recompile the base
240 fn.dependencies.forEach(function (dep) {
241 watchFile(dep, path, rootPath);
242 });
243 }
244
245 // --extension
246 var extname;
247 if (program.extension) extname = '.' + program.extension;
248 else if (options.client) extname = '.js';
249 else extname = '.html';
250
251 // path: foo.jade -> foo.<ext>
252 path = path.replace(re, extname);
253 if (program.out) {
254 // prepend output directory
255 if (rootPath && program.hierarchy) {
256 // replace the rootPath of the resolved path with output directory
257 path = resolve(path).replace(new RegExp('^' + resolve(rootPath)), '');
258 path = join(program.out, path);
259 } else {
260 if (rootPath && !hierarchyWarned) {
261 console.warn('In Jade 2.0.0 --hierarchy will become the default.');
262 hierarchyWarned = true;
263 }
264 // old behavior or if no rootPath handling is needed
265 path = join(program.out, basename(path));
266 }
267 }
268 var dir = resolve(dirname(path));
269 mkdirp.sync(dir, 0755);
270 var output = options.client ? fn : fn(options);
271 fs.writeFileSync(path, output);
272 console.log(' \033[90mrendered \033[36m%s\033[0m', normalize(path));
273 // Found directory
274 } else if (stat.isDirectory()) {
275 var files = fs.readdirSync(path);
276 files.map(function(filename) {
277 return path + '/' + filename;
278 }).forEach(function (file) {
279 render(file, rootPath || path);
280 });
281 }
282}
283
284/**
285 * Get a sensible name for a template function from a file path
286 *
287 * @param {String} filename
288 * @returns {String}
289 */
290function getNameFromFileName(filename) {
291 var file = basename(filename, '.jade');
292 return file.toLowerCase().replace(/[^a-z0-9]+([a-z])/g, function (_, character) {
293 return character.toUpperCase();
294 }) + 'Template';
295}