diff --git a/README.md b/README.md index c287c558..23a30f7f 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,7 @@ This generator can also be further configured with the following command line fl --no-view use static html instead of view engine -c, --css add stylesheet support (less|stylus|compass|sass) (defaults to plain css) --git add .gitignore + --es6 generate ES6 code and module-type project (requires Node 14.x or higher) -f, --force force on non-empty directory -h, --help output usage information diff --git a/bin/express-cli.js b/bin/express-cli.js index 380a447d..7dd4162d 100755 --- a/bin/express-cli.js +++ b/bin/express-cli.js @@ -14,6 +14,7 @@ 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 MIN_ES6_VERSION = 14 // parse args var unknown = [] @@ -26,7 +27,7 @@ var args = parseArgs(process.argv.slice(2), { H: 'hogan', v: 'view' }, - boolean: ['ejs', 'force', 'git', 'hbs', 'help', 'hogan', 'pug', 'version'], + boolean: ['ejs', 'es6', 'force', 'git', 'hbs', 'help', 'hogan', 'pug', 'version'], default: { css: true, view: true }, string: ['css', 'view'], unknown: function (s) { @@ -95,17 +96,20 @@ function createApplication (name, dir, options, done) { version: '0.0.0', private: true, scripts: { - start: 'node ./bin/www' + start: options.es6 ? 'node ./bin/www.mjs' : 'node ./bin/www' }, dependencies: { debug: '~2.6.9', express: '~4.17.1' } } + if (options.es6) { + pkg.type = 'module' + } // JavaScript - var app = loadTemplate('js/app.js') - var www = loadTemplate('js/www') + var app = loadTemplate(options.es6 ? 'mjs/app.js' : 'js/app.js') + var www = loadTemplate(options.es6 ? 'mjs/www' : 'js/www') // App name www.locals.name = name @@ -160,7 +164,9 @@ function createApplication (name, dir, options, done) { // copy route templates mkdir(dir, 'routes') - copyTemplateMulti('js/routes', dir + '/routes', '*.js') + copyTemplateMulti( + options.es6 ? 'mjs/routes' : 'js/routes', + dir + '/routes', options.es6 ? '*.mjs' : '*.js') if (options.view) { // Copy view templates @@ -283,10 +289,10 @@ function createApplication (name, dir, options, done) { pkg.dependencies = sortedObject(pkg.dependencies) // write files - write(path.join(dir, 'app.js'), app.render()) + write(path.join(dir, options.es6 ? 'app.mjs' : '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) + write(path.join(dir, options.es6 ? 'bin/www.mjs' : 'bin/www'), www.render(), MODE_0755) var prompt = launchedFromCmd() ? '>' : '$' @@ -433,6 +439,10 @@ function main (options, done) { usage() error('option `-v, --view \' argument missing') done(1) + } else if (options.es6 && process.versions.node.split('.')[0] < MIN_ES6_VERSION) { + usage() + error('option `--es6\' requires Node version ' + MIN_ES6_VERSION + '.x or higher') + done(1) } else { // Path var destinationPath = options._[0] || '.' @@ -521,6 +531,7 @@ function usage () { console.log(' --no-view use static html instead of view engine') console.log(' -c, --css add stylesheet support (less|stylus|compass|sass) (defaults to plain css)') console.log(' --git add .gitignore') + console.log(' --es6 generate ES6 code and module-type project (requires Node 14.x or higher)') console.log(' -f, --force force on non-empty directory') console.log(' --version output the version number') console.log(' -h, --help output usage information') diff --git a/templates/mjs/app.js.ejs b/templates/mjs/app.js.ejs new file mode 100644 index 00000000..ae5e7341 --- /dev/null +++ b/templates/mjs/app.js.ejs @@ -0,0 +1,55 @@ +<% if (view) { -%> +import createError from 'http-errors'; +<% } -%> +import path from 'path'; +import { fileURLToPath } from 'url'; + +import express from 'express'; +<% Object.keys(modules).sort().forEach(function (variable) { -%> +import <%- variable %> from '<%- modules[variable] %>'; +<% }); -%> + +<% Object.keys(localModules).sort().forEach(function (variable) { -%> +import <%- variable %> from '<%- localModules[variable] %>.mjs'; +<% }); -%> + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +const app = express(); + +<% if (view) { -%> +// view engine setup +<% if (view.render) { -%> +app.engine('<%- view.engine %>', <%- view.render %>); +<% } -%> +app.set('views', path.join(__dirname, 'views')); +app.set('view engine', '<%- view.engine %>'); + +<% } -%> +<% uses.forEach(function (use) { -%> +app.use(<%- use %>); +<% }); -%> + +<% mounts.forEach(function (mount) { -%> +app.use(<%= mount.path %>, <%- mount.code %>); +<% }); -%> + +<% if (view) { -%> +// catch 404 and forward to error handler +app.use((req, res, next) => { + next(createError(404)); +}); + +// error handler +app.use((err, req, res, next) => { + // set locals, only providing error in development + res.locals.message = err.message; + res.locals.error = req.app.get('env') === 'development' ? err : {}; + + // render the error page + res.status(err.status || 500); + res.render('error'); +}); + +<% } -%> +export default app; diff --git a/templates/mjs/routes/index.mjs b/templates/mjs/routes/index.mjs new file mode 100644 index 00000000..cacebed8 --- /dev/null +++ b/templates/mjs/routes/index.mjs @@ -0,0 +1,9 @@ +import express from 'express'; +const router = express.Router(); + +/* GET home page. */ +router.get('/', (req, res, next) => { + res.render('index', { title: 'Express' }); +}); + +export default router; diff --git a/templates/mjs/routes/users.mjs b/templates/mjs/routes/users.mjs new file mode 100644 index 00000000..26448614 --- /dev/null +++ b/templates/mjs/routes/users.mjs @@ -0,0 +1,9 @@ +import express from 'express'; +const router = express.Router(); + +/* GET users listing. */ +router.get('/', (req, res, next) => { + res.send('respond with a resource'); +}); + +export default router; diff --git a/templates/mjs/www.ejs b/templates/mjs/www.ejs new file mode 100644 index 00000000..bf48377a --- /dev/null +++ b/templates/mjs/www.ejs @@ -0,0 +1,92 @@ +#!/usr/bin/env node + +/** + * Module dependencies. + */ + +import http from 'http'; + +import app from '../app.mjs'; +import debugFunction from 'debug'; +const debug = debugFunction('<%- name %>:server'); + +/** + * Get port from environment and store in Express. + */ + +const port = normalizePort(process.env.PORT || '3000'); +app.set('port', port); + +/** + * Create HTTP server. + */ + +const server = http.createServer(app); + +/** + * Listen on provided port, on all network interfaces. + */ + +server.listen(port); +server.on('error', onError); +server.on('listening', onListening); + +/** + * Normalize a port into a number, string, or false. + */ + +function normalizePort(val) { + const port = parseInt(val, 10); + + if (isNaN(port)) { + // named pipe + return val; + } + + if (port >= 0) { + // port number + return port; + } + + return false; +} + +/** + * Event listener for HTTP server "error" event. + */ + +function onError(error) { + if (error.syscall !== 'listen') { + throw error; + } + + const bind = typeof port === 'string' + ? 'Pipe ' + port + : 'Port ' + port; + + // handle specific listen errors with friendly messages + switch (error.code) { + case 'EACCES': + console.error(bind + ' requires elevated privileges'); + process.exit(1); + break; + case 'EADDRINUSE': + console.error(bind + ' is already in use'); + process.exit(1); + break; + default: + throw error; + } +} + +/** + * Event listener for HTTP server "listening" event. + */ + +function onListening() { + const addr = server.address(); + const bind = typeof addr === 'string' + ? 'pipe ' + addr + : 'port ' + addr.port; + debug('Listening on ' + bind); +} diff --git a/test/cmd.js b/test/cmd.js index 4a102a94..6a26fb22 100644 --- a/test/cmd.js +++ b/test/cmd.js @@ -17,6 +17,7 @@ var BIN_PATH = path.resolve(path.dirname(PKG_PATH), require(PKG_PATH).bin.expres var NPM_INSTALL_TIMEOUT = 300000 // 5 minutes var STDERR_MAX_BUFFER = 5 * 1024 * 1024 // 5mb var TEMP_DIR = utils.tmpDir() +var MIN_ES6_VERSION = 14 describe('express(1)', function () { after(function (done) { @@ -484,6 +485,143 @@ describe('express(1)', function () { }) }) + describe('--es6', function () { + var ctx = setupTestEnvironment(this.fullTitle()) + + if (process.versions.node.split('.')[0] < MIN_ES6_VERSION) { + it('should exit with code 1', function (done) { + runRaw(ctx.dir, ['--es6'], function (err, code, stdout, stderr) { + if (err) return done(err) + assert.strictEqual(code, 1) + done() + }) + }) + + it('should print usage and error message', function (done) { + runRaw(ctx.dir, ['--es6'], function (err, code, stdout, stderr) { + if (err) return done(err) + assert.ok(/Usage: express /.test(stdout)) + assert.ok(/--help/.test(stdout)) + assert.ok(/--version/.test(stdout)) + var reg = RegExp('error: option `--es6\' requires Node version ' + MIN_ES6_VERSION) + assert.ok(reg.test(stderr)) + done() + }) + }) + } else { + it('should create basic app', function (done) { + run(ctx.dir, ['--es6'], function (err, stdout, warnings) { + if (err) return done(err) + ctx.files = utils.parseCreatedFiles(stdout, ctx.dir) + ctx.stdout = stdout + ctx.warnings = warnings + assert.strictEqual(ctx.files.length, 16) + done() + }) + }) + + it('should print jade view warning', function () { + assert.ok(ctx.warnings.some(function (warn) { + return warn === 'the default view engine will not be jade in future releases\nuse `--view=jade\' or `--help\' for additional options' + })) + }) + + it('should provide debug instructions', function () { + assert.ok(/DEBUG=express-1---es6:\* (?:& )?npm start/.test(ctx.stdout)) + }) + + it('should have basic files', function () { + assert.notStrictEqual(ctx.files.indexOf('bin/www.mjs'), -1) + assert.notStrictEqual(ctx.files.indexOf('app.mjs'), -1) + assert.notStrictEqual(ctx.files.indexOf('package.json'), -1) + }) + + it('should have jade templates', function () { + assert.notStrictEqual(ctx.files.indexOf('views/error.jade'), -1) + assert.notStrictEqual(ctx.files.indexOf('views/index.jade'), -1) + assert.notStrictEqual(ctx.files.indexOf('views/layout.jade'), -1) + }) + + it('should have a package.json file with type "module"', function () { + var file = path.resolve(ctx.dir, 'package.json') + var contents = fs.readFileSync(file, 'utf8') + assert.strictEqual(contents, '{\n' + + ' "name": "express-1---es6",\n' + + ' "version": "0.0.0",\n' + + ' "private": true,\n' + + ' "scripts": {\n' + + ' "start": "node ./bin/www.mjs"\n' + + ' },\n' + + ' "dependencies": {\n' + + ' "cookie-parser": "~1.4.5",\n' + + ' "debug": "~2.6.9",\n' + + ' "express": "~4.17.1",\n' + + ' "http-errors": "~1.7.2",\n' + + ' "jade": "~1.11.0",\n' + + ' "morgan": "~1.10.0"\n' + + ' },\n' + + ' "type": "module"\n' + + '}\n') + }) + + it('should have installable dependencies', function (done) { + this.timeout(NPM_INSTALL_TIMEOUT) + npmInstall(ctx.dir, done) + }) + + it('should export an express app from app.mjs', function (done) { + // Use eval since otherwise early Nodes choke on import reserved word + // eslint-disable-next-line no-eval + eval( + 'const { pathToFileURL } = require("url");' + + 'const file = path.resolve(ctx.dir, "app.mjs");' + + 'import(pathToFileURL(file).href)' + + '.then(moduleNamespaceObject => {' + + 'const app = moduleNamespaceObject.default;' + + 'assert.strictEqual(typeof app, "function");' + + 'assert.strictEqual(typeof app.handle, "function");' + + 'done();' + + '})' + + '.catch(reason => done(reason))' + ) + }) + + describe('npm start', function () { + before('start app', function () { + this.app = new AppRunner(ctx.dir) + }) + + after('stop app', function (done) { + this.timeout(APP_START_STOP_TIMEOUT) + this.app.stop(done) + }) + + it('should start app', function (done) { + this.timeout(APP_START_STOP_TIMEOUT) + this.app.start(done) + }) + + it('should respond to HTTP request', function (done) { + request(this.app) + .get('/') + .expect(200, /Express<\/title>/, done) + }) + + it('should respond to /users HTTP request', function (done) { + request(this.app) + .get('/users') + .expect(200, /respond with a resource/, done) + }) + + it('should generate a 404', function (done) { + request(this.app) + .get('/does_not_exist') + .expect(404, /<h1>Not Found<\/h1>/, done) + }) + }) + } + }) + describe('--git', function () { var ctx = setupTestEnvironment(this.fullTitle())