express-generator
Version:
Express' application generator
540 lines (457 loc) • 13.5 kB
JavaScript
var ejs = require('ejs')
var fs = require('fs')
var minimatch = require('minimatch')
var mkdirp = require('mkdirp')
var path = require('path')
var program = require('commander')
var readline = require('readline')
var sortedObject = require('sorted-object')
var util = require('util')
var MODE_0666 = parseInt('0666', 8)
var MODE_0755 = parseInt('0755', 8)
var TEMPLATE_DIR = path.join(__dirname, '..', 'templates')
var VERSION = require('../package').version
var _exit = process.exit
// Re-assign process.exit because of commander
// TODO: Switch to a different command framework
process.exit = exit
// CLI
around(program, 'optionMissingArgument', function (fn, args) {
program.outputHelp()
fn.apply(this, args)
return { args: [], unknown: [] }
})
before(program, 'outputHelp', function () {
// track if help was shown for unknown option
this._helpShown = true
})
before(program, 'unknownOption', function () {
// allow unknown options if help was shown, to prevent trailing error
this._allowUnknownOption = this._helpShown
// show help if not yet shown
if (!this._helpShown) {
program.outputHelp()
}
})
program
.name('express')
.version(VERSION, ' --version')
.usage('[options] [dir]')
.option('-e, --ejs', 'add ejs engine support', renamedOption('--ejs', '--view=ejs'))
.option(' --pug', 'add pug engine support', renamedOption('--pug', '--view=pug'))
.option(' --hbs', 'add handlebars engine support', renamedOption('--hbs', '--view=hbs'))
.option('-H, --hogan', 'add hogan.js engine support', renamedOption('--hogan', '--view=hogan'))
.option('-v, --view <engine>', 'add view <engine> support (dust|ejs|hbs|hjs|jade|pug|twig|vash) (defaults to jade)')
.option(' --no-view', 'use static html instead of view engine')
.option('-c, --css <engine>', 'add stylesheet <engine> support (less|stylus|compass|sass) (defaults to plain css)')
.option(' --git', 'add .gitignore')
.option('-f, --force', 'force on non-empty directory')
.parse(process.argv)
if (!exit.exited) {
main()
}
/**
* Install an around function; AOP.
*/
function around (obj, method, fn) {
var old = obj[method]
obj[method] = function () {
var args = new Array(arguments.length)
for (var i = 0; i < args.length; i++) args[i] = arguments[i]
return fn.call(this, old, args)
}
}
/**
* Install a before function; AOP.
*/
function before (obj, method, fn) {
var old = obj[method]
obj[method] = function () {
fn.call(this)
old.apply(this, arguments)
}
}
/**
* Prompt for confirmation on STDOUT/STDIN
*/
function confirm (msg, callback) {
var rl = readline.createInterface({
input: process.stdin,
output: process.stdout
})
rl.question(msg, function (input) {
rl.close()
callback(/^y|yes|ok|true$/i.test(input))
})
}
/**
* Copy file from template directory.
*/
function copyTemplate (from, to) {
write(to, fs.readFileSync(path.join(TEMPLATE_DIR, from), 'utf-8'))
}
/**
* Copy multiple files from template directory.
*/
function copyTemplateMulti (fromDir, toDir, nameGlob) {
fs.readdirSync(path.join(TEMPLATE_DIR, fromDir))
.filter(minimatch.filter(nameGlob, { matchBase: true }))
.forEach(function (name) {
copyTemplate(path.join(fromDir, name), path.join(toDir, name))
})
}
/**
* Create application at the given directory.
*
* @param {string} name
* @param {string} dir
*/
function createApplication (name, dir) {
console.log()
// Package
var pkg = {
name: name,
version: '0.0.0',
private: true,
scripts: {
start: 'node ./bin/www'
},
dependencies: {
'debug': '~2.6.9',
'express': '~4.16.1'
}
}
// JavaScript
var app = loadTemplate('js/app.js')
var www = loadTemplate('js/www')
// App name
www.locals.name = name
// App modules
app.locals.localModules = Object.create(null)
app.locals.modules = Object.create(null)
app.locals.mounts = []
app.locals.uses = []
// Request logger
app.locals.modules.logger = 'morgan'
app.locals.uses.push("logger('dev')")
pkg.dependencies.morgan = '~1.9.1'
// Body parsers
app.locals.uses.push('express.json()')
app.locals.uses.push('express.urlencoded({ extended: false })')
// Cookie parser
app.locals.modules.cookieParser = 'cookie-parser'
app.locals.uses.push('cookieParser()')
pkg.dependencies['cookie-parser'] = '~1.4.4'
if (dir !== '.') {
mkdir(dir, '.')
}
mkdir(dir, 'public')
mkdir(dir, 'public/javascripts')
mkdir(dir, 'public/images')
mkdir(dir, 'public/stylesheets')
// copy css templates
switch (program.css) {
case 'less':
copyTemplateMulti('css', dir + '/public/stylesheets', '*.less')
break
case 'stylus':
copyTemplateMulti('css', dir + '/public/stylesheets', '*.styl')
break
case 'compass':
copyTemplateMulti('css', dir + '/public/stylesheets', '*.scss')
break
case 'sass':
copyTemplateMulti('css', dir + '/public/stylesheets', '*.sass')
break
default:
copyTemplateMulti('css', dir + '/public/stylesheets', '*.css')
break
}
// copy route templates
mkdir(dir, 'routes')
copyTemplateMulti('js/routes', dir + '/routes', '*.js')
if (program.view) {
// Copy view templates
mkdir(dir, 'views')
pkg.dependencies['http-errors'] = '~1.6.3'
switch (program.view) {
case 'dust':
copyTemplateMulti('views', dir + '/views', '*.dust')
break
case 'ejs':
copyTemplateMulti('views', dir + '/views', '*.ejs')
break
case 'hbs':
copyTemplateMulti('views', dir + '/views', '*.hbs')
break
case 'hjs':
copyTemplateMulti('views', dir + '/views', '*.hjs')
break
case 'jade':
copyTemplateMulti('views', dir + '/views', '*.jade')
break
case 'pug':
copyTemplateMulti('views', dir + '/views', '*.pug')
break
case 'twig':
copyTemplateMulti('views', dir + '/views', '*.twig')
break
case 'vash':
copyTemplateMulti('views', dir + '/views', '*.vash')
break
}
} else {
// Copy extra public files
copyTemplate('js/index.html', path.join(dir, 'public/index.html'))
}
// CSS Engine support
switch (program.css) {
case 'compass':
app.locals.modules.compass = 'node-compass'
app.locals.uses.push("compass({ mode: 'expanded' })")
pkg.dependencies['node-compass'] = '0.2.3'
break
case 'less':
app.locals.modules.lessMiddleware = 'less-middleware'
app.locals.uses.push("lessMiddleware(path.join(__dirname, 'public'))")
pkg.dependencies['less-middleware'] = '~2.2.1'
break
case 'sass':
app.locals.modules.sassMiddleware = 'node-sass-middleware'
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})")
pkg.dependencies['node-sass-middleware'] = '0.11.0'
break
case 'stylus':
app.locals.modules.stylus = 'stylus'
app.locals.uses.push("stylus.middleware(path.join(__dirname, 'public'))")
pkg.dependencies['stylus'] = '0.54.5'
break
}
// Index router mount
app.locals.localModules.indexRouter = './routes/index'
app.locals.mounts.push({ path: '/', code: 'indexRouter' })
// User router mount
app.locals.localModules.usersRouter = './routes/users'
app.locals.mounts.push({ path: '/users', code: 'usersRouter' })
// Template support
switch (program.view) {
case 'dust':
app.locals.modules.adaro = 'adaro'
app.locals.view = {
engine: 'dust',
render: 'adaro.dust()'
}
pkg.dependencies.adaro = '~1.0.4'
break
case 'ejs':
app.locals.view = { engine: 'ejs' }
pkg.dependencies.ejs = '~2.6.1'
break
case 'hbs':
app.locals.view = { engine: 'hbs' }
pkg.dependencies.hbs = '~4.0.4'
break
case 'hjs':
app.locals.view = { engine: 'hjs' }
pkg.dependencies.hjs = '~0.0.6'
break
case 'jade':
app.locals.view = { engine: 'jade' }
pkg.dependencies.jade = '~1.11.0'
break
case 'pug':
app.locals.view = { engine: 'pug' }
pkg.dependencies.pug = '2.0.0-beta11'
break
case 'twig':
app.locals.view = { engine: 'twig' }
pkg.dependencies.twig = '~0.10.3'
break
case 'vash':
app.locals.view = { engine: 'vash' }
pkg.dependencies.vash = '~0.12.6'
break
default:
app.locals.view = false
break
}
// Static files
app.locals.uses.push("express.static(path.join(__dirname, 'public'))")
if (program.git) {
copyTemplate('js/gitignore', path.join(dir, '.gitignore'))
}
// sort dependencies like npm(1)
pkg.dependencies = sortedObject(pkg.dependencies)
// write files
write(path.join(dir, 'app.js'), app.render())
write(path.join(dir, 'package.json'), JSON.stringify(pkg, null, 2) + '\n')
mkdir(dir, 'bin')
write(path.join(dir, 'bin/www'), www.render(), MODE_0755)
var prompt = launchedFromCmd() ? '>' : '$'
if (dir !== '.') {
console.log()
console.log(' change directory:')
console.log(' %s cd %s', prompt, dir)
}
console.log()
console.log(' install dependencies:')
console.log(' %s npm install', prompt)
console.log()
console.log(' run the app:')
if (launchedFromCmd()) {
console.log(' %s SET DEBUG=%s:* & npm start', prompt, name)
} else {
console.log(' %s DEBUG=%s:* npm start', prompt, name)
}
console.log()
}
/**
* Create an app name from a directory path, fitting npm naming requirements.
*
* @param {String} pathName
*/
function createAppName (pathName) {
return path.basename(pathName)
.replace(/[^A-Za-z0-9.-]+/g, '-')
.replace(/^[-_.]+|-+$/g, '')
.toLowerCase()
}
/**
* Check if the given directory `dir` is empty.
*
* @param {String} dir
* @param {Function} fn
*/
function emptyDirectory (dir, fn) {
fs.readdir(dir, function (err, files) {
if (err && err.code !== 'ENOENT') throw err
fn(!files || !files.length)
})
}
/**
* Graceful exit for async STDIO
*/
function exit (code) {
// flush output for Node.js Windows pipe bug
// https://github.com/joyent/node/issues/6247 is just one bug example
// https://github.com/visionmedia/mocha/issues/333 has a good discussion
function done () {
if (!(draining--)) _exit(code)
}
var draining = 0
var streams = [process.stdout, process.stderr]
exit.exited = true
streams.forEach(function (stream) {
// submit empty write request and wait for completion
draining += 1
stream.write('', done)
})
done()
}
/**
* Determine if launched from cmd.exe
*/
function launchedFromCmd () {
return process.platform === 'win32' &&
process.env._ === undefined
}
/**
* Load template file.
*/
function loadTemplate (name) {
var contents = fs.readFileSync(path.join(__dirname, '..', 'templates', (name + '.ejs')), 'utf-8')
var locals = Object.create(null)
function render () {
return ejs.render(contents, locals, {
escape: util.inspect
})
}
return {
locals: locals,
render: render
}
}
/**
* Main program.
*/
function main () {
// Path
var destinationPath = program.args.shift() || '.'
// App name
var appName = createAppName(path.resolve(destinationPath)) || 'hello-world'
// View engine
if (program.view === true) {
if (program.ejs) program.view = 'ejs'
if (program.hbs) program.view = 'hbs'
if (program.hogan) program.view = 'hjs'
if (program.pug) program.view = 'pug'
}
// Default view engine
if (program.view === true) {
warning('the default view engine will not be jade in future releases\n' +
"use `--view=jade' or `--help' for additional options")
program.view = 'jade'
}
// Generate application
emptyDirectory(destinationPath, function (empty) {
if (empty || program.force) {
createApplication(appName, destinationPath)
} else {
confirm('destination is not empty, continue? [y/N] ', function (ok) {
if (ok) {
process.stdin.destroy()
createApplication(appName, destinationPath)
} else {
console.error('aborting')
exit(1)
}
})
}
})
}
/**
* Make the given dir relative to base.
*
* @param {string} base
* @param {string} dir
*/
function mkdir (base, dir) {
var loc = path.join(base, dir)
console.log(' \x1b[36mcreate\x1b[0m : ' + loc + path.sep)
mkdirp.sync(loc, MODE_0755)
}
/**
* Generate a callback function for commander to warn about renamed option.
*
* @param {String} originalName
* @param {String} newName
*/
function renamedOption (originalName, newName) {
return function (val) {
warning(util.format("option `%s' has been renamed to `%s'", originalName, newName))
return val
}
}
/**
* Display a warning similar to how errors are displayed by commander.
*
* @param {String} message
*/
function warning (message) {
console.error()
message.split('\n').forEach(function (line) {
console.error(' warning: %s', line)
})
console.error()
}
/**
* echo str > file.
*
* @param {String} file
* @param {String} str
*/
function write (file, str, mode) {
fs.writeFileSync(file, str, { mode: mode || MODE_0666 })
console.log(' \x1b[36mcreate\x1b[0m : ' + file)
}