1 | #!/usr/bin/env node
|
2 |
|
3 | var ejs = require('ejs')
|
4 | var fs = require('fs')
|
5 | var minimatch = require('minimatch')
|
6 | var mkdirp = require('mkdirp')
|
7 | var path = require('path')
|
8 | var program = require('commander')
|
9 | var readline = require('readline')
|
10 | var sortedObject = require('sorted-object')
|
11 | var util = require('util')
|
12 |
|
13 | var MODE_0666 = parseInt('0666', 8)
|
14 | var MODE_0755 = parseInt('0755', 8)
|
15 | var TEMPLATE_DIR = path.join(__dirname, '..', 'templates')
|
16 | var VERSION = require('../package').version
|
17 |
|
18 | var _exit = process.exit
|
19 |
|
20 |
|
21 |
|
22 | process.exit = exit
|
23 |
|
24 |
|
25 |
|
26 | around(program, 'optionMissingArgument', function (fn, args) {
|
27 | program.outputHelp()
|
28 | fn.apply(this, args)
|
29 | return { args: [], unknown: [] }
|
30 | })
|
31 |
|
32 | before(program, 'outputHelp', function () {
|
33 |
|
34 | this._helpShown = true
|
35 | })
|
36 |
|
37 | before(program, 'unknownOption', function () {
|
38 |
|
39 | this._allowUnknownOption = this._helpShown
|
40 |
|
41 |
|
42 | if (!this._helpShown) {
|
43 | program.outputHelp()
|
44 | }
|
45 | })
|
46 |
|
47 | program
|
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 |
|
62 | if (!exit.exited) {
|
63 | main()
|
64 | }
|
65 |
|
66 |
|
67 |
|
68 |
|
69 |
|
70 | function 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 |
|
82 |
|
83 |
|
84 | function 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 |
|
95 |
|
96 |
|
97 | function 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 |
|
111 |
|
112 |
|
113 | function copyTemplate (from, to) {
|
114 | write(to, fs.readFileSync(path.join(TEMPLATE_DIR, from), 'utf-8'))
|
115 | }
|
116 |
|
117 |
|
118 |
|
119 |
|
120 |
|
121 | function 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 |
|
131 |
|
132 |
|
133 |
|
134 |
|
135 |
|
136 | function createApplication (name, dir) {
|
137 | console.log()
|
138 |
|
139 |
|
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 |
|
154 | var app = loadTemplate('js/app.js')
|
155 | var www = loadTemplate('js/www')
|
156 |
|
157 |
|
158 | www.locals.name = name
|
159 |
|
160 |
|
161 | app.locals.localModules = Object.create(null)
|
162 | app.locals.modules = Object.create(null)
|
163 | app.locals.mounts = []
|
164 | app.locals.uses = []
|
165 |
|
166 |
|
167 | app.locals.modules.logger = 'morgan'
|
168 | app.locals.uses.push("logger('dev')")
|
169 | pkg.dependencies.morgan = '~1.9.1'
|
170 |
|
171 |
|
172 | app.locals.uses.push('express.json()')
|
173 | app.locals.uses.push('express.urlencoded({ extended: false })')
|
174 |
|
175 |
|
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 |
|
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 |
|
209 | mkdir(dir, 'routes')
|
210 | copyTemplateMulti('js/routes', dir + '/routes', '*.js')
|
211 |
|
212 | if (program.view) {
|
213 |
|
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 |
|
244 | copyTemplate('js/index.html', path.join(dir, 'public/index.html'))
|
245 | }
|
246 |
|
247 |
|
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 |
|
272 | app.locals.localModules.indexRouter = './routes/index'
|
273 | app.locals.mounts.push({ path: '/', code: 'indexRouter' })
|
274 |
|
275 |
|
276 | app.locals.localModules.usersRouter = './routes/users'
|
277 | app.locals.mounts.push({ path: '/users', code: 'usersRouter' })
|
278 |
|
279 |
|
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 |
|
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 |
|
330 | pkg.dependencies = sortedObject(pkg.dependencies)
|
331 |
|
332 |
|
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 |
|
363 |
|
364 |
|
365 |
|
366 |
|
367 | function createAppName (pathName) {
|
368 | return path.basename(pathName)
|
369 | .replace(/[^A-Za-z0-9.-]+/g, '-')
|
370 | .replace(/^[-_.]+|-+$/g, '')
|
371 | .toLowerCase()
|
372 | }
|
373 |
|
374 |
|
375 |
|
376 |
|
377 |
|
378 |
|
379 |
|
380 |
|
381 | function 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 |
|
390 |
|
391 |
|
392 | function exit (code) {
|
393 |
|
394 |
|
395 |
|
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 |
|
407 | draining += 1
|
408 | stream.write('', done)
|
409 | })
|
410 |
|
411 | done()
|
412 | }
|
413 |
|
414 |
|
415 |
|
416 |
|
417 |
|
418 | function launchedFromCmd () {
|
419 | return process.platform === 'win32' &&
|
420 | process.env._ === undefined
|
421 | }
|
422 |
|
423 |
|
424 |
|
425 |
|
426 |
|
427 | function 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 |
|
445 |
|
446 |
|
447 | function main () {
|
448 |
|
449 | var destinationPath = program.args.shift() || '.'
|
450 |
|
451 |
|
452 | var appName = createAppName(path.resolve(destinationPath)) || 'hello-world'
|
453 |
|
454 |
|
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 |
|
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 |
|
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 |
|
489 |
|
490 |
|
491 |
|
492 |
|
493 |
|
494 | function 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 |
|
503 |
|
504 |
|
505 |
|
506 |
|
507 |
|
508 | function 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 |
|
517 |
|
518 |
|
519 |
|
520 |
|
521 | function 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 |
|
531 |
|
532 |
|
533 |
|
534 |
|
535 |
|
536 | function write (file, str, mode) {
|
537 | fs.writeFileSync(file, str, { mode: mode || MODE_0666 })
|
538 | console.log(' \x1b[36mcreate\x1b[0m : ' + file)
|
539 | }
|