UNPKG

13.5 kBJavaScriptView Raw
1#!/usr/bin/env node
2
3var ejs = require('ejs')
4var fs = require('fs')
5var minimatch = require('minimatch')
6var mkdirp = require('mkdirp')
7var path = require('path')
8var program = require('commander')
9var readline = require('readline')
10var sortedObject = require('sorted-object')
11var util = require('util')
12
13var MODE_0666 = parseInt('0666', 8)
14var MODE_0755 = parseInt('0755', 8)
15var TEMPLATE_DIR = path.join(__dirname, '..', 'templates')
16var VERSION = require('../package').version
17
18var _exit = process.exit
19
20// Re-assign process.exit because of commander
21// TODO: Switch to a different command framework
22process.exit = exit
23
24// CLI
25
26around(program, 'optionMissingArgument', function (fn, args) {
27 program.outputHelp()
28 fn.apply(this, args)
29 return { args: [], unknown: [] }
30})
31
32before(program, 'outputHelp', function () {
33 // track if help was shown for unknown option
34 this._helpShown = true
35})
36
37before(program, 'unknownOption', function () {
38 // allow unknown options if help was shown, to prevent trailing error
39 this._allowUnknownOption = this._helpShown
40
41 // show help if not yet shown
42 if (!this._helpShown) {
43 program.outputHelp()
44 }
45})
46
47program
48 .name('express')
49 .version(VERSION, ' --version')
50 .usage('[options] [dir]')
51 .option('-e, --ejs', 'add ejs engine support', renamedOption('--ejs', '--view=ejs'))
52 .option(' --pug', 'add pug engine support', renamedOption('--pug', '--view=pug'))
53 .option(' --hbs', 'add handlebars engine support', renamedOption('--hbs', '--view=hbs'))
54 .option('-H, --hogan', 'add hogan.js engine support', renamedOption('--hogan', '--view=hogan'))
55 .option('-v, --view <engine>', 'add view <engine> support (dust|ejs|hbs|hjs|jade|pug|twig|vash) (defaults to jade)')
56 .option(' --no-view', 'use static html instead of view engine')
57 .option('-c, --css <engine>', 'add stylesheet <engine> support (less|stylus|compass|sass) (defaults to plain css)')
58 .option(' --git', 'add .gitignore')
59 .option('-f, --force', 'force on non-empty directory')
60 .parse(process.argv)
61
62if (!exit.exited) {
63 main()
64}
65
66/**
67 * Install an around function; AOP.
68 */
69
70function around (obj, method, fn) {
71 var old = obj[method]
72
73 obj[method] = function () {
74 var args = new Array(arguments.length)
75 for (var i = 0; i < args.length; i++) args[i] = arguments[i]
76 return fn.call(this, old, args)
77 }
78}
79
80/**
81 * Install a before function; AOP.
82 */
83
84function before (obj, method, fn) {
85 var old = obj[method]
86
87 obj[method] = function () {
88 fn.call(this)
89 old.apply(this, arguments)
90 }
91}
92
93/**
94 * Prompt for confirmation on STDOUT/STDIN
95 */
96
97function confirm (msg, callback) {
98 var rl = readline.createInterface({
99 input: process.stdin,
100 output: process.stdout
101 })
102
103 rl.question(msg, function (input) {
104 rl.close()
105 callback(/^y|yes|ok|true$/i.test(input))
106 })
107}
108
109/**
110 * Copy file from template directory.
111 */
112
113function copyTemplate (from, to) {
114 write(to, fs.readFileSync(path.join(TEMPLATE_DIR, from), 'utf-8'))
115}
116
117/**
118 * Copy multiple files from template directory.
119 */
120
121function copyTemplateMulti (fromDir, toDir, nameGlob) {
122 fs.readdirSync(path.join(TEMPLATE_DIR, fromDir))
123 .filter(minimatch.filter(nameGlob, { matchBase: true }))
124 .forEach(function (name) {
125 copyTemplate(path.join(fromDir, name), path.join(toDir, name))
126 })
127}
128
129/**
130 * Create application at the given directory.
131 *
132 * @param {string} name
133 * @param {string} dir
134 */
135
136function createApplication (name, dir) {
137 console.log()
138
139 // Package
140 var pkg = {
141 name: name,
142 version: '0.0.0',
143 private: true,
144 scripts: {
145 start: 'node ./bin/www'
146 },
147 dependencies: {
148 'debug': '~2.6.9',
149 'express': '~4.16.1'
150 }
151 }
152
153 // JavaScript
154 var app = loadTemplate('js/app.js')
155 var www = loadTemplate('js/www')
156
157 // App name
158 www.locals.name = name
159
160 // App modules
161 app.locals.localModules = Object.create(null)
162 app.locals.modules = Object.create(null)
163 app.locals.mounts = []
164 app.locals.uses = []
165
166 // Request logger
167 app.locals.modules.logger = 'morgan'
168 app.locals.uses.push("logger('dev')")
169 pkg.dependencies.morgan = '~1.9.1'
170
171 // Body parsers
172 app.locals.uses.push('express.json()')
173 app.locals.uses.push('express.urlencoded({ extended: false })')
174
175 // Cookie parser
176 app.locals.modules.cookieParser = 'cookie-parser'
177 app.locals.uses.push('cookieParser()')
178 pkg.dependencies['cookie-parser'] = '~1.4.4'
179
180 if (dir !== '.') {
181 mkdir(dir, '.')
182 }
183
184 mkdir(dir, 'public')
185 mkdir(dir, 'public/javascripts')
186 mkdir(dir, 'public/images')
187 mkdir(dir, 'public/stylesheets')
188
189 // copy css templates
190 switch (program.css) {
191 case 'less':
192 copyTemplateMulti('css', dir + '/public/stylesheets', '*.less')
193 break
194 case 'stylus':
195 copyTemplateMulti('css', dir + '/public/stylesheets', '*.styl')
196 break
197 case 'compass':
198 copyTemplateMulti('css', dir + '/public/stylesheets', '*.scss')
199 break
200 case 'sass':
201 copyTemplateMulti('css', dir + '/public/stylesheets', '*.sass')
202 break
203 default:
204 copyTemplateMulti('css', dir + '/public/stylesheets', '*.css')
205 break
206 }
207
208 // copy route templates
209 mkdir(dir, 'routes')
210 copyTemplateMulti('js/routes', dir + '/routes', '*.js')
211
212 if (program.view) {
213 // Copy view templates
214 mkdir(dir, 'views')
215 pkg.dependencies['http-errors'] = '~1.6.3'
216 switch (program.view) {
217 case 'dust':
218 copyTemplateMulti('views', dir + '/views', '*.dust')
219 break
220 case 'ejs':
221 copyTemplateMulti('views', dir + '/views', '*.ejs')
222 break
223 case 'hbs':
224 copyTemplateMulti('views', dir + '/views', '*.hbs')
225 break
226 case 'hjs':
227 copyTemplateMulti('views', dir + '/views', '*.hjs')
228 break
229 case 'jade':
230 copyTemplateMulti('views', dir + '/views', '*.jade')
231 break
232 case 'pug':
233 copyTemplateMulti('views', dir + '/views', '*.pug')
234 break
235 case 'twig':
236 copyTemplateMulti('views', dir + '/views', '*.twig')
237 break
238 case 'vash':
239 copyTemplateMulti('views', dir + '/views', '*.vash')
240 break
241 }
242 } else {
243 // Copy extra public files
244 copyTemplate('js/index.html', path.join(dir, 'public/index.html'))
245 }
246
247 // CSS Engine support
248 switch (program.css) {
249 case 'compass':
250 app.locals.modules.compass = 'node-compass'
251 app.locals.uses.push("compass({ mode: 'expanded' })")
252 pkg.dependencies['node-compass'] = '0.2.3'
253 break
254 case 'less':
255 app.locals.modules.lessMiddleware = 'less-middleware'
256 app.locals.uses.push("lessMiddleware(path.join(__dirname, 'public'))")
257 pkg.dependencies['less-middleware'] = '~2.2.1'
258 break
259 case 'sass':
260 app.locals.modules.sassMiddleware = 'node-sass-middleware'
261 app.locals.uses.push("sassMiddleware({\n src: path.join(__dirname, 'public'),\n dest: path.join(__dirname, 'public'),\n indentedSyntax: true, // true = .sass and false = .scss\n sourceMap: true\n})")
262 pkg.dependencies['node-sass-middleware'] = '0.11.0'
263 break
264 case 'stylus':
265 app.locals.modules.stylus = 'stylus'
266 app.locals.uses.push("stylus.middleware(path.join(__dirname, 'public'))")
267 pkg.dependencies['stylus'] = '0.54.5'
268 break
269 }
270
271 // Index router mount
272 app.locals.localModules.indexRouter = './routes/index'
273 app.locals.mounts.push({ path: '/', code: 'indexRouter' })
274
275 // User router mount
276 app.locals.localModules.usersRouter = './routes/users'
277 app.locals.mounts.push({ path: '/users', code: 'usersRouter' })
278
279 // Template support
280 switch (program.view) {
281 case 'dust':
282 app.locals.modules.adaro = 'adaro'
283 app.locals.view = {
284 engine: 'dust',
285 render: 'adaro.dust()'
286 }
287 pkg.dependencies.adaro = '~1.0.4'
288 break
289 case 'ejs':
290 app.locals.view = { engine: 'ejs' }
291 pkg.dependencies.ejs = '~2.6.1'
292 break
293 case 'hbs':
294 app.locals.view = { engine: 'hbs' }
295 pkg.dependencies.hbs = '~4.0.4'
296 break
297 case 'hjs':
298 app.locals.view = { engine: 'hjs' }
299 pkg.dependencies.hjs = '~0.0.6'
300 break
301 case 'jade':
302 app.locals.view = { engine: 'jade' }
303 pkg.dependencies.jade = '~1.11.0'
304 break
305 case 'pug':
306 app.locals.view = { engine: 'pug' }
307 pkg.dependencies.pug = '2.0.0-beta11'
308 break
309 case 'twig':
310 app.locals.view = { engine: 'twig' }
311 pkg.dependencies.twig = '~0.10.3'
312 break
313 case 'vash':
314 app.locals.view = { engine: 'vash' }
315 pkg.dependencies.vash = '~0.12.6'
316 break
317 default:
318 app.locals.view = false
319 break
320 }
321
322 // Static files
323 app.locals.uses.push("express.static(path.join(__dirname, 'public'))")
324
325 if (program.git) {
326 copyTemplate('js/gitignore', path.join(dir, '.gitignore'))
327 }
328
329 // sort dependencies like npm(1)
330 pkg.dependencies = sortedObject(pkg.dependencies)
331
332 // write files
333 write(path.join(dir, 'app.js'), app.render())
334 write(path.join(dir, 'package.json'), JSON.stringify(pkg, null, 2) + '\n')
335 mkdir(dir, 'bin')
336 write(path.join(dir, 'bin/www'), www.render(), MODE_0755)
337
338 var prompt = launchedFromCmd() ? '>' : '$'
339
340 if (dir !== '.') {
341 console.log()
342 console.log(' change directory:')
343 console.log(' %s cd %s', prompt, dir)
344 }
345
346 console.log()
347 console.log(' install dependencies:')
348 console.log(' %s npm install', prompt)
349 console.log()
350 console.log(' run the app:')
351
352 if (launchedFromCmd()) {
353 console.log(' %s SET DEBUG=%s:* & npm start', prompt, name)
354 } else {
355 console.log(' %s DEBUG=%s:* npm start', prompt, name)
356 }
357
358 console.log()
359}
360
361/**
362 * Create an app name from a directory path, fitting npm naming requirements.
363 *
364 * @param {String} pathName
365 */
366
367function createAppName (pathName) {
368 return path.basename(pathName)
369 .replace(/[^A-Za-z0-9.-]+/g, '-')
370 .replace(/^[-_.]+|-+$/g, '')
371 .toLowerCase()
372}
373
374/**
375 * Check if the given directory `dir` is empty.
376 *
377 * @param {String} dir
378 * @param {Function} fn
379 */
380
381function emptyDirectory (dir, fn) {
382 fs.readdir(dir, function (err, files) {
383 if (err && err.code !== 'ENOENT') throw err
384 fn(!files || !files.length)
385 })
386}
387
388/**
389 * Graceful exit for async STDIO
390 */
391
392function exit (code) {
393 // flush output for Node.js Windows pipe bug
394 // https://github.com/joyent/node/issues/6247 is just one bug example
395 // https://github.com/visionmedia/mocha/issues/333 has a good discussion
396 function done () {
397 if (!(draining--)) _exit(code)
398 }
399
400 var draining = 0
401 var streams = [process.stdout, process.stderr]
402
403 exit.exited = true
404
405 streams.forEach(function (stream) {
406 // submit empty write request and wait for completion
407 draining += 1
408 stream.write('', done)
409 })
410
411 done()
412}
413
414/**
415 * Determine if launched from cmd.exe
416 */
417
418function launchedFromCmd () {
419 return process.platform === 'win32' &&
420 process.env._ === undefined
421}
422
423/**
424 * Load template file.
425 */
426
427function loadTemplate (name) {
428 var contents = fs.readFileSync(path.join(__dirname, '..', 'templates', (name + '.ejs')), 'utf-8')
429 var locals = Object.create(null)
430
431 function render () {
432 return ejs.render(contents, locals, {
433 escape: util.inspect
434 })
435 }
436
437 return {
438 locals: locals,
439 render: render
440 }
441}
442
443/**
444 * Main program.
445 */
446
447function main () {
448 // Path
449 var destinationPath = program.args.shift() || '.'
450
451 // App name
452 var appName = createAppName(path.resolve(destinationPath)) || 'hello-world'
453
454 // View engine
455 if (program.view === true) {
456 if (program.ejs) program.view = 'ejs'
457 if (program.hbs) program.view = 'hbs'
458 if (program.hogan) program.view = 'hjs'
459 if (program.pug) program.view = 'pug'
460 }
461
462 // Default view engine
463 if (program.view === true) {
464 warning('the default view engine will not be jade in future releases\n' +
465 "use `--view=jade' or `--help' for additional options")
466 program.view = 'jade'
467 }
468
469 // Generate application
470 emptyDirectory(destinationPath, function (empty) {
471 if (empty || program.force) {
472 createApplication(appName, destinationPath)
473 } else {
474 confirm('destination is not empty, continue? [y/N] ', function (ok) {
475 if (ok) {
476 process.stdin.destroy()
477 createApplication(appName, destinationPath)
478 } else {
479 console.error('aborting')
480 exit(1)
481 }
482 })
483 }
484 })
485}
486
487/**
488 * Make the given dir relative to base.
489 *
490 * @param {string} base
491 * @param {string} dir
492 */
493
494function mkdir (base, dir) {
495 var loc = path.join(base, dir)
496
497 console.log(' \x1b[36mcreate\x1b[0m : ' + loc + path.sep)
498 mkdirp.sync(loc, MODE_0755)
499}
500
501/**
502 * Generate a callback function for commander to warn about renamed option.
503 *
504 * @param {String} originalName
505 * @param {String} newName
506 */
507
508function renamedOption (originalName, newName) {
509 return function (val) {
510 warning(util.format("option `%s' has been renamed to `%s'", originalName, newName))
511 return val
512 }
513}
514
515/**
516 * Display a warning similar to how errors are displayed by commander.
517 *
518 * @param {String} message
519 */
520
521function warning (message) {
522 console.error()
523 message.split('\n').forEach(function (line) {
524 console.error(' warning: %s', line)
525 })
526 console.error()
527}
528
529/**
530 * echo str > file.
531 *
532 * @param {String} file
533 * @param {String} str
534 */
535
536function write (file, str, mode) {
537 fs.writeFileSync(file, str, { mode: mode || MODE_0666 })
538 console.log(' \x1b[36mcreate\x1b[0m : ' + file)
539}