From ae46832fa1f0231726c353ccf88a04f5a897d677 Mon Sep 17 00:00:00 2001 From: drjeffjackson <6119466+drjeffjackson@users.noreply.github.com> Date: Wed, 3 Apr 2024 20:23:59 -0400 Subject: [PATCH 01/12] Add ES module support. Add --esm switch that generates a project using ES modules by default. --- README.md | 1 + bin/express-cli.js | 15 ++++-- templates/mjs/app.js.ejs | 54 +++++++++++++++++++++ templates/mjs/routes/index.js | 9 ++++ templates/mjs/routes/users.js | 9 ++++ templates/mjs/www.ejs | 91 +++++++++++++++++++++++++++++++++++ 6 files changed, 175 insertions(+), 4 deletions(-) create mode 100644 templates/mjs/app.js.ejs create mode 100644 templates/mjs/routes/index.js create mode 100644 templates/mjs/routes/users.js create mode 100644 templates/mjs/www.ejs diff --git a/README.md b/README.md index c319b093..140617fe 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,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 + --esm use ECMAScript modules -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..fea8708d 100755 --- a/bin/express-cli.js +++ b/bin/express-cli.js @@ -26,7 +26,7 @@ var args = parseArgs(process.argv.slice(2), { H: 'hogan', v: 'view' }, - boolean: ['ejs', 'force', 'git', 'hbs', 'help', 'hogan', 'pug', 'version'], + boolean: ['ejs', 'esm', 'force', 'git', 'hbs', 'help', 'hogan', 'pug', 'version'], default: { css: true, view: true }, string: ['css', 'view'], unknown: function (s) { @@ -93,6 +93,7 @@ function createApplication (name, dir, options, done) { var pkg = { name: name, version: '0.0.0', + type: options.esm ? 'module' : 'commonjs', private: true, scripts: { start: 'node ./bin/www' @@ -104,12 +105,15 @@ function createApplication (name, dir, options, done) { } // JavaScript - var app = loadTemplate('js/app.js') - var www = loadTemplate('js/www') + var app = loadTemplate(options.esm ? 'mjs/app.js' : 'js/app.js') + var www = loadTemplate(options.esm ? 'mjs/www' : 'js/www') // App name www.locals.name = name + // App module type + app.locals.esm = options.esm + // App modules app.locals.localModules = Object.create(null) app.locals.modules = Object.create(null) @@ -160,7 +164,9 @@ function createApplication (name, dir, options, done) { // copy route templates mkdir(dir, 'routes') - copyTemplateMulti('js/routes', dir + '/routes', '*.js') + copyTemplateMulti( + options.esm ? 'mjs/routes' : 'js/routes', + dir + '/routes', '*.js') if (options.view) { // Copy view templates @@ -521,6 +527,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(' --esm use ECMAScript modules') 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..ae6bed93 --- /dev/null +++ b/templates/mjs/app.js.ejs @@ -0,0 +1,54 @@ +<% if (view) { -%> +import createError from 'http-errors'; +<% } -%> +import express from 'express'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +<% Object.keys(modules).sort().forEach(function (variable) { -%> +import <%- variable %> from '<%- modules[variable] %>'; +<% }); -%> + +<% Object.keys(localModules).sort().forEach(function (variable) { -%> +import <%- variable %> from '<%- localModules[variable] %>.js'; +<% }); -%> + +var app = express(); + +<% if (view) { -%> +// view engine setup +<% if (view.render) { -%> +app.engine('<%- view.engine %>', <%- view.render %>); +<% } -%> + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +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(function(req, res, next) { + next(createError(404)); +}); + +// error handler +app.use(function(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.js b/templates/mjs/routes/index.js new file mode 100644 index 00000000..18543562 --- /dev/null +++ b/templates/mjs/routes/index.js @@ -0,0 +1,9 @@ +import express from 'express'; +var router = express.Router(); + +/* GET home page. */ +router.get('/', function(req, res, next) { + res.render('index', { title: 'Express' }); +}); + +export default router; diff --git a/templates/mjs/routes/users.js b/templates/mjs/routes/users.js new file mode 100644 index 00000000..0080053d --- /dev/null +++ b/templates/mjs/routes/users.js @@ -0,0 +1,9 @@ +import express from 'express'; +var router = express.Router(); + +/* GET users listing. */ +router.get('/', function(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..4cacc936 --- /dev/null +++ b/templates/mjs/www.ejs @@ -0,0 +1,91 @@ +#!/usr/bin/env node + +/** + * Module dependencies. + */ + +import app from '../app.js'; +import http from 'node:http'; +import debugFunction from 'debug'; +const debug = debugFunction('<%- name %>:server'); + +/** + * Get port from environment and store in Express. + */ + +var port = normalizePort(process.env.PORT || '3000'); +app.set('port', port); + +/** + * Create HTTP server. + */ + +var 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) { + var 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; + } + + var 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() { + var addr = server.address(); + var bind = typeof addr === 'string' + ? 'pipe ' + addr + : 'port ' + addr.port; + debug('Listening on ' + bind); +} From b74e5822a5cfb9c439964a52021ce13e62af8b95 Mon Sep 17 00:00:00 2001 From: drjeffjackson <6119466+drjeffjackson@users.noreply.github.com> Date: Wed, 3 Apr 2024 21:29:44 -0400 Subject: [PATCH 02/12] Incorporate ES modules support in ejs files. --- bin/express-cli.js | 5 ++- templates/js/app.js.ejs | 26 ++++++++++++ templates/js/www.ejs | 7 ++++ templates/mjs/app.js.ejs | 54 ------------------------ templates/mjs/www.ejs | 91 ---------------------------------------- 5 files changed, 36 insertions(+), 147 deletions(-) delete mode 100644 templates/mjs/app.js.ejs delete mode 100644 templates/mjs/www.ejs diff --git a/bin/express-cli.js b/bin/express-cli.js index fea8708d..e442dfd2 100755 --- a/bin/express-cli.js +++ b/bin/express-cli.js @@ -105,14 +105,15 @@ function createApplication (name, dir, options, done) { } // JavaScript - var app = loadTemplate(options.esm ? 'mjs/app.js' : 'js/app.js') - var www = loadTemplate(options.esm ? 'mjs/www' : 'js/www') + var app = loadTemplate('js/app.js') + var www = loadTemplate('js/www') // App name www.locals.name = name // App module type app.locals.esm = options.esm + www.locals.esm = options.esm // App modules app.locals.localModules = Object.create(null) diff --git a/templates/js/app.js.ejs b/templates/js/app.js.ejs index 2ef75aec..1b0aa40c 100644 --- a/templates/js/app.js.ejs +++ b/templates/js/app.js.ejs @@ -1,3 +1,22 @@ +<% if (esm) { -%> + +<% if (view) { -%> +import createError from 'http-errors'; +<% } -%> +import express from 'express'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +<% Object.keys(modules).sort().forEach(function (variable) { -%> +import <%- variable %> from '<%- modules[variable] %>'; +<% }); -%> + +<% Object.keys(localModules).sort().forEach(function (variable) { -%> +import <%- variable %> from '<%- localModules[variable] %>.js'; +<% }); -%> + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +<% } else { -%> + <% if (view) { -%> var createError = require('http-errors'); <% } -%> @@ -11,6 +30,8 @@ var <%- variable %> = require('<%- modules[variable] %>'); var <%- variable %> = require('<%- localModules[variable] %>'); <% }); -%> +<% } -%> + var app = express(); <% if (view) { -%> @@ -48,4 +69,9 @@ app.use(function(err, req, res, next) { }); <% } -%> + +<% if (esm) { -%> +export default app; +<% } else { -%> module.exports = app; +<% } -%> \ No newline at end of file diff --git a/templates/js/www.ejs b/templates/js/www.ejs index 690fc488..92ac43d7 100644 --- a/templates/js/www.ejs +++ b/templates/js/www.ejs @@ -4,9 +4,16 @@ * Module dependencies. */ +<% if (esm) { -%> +import app from '../app.js'; +import http from 'node:http'; +import debugFunction from 'debug'; +const debug = debugFunction('<%- name %>:server'); +<% } else { -%> var app = require('../app'); var debug = require('debug')('<%- name %>:server'); var http = require('http'); +<% } -%> /** * Get port from environment and store in Express. diff --git a/templates/mjs/app.js.ejs b/templates/mjs/app.js.ejs deleted file mode 100644 index ae6bed93..00000000 --- a/templates/mjs/app.js.ejs +++ /dev/null @@ -1,54 +0,0 @@ -<% if (view) { -%> -import createError from 'http-errors'; -<% } -%> -import express from 'express'; -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; -<% Object.keys(modules).sort().forEach(function (variable) { -%> -import <%- variable %> from '<%- modules[variable] %>'; -<% }); -%> - -<% Object.keys(localModules).sort().forEach(function (variable) { -%> -import <%- variable %> from '<%- localModules[variable] %>.js'; -<% }); -%> - -var app = express(); - -<% if (view) { -%> -// view engine setup -<% if (view.render) { -%> -app.engine('<%- view.engine %>', <%- view.render %>); -<% } -%> - -const __dirname = path.dirname(fileURLToPath(import.meta.url)); -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(function(req, res, next) { - next(createError(404)); -}); - -// error handler -app.use(function(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/www.ejs b/templates/mjs/www.ejs deleted file mode 100644 index 4cacc936..00000000 --- a/templates/mjs/www.ejs +++ /dev/null @@ -1,91 +0,0 @@ -#!/usr/bin/env node - -/** - * Module dependencies. - */ - -import app from '../app.js'; -import http from 'node:http'; -import debugFunction from 'debug'; -const debug = debugFunction('<%- name %>:server'); - -/** - * Get port from environment and store in Express. - */ - -var port = normalizePort(process.env.PORT || '3000'); -app.set('port', port); - -/** - * Create HTTP server. - */ - -var 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) { - var 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; - } - - var 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() { - var addr = server.address(); - var bind = typeof addr === 'string' - ? 'pipe ' + addr - : 'port ' + addr.port; - debug('Listening on ' + bind); -} From e9d9dcc609b5d4f62a0ecbb7ed67656e688333d3 Mon Sep 17 00:00:00 2001 From: drjeffjackson <6119466+drjeffjackson@users.noreply.github.com> Date: Thu, 4 Apr 2024 08:47:28 -0400 Subject: [PATCH 03/12] Update app.js.ejs Eliminate whitespace differences in the generated app.js. --- templates/js/app.js.ejs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/templates/js/app.js.ejs b/templates/js/app.js.ejs index 1b0aa40c..0951e3c1 100644 --- a/templates/js/app.js.ejs +++ b/templates/js/app.js.ejs @@ -1,5 +1,4 @@ <% if (esm) { -%> - <% if (view) { -%> import createError from 'http-errors'; <% } -%> @@ -16,7 +15,6 @@ import <%- variable %> from '<%- localModules[variable] %>.js'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); <% } else { -%> - <% if (view) { -%> var createError = require('http-errors'); <% } -%> @@ -29,7 +27,6 @@ var <%- variable %> = require('<%- modules[variable] %>'); <% Object.keys(localModules).sort().forEach(function (variable) { -%> var <%- variable %> = require('<%- localModules[variable] %>'); <% }); -%> - <% } -%> var app = express(); @@ -69,9 +66,8 @@ app.use(function(err, req, res, next) { }); <% } -%> - <% if (esm) { -%> export default app; <% } else { -%> module.exports = app; -<% } -%> \ No newline at end of file +<% } -%> From 967aae26cd761e2906fd8950573b2ac9d10c703b Mon Sep 17 00:00:00 2001 From: drjeffjackson <6119466+drjeffjackson@users.noreply.github.com> Date: Thu, 4 Apr 2024 10:22:08 -0400 Subject: [PATCH 04/12] Add tests Adding testing. Necessitated adding .js extension to generated bin/www filename. --- bin/express-cli.js | 4 +- test/cmd.js | 114 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 116 insertions(+), 2 deletions(-) diff --git a/bin/express-cli.js b/bin/express-cli.js index e442dfd2..d534d08a 100755 --- a/bin/express-cli.js +++ b/bin/express-cli.js @@ -96,7 +96,7 @@ function createApplication (name, dir, options, done) { type: options.esm ? 'module' : 'commonjs', private: true, scripts: { - start: 'node ./bin/www' + start: options.esm ? 'node ./bin/www.js' : 'node ./bin/www' }, dependencies: { debug: '~2.6.9', @@ -293,7 +293,7 @@ function createApplication (name, dir, options, done) { 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) + write(path.join(dir, options.esm ? 'bin/www.js' : 'bin/www'), www.render(), MODE_0755) var prompt = launchedFromCmd() ? '>' : '$' diff --git a/test/cmd.js b/test/cmd.js index 4a102a94..134e2972 100644 --- a/test/cmd.js +++ b/test/cmd.js @@ -66,6 +66,7 @@ describe('express(1)', function () { assert.strictEqual(contents, '{\n' + ' "name": "express-1-no-args",\n' + ' "version": "0.0.0",\n' + + ' "type": "commonjs",\n' + ' "private": true,\n' + ' "scripts": {\n' + ' "start": "node ./bin/www"\n' + @@ -484,6 +485,119 @@ describe('express(1)', function () { }) }) + describe('--esm', function () { + var ctx = setupTestEnvironment(this.fullTitle()) + + it('should create basic app', function (done) { + run(ctx.dir, ['--esm'], 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---esm:\* (?:& )?npm start/.test(ctx.stdout)) + }) + + it('should have basic files', function () { + assert.notStrictEqual(ctx.files.indexOf('bin/www.js'), -1) + assert.notStrictEqual(ctx.files.indexOf('app.js'), -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---esm",\n' + + ' "version": "0.0.0",\n' + + ' "type": "module",\n' + + ' "private": true,\n' + + ' "scripts": {\n' + + ' "start": "node ./bin/www.js"\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' + + '}\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.js', function () { + var file = path.resolve(ctx.dir, 'app.js') + import(file) + .then(module => { + const app = module.default + assert.strictEqual(typeof app, 'function') + assert.strictEqual(typeof app.handle, 'function') + }) + /* + var app = require(file) + assert.strictEqual(typeof app, 'function') + assert.strictEqual(typeof app.handle, 'function') + */ + }) + + 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()) From 20a2a7d9d0c50d878a6f0ff936858ca57b66883b Mon Sep 17 00:00:00 2001 From: drjeffjackson <6119466+drjeffjackson@users.noreply.github.com> Date: Thu, 4 Apr 2024 10:50:26 -0400 Subject: [PATCH 05/12] Update cmd.js Remove commented-out code. --- test/cmd.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/test/cmd.js b/test/cmd.js index 134e2972..58a1461b 100644 --- a/test/cmd.js +++ b/test/cmd.js @@ -556,11 +556,6 @@ describe('express(1)', function () { assert.strictEqual(typeof app, 'function') assert.strictEqual(typeof app.handle, 'function') }) - /* - var app = require(file) - assert.strictEqual(typeof app, 'function') - assert.strictEqual(typeof app.handle, 'function') - */ }) describe('npm start', function () { From 1a70274d5099917cfd0b5394be6cbcb7cc5396cf Mon Sep 17 00:00:00 2001 From: drjeffjackson <6119466+drjeffjackson@users.noreply.github.com> Date: Fri, 5 Apr 2024 12:08:45 -0400 Subject: [PATCH 06/12] Exit if --esm switch is not supported Modify the --esm switch processing to error out of the generator if the Node version is less than 14. Update documentation and testing to reflect this change. --- README.md | 2 +- bin/express-cli.js | 7 +- test/cmd.js | 197 +++++++++++++++++++++++++-------------------- 3 files changed, 117 insertions(+), 89 deletions(-) diff --git a/README.md b/README.md index b474b2d8..e97fab17 100644 --- a/README.md +++ b/README.md @@ -54,7 +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 <engine> add stylesheet <engine> support (less|stylus|compass|sass) (defaults to plain css) --git add .gitignore - --esm use ECMAScript modules + --esm use ECMAScript modules (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 d534d08a..86873850 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_ESM_VERSION = 14 // parse args var unknown = [] @@ -440,6 +441,10 @@ function main (options, done) { usage() error('option `-v, --view <engine>\' argument missing') done(1) + } else if (options.esm && process.versions.node.split('.')[0] < MIN_ESM_VERSION) { + usage() + error('option `--esm\' requires Node version ' + MIN_ESM_VERSION + '.x or higher') + done(1) } else { // Path var destinationPath = options._[0] || '.' @@ -528,7 +533,7 @@ function usage () { console.log(' --no-view use static html instead of view engine') console.log(' -c, --css <engine> add stylesheet <engine> support (less|stylus|compass|sass) (defaults to plain css)') console.log(' --git add .gitignore') - console.log(' --esm use ECMAScript modules') + console.log(' --esm use ECMAScript modules (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/test/cmd.js b/test/cmd.js index 58a1461b..e6777389 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_ESM_VERSION = 14 describe('express(1)', function () { after(function (done) { @@ -488,109 +489,131 @@ describe('express(1)', function () { describe('--esm', function () { var ctx = setupTestEnvironment(this.fullTitle()) - it('should create basic app', function (done) { - run(ctx.dir, ['--esm'], 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() + if (process.versions.node.split('.')[0] < MIN_ESM_VERSION) { + it('should exit with code 1', function (done) { + runRaw(ctx.dir, ['--esm'], function (err, code, stdout, stderr) { + if (err) return done(err) + assert.strictEqual(code, 1) + 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---esm:\* (?:& )?npm start/.test(ctx.stdout)) - }) - - it('should have basic files', function () { - assert.notStrictEqual(ctx.files.indexOf('bin/www.js'), -1) - assert.notStrictEqual(ctx.files.indexOf('app.js'), -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---esm",\n' + - ' "version": "0.0.0",\n' + - ' "type": "module",\n' + - ' "private": true,\n' + - ' "scripts": {\n' + - ' "start": "node ./bin/www.js"\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' + - '}\n') - }) + it('should print usage and error message', function (done) { + runRaw(ctx.dir, ['--esm'], 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 `--esm\' requires Node version ' + MIN_ESM_VERSION) + assert.ok(reg.test(stderr)) + done() + }) + }) + } else { + it('should create basic app', function (done) { + run(ctx.dir, ['--esm'], 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 have installable dependencies', function (done) { - this.timeout(NPM_INSTALL_TIMEOUT) - npmInstall(ctx.dir, 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 export an express app from app.js', function () { - var file = path.resolve(ctx.dir, 'app.js') - import(file) - .then(module => { - const app = module.default - assert.strictEqual(typeof app, 'function') - assert.strictEqual(typeof app.handle, 'function') - }) - }) + it('should provide debug instructions', function () { + assert.ok(/DEBUG=express-1---esm:\* (?:& )?npm start/.test(ctx.stdout)) + }) - describe('npm start', function () { - before('start app', function () { - this.app = new AppRunner(ctx.dir) + it('should have basic files', function () { + assert.notStrictEqual(ctx.files.indexOf('bin/www.js'), -1) + assert.notStrictEqual(ctx.files.indexOf('app.js'), -1) + assert.notStrictEqual(ctx.files.indexOf('package.json'), -1) }) - after('stop app', function (done) { - this.timeout(APP_START_STOP_TIMEOUT) - this.app.stop(done) + 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 start app', function (done) { - this.timeout(APP_START_STOP_TIMEOUT) - this.app.start(done) + 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---esm",\n' + + ' "version": "0.0.0",\n' + + ' "type": "module",\n' + + ' "private": true,\n' + + ' "scripts": {\n' + + ' "start": "node ./bin/www.js"\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' + + '}\n') }) - it('should respond to HTTP request', function (done) { - request(this.app) - .get('/') - .expect(200, /<title>Express<\/title>/, done) + it('should have installable dependencies', function (done) { + this.timeout(NPM_INSTALL_TIMEOUT) + npmInstall(ctx.dir, done) }) - it('should respond to /users HTTP request', function (done) { - request(this.app) - .get('/users') - .expect(200, /respond with a resource/, done) + it('should export an express app from app.js', function () { + var file = path.resolve(ctx.dir, 'app.js') + import(file) + .then(module => { + const app = module.default + assert.strictEqual(typeof app, 'function') + assert.strictEqual(typeof app.handle, 'function') + }) }) - it('should generate a 404', function (done) { - request(this.app) - .get('/does_not_exist') - .expect(404, /<h1>Not Found<\/h1>/, done) + 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, /<title>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 () { From 5f3c3c0efac4ccd07381d2b38356a8216a48da65 Mon Sep 17 00:00:00 2001 From: drjeffjackson <6119466+drjeffjackson@users.noreply.github.com> Date: Sat, 6 Apr 2024 00:27:47 -0400 Subject: [PATCH 07/12] Test fix, update ESM code Fix esm 'should export an express app from app.js; test failures for early Node versions by moving import into a call to eval(). Also correct two errors in this test and include error handling. In addition, update all generated code when --esm is active to use const rather than var and arrow syntax rather than function expression syntax for callbacks. --- templates/js/app.js.ejs | 6 +++--- templates/js/www.ejs | 12 ++++++------ templates/mjs/routes/index.js | 4 ++-- templates/mjs/routes/users.js | 4 ++-- test/cmd.js | 23 +++++++++++++++-------- 5 files changed, 28 insertions(+), 21 deletions(-) diff --git a/templates/js/app.js.ejs b/templates/js/app.js.ejs index 0951e3c1..f5e691d2 100644 --- a/templates/js/app.js.ejs +++ b/templates/js/app.js.ejs @@ -29,7 +29,7 @@ var <%- variable %> = require('<%- localModules[variable] %>'); <% }); -%> <% } -%> -var app = express(); +<%- esm ? 'const' : 'var' %> app = express(); <% if (view) { -%> // view engine setup @@ -50,12 +50,12 @@ app.use(<%= mount.path %>, <%- mount.code %>); <% if (view) { -%> // catch 404 and forward to error handler -app.use(function(req, res, next) { +app.use(<%- esm ? '' : 'function' %>(req, res, next)<%- esm ? ' =>' : '' %> { next(createError(404)); }); // error handler -app.use(function(err, req, res, next) { +app.use(<%- esm ? '' : 'function' %>(err, req, res, next)<%- esm ? ' =>' : '' %> { // set locals, only providing error in development res.locals.message = err.message; res.locals.error = req.app.get('env') === 'development' ? err : {}; diff --git a/templates/js/www.ejs b/templates/js/www.ejs index 92ac43d7..b964a3bc 100644 --- a/templates/js/www.ejs +++ b/templates/js/www.ejs @@ -19,14 +19,14 @@ var http = require('http'); * Get port from environment and store in Express. */ -var port = normalizePort(process.env.PORT || '3000'); +<%- esm ? 'const' : 'var' %> port = normalizePort(process.env.PORT || '3000'); app.set('port', port); /** * Create HTTP server. */ -var server = http.createServer(app); +<%- esm ? 'const' : 'var' %> server = http.createServer(app); /** * Listen on provided port, on all network interfaces. @@ -41,7 +41,7 @@ server.on('listening', onListening); */ function normalizePort(val) { - var port = parseInt(val, 10); + <%- esm ? 'const' : 'var' %> port = parseInt(val, 10); if (isNaN(port)) { // named pipe @@ -65,7 +65,7 @@ function onError(error) { throw error; } - var bind = typeof port === 'string' + <%- esm ? 'const' : 'var' %> bind = typeof port === 'string' ? 'Pipe ' + port : 'Port ' + port; @@ -89,8 +89,8 @@ function onError(error) { */ function onListening() { - var addr = server.address(); - var bind = typeof addr === 'string' + <%- esm ? 'const' : 'var' %> addr = server.address(); + <%- esm ? 'const' : 'var' %> bind = typeof addr === 'string' ? 'pipe ' + addr : 'port ' + addr.port; debug('Listening on ' + bind); diff --git a/templates/mjs/routes/index.js b/templates/mjs/routes/index.js index 18543562..cacebed8 100644 --- a/templates/mjs/routes/index.js +++ b/templates/mjs/routes/index.js @@ -1,8 +1,8 @@ import express from 'express'; -var router = express.Router(); +const router = express.Router(); /* GET home page. */ -router.get('/', function(req, res, next) { +router.get('/', (req, res, next) => { res.render('index', { title: 'Express' }); }); diff --git a/templates/mjs/routes/users.js b/templates/mjs/routes/users.js index 0080053d..26448614 100644 --- a/templates/mjs/routes/users.js +++ b/templates/mjs/routes/users.js @@ -1,8 +1,8 @@ import express from 'express'; -var router = express.Router(); +const router = express.Router(); /* GET users listing. */ -router.get('/', function(req, res, next) { +router.get('/', (req, res, next) => { res.send('respond with a resource'); }); diff --git a/test/cmd.js b/test/cmd.js index e6777389..b1e82534 100644 --- a/test/cmd.js +++ b/test/cmd.js @@ -570,14 +570,21 @@ describe('express(1)', function () { npmInstall(ctx.dir, done) }) - it('should export an express app from app.js', function () { - var file = path.resolve(ctx.dir, 'app.js') - import(file) - .then(module => { - const app = module.default - assert.strictEqual(typeof app, 'function') - assert.strictEqual(typeof app.handle, 'function') - }) + it('should export an express app from app.js', function (done) { + // Use eval since otherwise early Nodes choke on import reserved word + // eslint-disable-next-line no-eval + eval( + 'const { pathToFileURL } = require("node:url");' + + 'const file = path.resolve(ctx.dir, "app.js");' + + '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 () { From ee6cc5046ff035039478b7877a458aa8f9d3840c Mon Sep 17 00:00:00 2001 From: drjeffjackson <6119466+drjeffjackson@users.noreply.github.com> Date: Sun, 7 Apr 2024 19:49:01 -0400 Subject: [PATCH 08/12] esm -> es6 Change switch from esm to es6 to better reflect what the switch does. --- README.md | 2 +- bin/express-cli.js | 22 +++++++++++----------- templates/js/app.js.ejs | 10 +++++----- templates/js/www.ejs | 14 +++++++------- test/cmd.js | 18 +++++++++--------- 5 files changed, 33 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index e97fab17..23a30f7f 100644 --- a/README.md +++ b/README.md @@ -54,7 +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 <engine> add stylesheet <engine> support (less|stylus|compass|sass) (defaults to plain css) --git add .gitignore - --esm use ECMAScript modules (requires Node 14.x or higher) + --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 86873850..a287dc2d 100755 --- a/bin/express-cli.js +++ b/bin/express-cli.js @@ -14,7 +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_ESM_VERSION = 14 +var MIN_ES6_VERSION = 14 // parse args var unknown = [] @@ -27,7 +27,7 @@ var args = parseArgs(process.argv.slice(2), { H: 'hogan', v: 'view' }, - boolean: ['ejs', 'esm', '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) { @@ -94,10 +94,10 @@ function createApplication (name, dir, options, done) { var pkg = { name: name, version: '0.0.0', - type: options.esm ? 'module' : 'commonjs', + type: options.es6 ? 'module' : 'commonjs', private: true, scripts: { - start: options.esm ? 'node ./bin/www.js' : 'node ./bin/www' + start: options.es6 ? 'node ./bin/www.js' : 'node ./bin/www' }, dependencies: { debug: '~2.6.9', @@ -113,8 +113,8 @@ function createApplication (name, dir, options, done) { www.locals.name = name // App module type - app.locals.esm = options.esm - www.locals.esm = options.esm + app.locals.es6 = options.es6 + www.locals.es6 = options.es6 // App modules app.locals.localModules = Object.create(null) @@ -167,7 +167,7 @@ function createApplication (name, dir, options, done) { // copy route templates mkdir(dir, 'routes') copyTemplateMulti( - options.esm ? 'mjs/routes' : 'js/routes', + options.es6 ? 'mjs/routes' : 'js/routes', dir + '/routes', '*.js') if (options.view) { @@ -294,7 +294,7 @@ function createApplication (name, dir, options, done) { 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, options.esm ? 'bin/www.js' : 'bin/www'), www.render(), MODE_0755) + write(path.join(dir, options.es6 ? 'bin/www.js' : 'bin/www'), www.render(), MODE_0755) var prompt = launchedFromCmd() ? '>' : '$' @@ -441,9 +441,9 @@ function main (options, done) { usage() error('option `-v, --view <engine>\' argument missing') done(1) - } else if (options.esm && process.versions.node.split('.')[0] < MIN_ESM_VERSION) { + } else if (options.es6 && process.versions.node.split('.')[0] < MIN_ES6_VERSION) { usage() - error('option `--esm\' requires Node version ' + MIN_ESM_VERSION + '.x or higher') + error('option `--es6\' requires Node version ' + MIN_ES6_VERSION + '.x or higher') done(1) } else { // Path @@ -533,7 +533,7 @@ function usage () { console.log(' --no-view use static html instead of view engine') console.log(' -c, --css <engine> add stylesheet <engine> support (less|stylus|compass|sass) (defaults to plain css)') console.log(' --git add .gitignore') - console.log(' --esm use ECMAScript modules (requires Node 14.x or higher)') + 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/js/app.js.ejs b/templates/js/app.js.ejs index f5e691d2..1860f8c7 100644 --- a/templates/js/app.js.ejs +++ b/templates/js/app.js.ejs @@ -1,4 +1,4 @@ -<% if (esm) { -%> +<% if (es6) { -%> <% if (view) { -%> import createError from 'http-errors'; <% } -%> @@ -29,7 +29,7 @@ var <%- variable %> = require('<%- localModules[variable] %>'); <% }); -%> <% } -%> -<%- esm ? 'const' : 'var' %> app = express(); +<%- es6 ? 'const' : 'var' %> app = express(); <% if (view) { -%> // view engine setup @@ -50,12 +50,12 @@ app.use(<%= mount.path %>, <%- mount.code %>); <% if (view) { -%> // catch 404 and forward to error handler -app.use(<%- esm ? '' : 'function' %>(req, res, next)<%- esm ? ' =>' : '' %> { +app.use(<%- es6 ? '' : 'function' %>(req, res, next)<%- es6 ? ' =>' : '' %> { next(createError(404)); }); // error handler -app.use(<%- esm ? '' : 'function' %>(err, req, res, next)<%- esm ? ' =>' : '' %> { +app.use(<%- es6 ? '' : 'function' %>(err, req, res, next)<%- es6 ? ' =>' : '' %> { // set locals, only providing error in development res.locals.message = err.message; res.locals.error = req.app.get('env') === 'development' ? err : {}; @@ -66,7 +66,7 @@ app.use(<%- esm ? '' : 'function' %>(err, req, res, next)<%- esm ? ' =>' : '' %> }); <% } -%> -<% if (esm) { -%> +<% if (es6) { -%> export default app; <% } else { -%> module.exports = app; diff --git a/templates/js/www.ejs b/templates/js/www.ejs index b964a3bc..5d569f77 100644 --- a/templates/js/www.ejs +++ b/templates/js/www.ejs @@ -4,7 +4,7 @@ * Module dependencies. */ -<% if (esm) { -%> +<% if (es6) { -%> import app from '../app.js'; import http from 'node:http'; import debugFunction from 'debug'; @@ -19,14 +19,14 @@ var http = require('http'); * Get port from environment and store in Express. */ -<%- esm ? 'const' : 'var' %> port = normalizePort(process.env.PORT || '3000'); +<%- es6 ? 'const' : 'var' %> port = normalizePort(process.env.PORT || '3000'); app.set('port', port); /** * Create HTTP server. */ -<%- esm ? 'const' : 'var' %> server = http.createServer(app); +<%- es6 ? 'const' : 'var' %> server = http.createServer(app); /** * Listen on provided port, on all network interfaces. @@ -41,7 +41,7 @@ server.on('listening', onListening); */ function normalizePort(val) { - <%- esm ? 'const' : 'var' %> port = parseInt(val, 10); + <%- es6 ? 'const' : 'var' %> port = parseInt(val, 10); if (isNaN(port)) { // named pipe @@ -65,7 +65,7 @@ function onError(error) { throw error; } - <%- esm ? 'const' : 'var' %> bind = typeof port === 'string' + <%- es6 ? 'const' : 'var' %> bind = typeof port === 'string' ? 'Pipe ' + port : 'Port ' + port; @@ -89,8 +89,8 @@ function onError(error) { */ function onListening() { - <%- esm ? 'const' : 'var' %> addr = server.address(); - <%- esm ? 'const' : 'var' %> bind = typeof addr === 'string' + <%- es6 ? 'const' : 'var' %> addr = server.address(); + <%- es6 ? 'const' : 'var' %> bind = typeof addr === 'string' ? 'pipe ' + addr : 'port ' + addr.port; debug('Listening on ' + bind); diff --git a/test/cmd.js b/test/cmd.js index b1e82534..d1e09a03 100644 --- a/test/cmd.js +++ b/test/cmd.js @@ -17,7 +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_ESM_VERSION = 14 +var MIN_ES6_VERSION = 14 describe('express(1)', function () { after(function (done) { @@ -486,12 +486,12 @@ describe('express(1)', function () { }) }) - describe('--esm', function () { + describe('--es6', function () { var ctx = setupTestEnvironment(this.fullTitle()) - if (process.versions.node.split('.')[0] < MIN_ESM_VERSION) { + if (process.versions.node.split('.')[0] < MIN_ES6_VERSION) { it('should exit with code 1', function (done) { - runRaw(ctx.dir, ['--esm'], function (err, code, stdout, stderr) { + runRaw(ctx.dir, ['--es6'], function (err, code, stdout, stderr) { if (err) return done(err) assert.strictEqual(code, 1) done() @@ -499,19 +499,19 @@ describe('express(1)', function () { }) it('should print usage and error message', function (done) { - runRaw(ctx.dir, ['--esm'], function (err, code, stdout, stderr) { + 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 `--esm\' requires Node version ' + MIN_ESM_VERSION) + 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, ['--esm'], function (err, stdout, warnings) { + run(ctx.dir, ['--es6'], function (err, stdout, warnings) { if (err) return done(err) ctx.files = utils.parseCreatedFiles(stdout, ctx.dir) ctx.stdout = stdout @@ -528,7 +528,7 @@ describe('express(1)', function () { }) it('should provide debug instructions', function () { - assert.ok(/DEBUG=express-1---esm:\* (?:& )?npm start/.test(ctx.stdout)) + assert.ok(/DEBUG=express-1---es6:\* (?:& )?npm start/.test(ctx.stdout)) }) it('should have basic files', function () { @@ -547,7 +547,7 @@ describe('express(1)', function () { var file = path.resolve(ctx.dir, 'package.json') var contents = fs.readFileSync(file, 'utf8') assert.strictEqual(contents, '{\n' + - ' "name": "express-1---esm",\n' + + ' "name": "express-1---es6",\n' + ' "version": "0.0.0",\n' + ' "type": "module",\n' + ' "private": true,\n' + From 9c3a1fb14b1fac6ef83e4106d7376875d39c1af4 Mon Sep 17 00:00:00 2001 From: drjeffjackson <6119466+drjeffjackson@users.noreply.github.com> Date: Mon, 8 Apr 2024 01:39:11 -0400 Subject: [PATCH 09/12] parallel templates, .mjs, fix "should export" test Use parallel template files for non-es6 and es6 versions of app and www. In es6 mode, use .mjs extension on all generated JavaScript files. Remove node: in the es6 "should export" test's require of url. --- bin/express-cli.js | 16 ++-- templates/js/app.js.ejs | 28 +----- templates/js/www.ejs | 19 ++-- templates/mjs/app.js.ejs | 54 ++++++++++++ templates/mjs/routes/{index.js => index.mjs} | 0 templates/mjs/routes/{users.js => users.mjs} | 0 templates/mjs/www.ejs | 91 ++++++++++++++++++++ test/cmd.js | 12 +-- 8 files changed, 166 insertions(+), 54 deletions(-) create mode 100644 templates/mjs/app.js.ejs rename templates/mjs/routes/{index.js => index.mjs} (100%) rename templates/mjs/routes/{users.js => users.mjs} (100%) create mode 100644 templates/mjs/www.ejs diff --git a/bin/express-cli.js b/bin/express-cli.js index a287dc2d..8f40b1c4 100755 --- a/bin/express-cli.js +++ b/bin/express-cli.js @@ -97,7 +97,7 @@ function createApplication (name, dir, options, done) { type: options.es6 ? 'module' : 'commonjs', private: true, scripts: { - start: options.es6 ? 'node ./bin/www.js' : 'node ./bin/www' + start: options.es6 ? 'node ./bin/www.mjs' : 'node ./bin/www' }, dependencies: { debug: '~2.6.9', @@ -106,16 +106,12 @@ function createApplication (name, dir, options, done) { } // 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 - // App module type - app.locals.es6 = options.es6 - www.locals.es6 = options.es6 - // App modules app.locals.localModules = Object.create(null) app.locals.modules = Object.create(null) @@ -168,7 +164,7 @@ function createApplication (name, dir, options, done) { mkdir(dir, 'routes') copyTemplateMulti( options.es6 ? 'mjs/routes' : 'js/routes', - dir + '/routes', '*.js') + dir + '/routes', options.es6 ? '*.mjs' : '*.js') if (options.view) { // Copy view templates @@ -291,10 +287,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, options.es6 ? 'bin/www.js' : 'bin/www'), www.render(), MODE_0755) + write(path.join(dir, options.es6 ? 'bin/www.mjs' : 'bin/www'), www.render(), MODE_0755) var prompt = launchedFromCmd() ? '>' : '$' diff --git a/templates/js/app.js.ejs b/templates/js/app.js.ejs index 1860f8c7..2ef75aec 100644 --- a/templates/js/app.js.ejs +++ b/templates/js/app.js.ejs @@ -1,20 +1,3 @@ -<% if (es6) { -%> -<% if (view) { -%> -import createError from 'http-errors'; -<% } -%> -import express from 'express'; -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; -<% Object.keys(modules).sort().forEach(function (variable) { -%> -import <%- variable %> from '<%- modules[variable] %>'; -<% }); -%> - -<% Object.keys(localModules).sort().forEach(function (variable) { -%> -import <%- variable %> from '<%- localModules[variable] %>.js'; -<% }); -%> - -const __dirname = path.dirname(fileURLToPath(import.meta.url)); -<% } else { -%> <% if (view) { -%> var createError = require('http-errors'); <% } -%> @@ -27,9 +10,8 @@ var <%- variable %> = require('<%- modules[variable] %>'); <% Object.keys(localModules).sort().forEach(function (variable) { -%> var <%- variable %> = require('<%- localModules[variable] %>'); <% }); -%> -<% } -%> -<%- es6 ? 'const' : 'var' %> app = express(); +var app = express(); <% if (view) { -%> // view engine setup @@ -50,12 +32,12 @@ app.use(<%= mount.path %>, <%- mount.code %>); <% if (view) { -%> // catch 404 and forward to error handler -app.use(<%- es6 ? '' : 'function' %>(req, res, next)<%- es6 ? ' =>' : '' %> { +app.use(function(req, res, next) { next(createError(404)); }); // error handler -app.use(<%- es6 ? '' : 'function' %>(err, req, res, next)<%- es6 ? ' =>' : '' %> { +app.use(function(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 : {}; @@ -66,8 +48,4 @@ app.use(<%- es6 ? '' : 'function' %>(err, req, res, next)<%- es6 ? ' =>' : '' %> }); <% } -%> -<% if (es6) { -%> -export default app; -<% } else { -%> module.exports = app; -<% } -%> diff --git a/templates/js/www.ejs b/templates/js/www.ejs index 5d569f77..690fc488 100644 --- a/templates/js/www.ejs +++ b/templates/js/www.ejs @@ -4,29 +4,22 @@ * Module dependencies. */ -<% if (es6) { -%> -import app from '../app.js'; -import http from 'node:http'; -import debugFunction from 'debug'; -const debug = debugFunction('<%- name %>:server'); -<% } else { -%> var app = require('../app'); var debug = require('debug')('<%- name %>:server'); var http = require('http'); -<% } -%> /** * Get port from environment and store in Express. */ -<%- es6 ? 'const' : 'var' %> port = normalizePort(process.env.PORT || '3000'); +var port = normalizePort(process.env.PORT || '3000'); app.set('port', port); /** * Create HTTP server. */ -<%- es6 ? 'const' : 'var' %> server = http.createServer(app); +var server = http.createServer(app); /** * Listen on provided port, on all network interfaces. @@ -41,7 +34,7 @@ server.on('listening', onListening); */ function normalizePort(val) { - <%- es6 ? 'const' : 'var' %> port = parseInt(val, 10); + var port = parseInt(val, 10); if (isNaN(port)) { // named pipe @@ -65,7 +58,7 @@ function onError(error) { throw error; } - <%- es6 ? 'const' : 'var' %> bind = typeof port === 'string' + var bind = typeof port === 'string' ? 'Pipe ' + port : 'Port ' + port; @@ -89,8 +82,8 @@ function onError(error) { */ function onListening() { - <%- es6 ? 'const' : 'var' %> addr = server.address(); - <%- es6 ? 'const' : 'var' %> bind = typeof addr === 'string' + var addr = server.address(); + var bind = typeof addr === 'string' ? 'pipe ' + addr : 'port ' + addr.port; debug('Listening on ' + bind); diff --git a/templates/mjs/app.js.ejs b/templates/mjs/app.js.ejs new file mode 100644 index 00000000..e3ee123f --- /dev/null +++ b/templates/mjs/app.js.ejs @@ -0,0 +1,54 @@ +<% if (view) { -%> +import createError from 'http-errors'; +<% } -%> +import express from 'express'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +<% 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.js b/templates/mjs/routes/index.mjs similarity index 100% rename from templates/mjs/routes/index.js rename to templates/mjs/routes/index.mjs diff --git a/templates/mjs/routes/users.js b/templates/mjs/routes/users.mjs similarity index 100% rename from templates/mjs/routes/users.js rename to templates/mjs/routes/users.mjs diff --git a/templates/mjs/www.ejs b/templates/mjs/www.ejs new file mode 100644 index 00000000..a6b562d0 --- /dev/null +++ b/templates/mjs/www.ejs @@ -0,0 +1,91 @@ +#!/usr/bin/env node + +/** + * Module dependencies. + */ + +import app from '../app.mjs'; +import http from 'node:http'; +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 d1e09a03..11c7cb16 100644 --- a/test/cmd.js +++ b/test/cmd.js @@ -532,8 +532,8 @@ describe('express(1)', function () { }) it('should have basic files', function () { - assert.notStrictEqual(ctx.files.indexOf('bin/www.js'), -1) - assert.notStrictEqual(ctx.files.indexOf('app.js'), -1) + 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) }) @@ -552,7 +552,7 @@ describe('express(1)', function () { ' "type": "module",\n' + ' "private": true,\n' + ' "scripts": {\n' + - ' "start": "node ./bin/www.js"\n' + + ' "start": "node ./bin/www.mjs"\n' + ' },\n' + ' "dependencies": {\n' + ' "cookie-parser": "~1.4.5",\n' + @@ -570,12 +570,12 @@ describe('express(1)', function () { npmInstall(ctx.dir, done) }) - it('should export an express app from app.js', function (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("node:url");' + - 'const file = path.resolve(ctx.dir, "app.js");' + + 'const { pathToFileURL } = require("url");' + + 'const file = path.resolve(ctx.dir, "app.mjs");' + 'import(pathToFileURL(file).href)' + '.then(moduleNamespaceObject => {' + 'const app = moduleNamespaceObject.default;' + From ed2910be51babc9d1999aebe31da05332e1f42a9 Mon Sep 17 00:00:00 2001 From: drjeffjackson <6119466+drjeffjackson@users.noreply.github.com> Date: Tue, 9 Apr 2024 13:07:11 -0400 Subject: [PATCH 10/12] Remove type:commonjs. The type field is now added to package.json only when the generator is run with the --es6 switch. --- bin/express-cli.js | 4 +++- test/cmd.js | 5 ++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/bin/express-cli.js b/bin/express-cli.js index 8f40b1c4..7dd4162d 100755 --- a/bin/express-cli.js +++ b/bin/express-cli.js @@ -94,7 +94,6 @@ function createApplication (name, dir, options, done) { var pkg = { name: name, version: '0.0.0', - type: options.es6 ? 'module' : 'commonjs', private: true, scripts: { start: options.es6 ? 'node ./bin/www.mjs' : 'node ./bin/www' @@ -104,6 +103,9 @@ function createApplication (name, dir, options, done) { express: '~4.17.1' } } + if (options.es6) { + pkg.type = 'module' + } // JavaScript var app = loadTemplate(options.es6 ? 'mjs/app.js' : 'js/app.js') diff --git a/test/cmd.js b/test/cmd.js index 11c7cb16..6a26fb22 100644 --- a/test/cmd.js +++ b/test/cmd.js @@ -67,7 +67,6 @@ describe('express(1)', function () { assert.strictEqual(contents, '{\n' + ' "name": "express-1-no-args",\n' + ' "version": "0.0.0",\n' + - ' "type": "commonjs",\n' + ' "private": true,\n' + ' "scripts": {\n' + ' "start": "node ./bin/www"\n' + @@ -549,7 +548,6 @@ describe('express(1)', function () { assert.strictEqual(contents, '{\n' + ' "name": "express-1---es6",\n' + ' "version": "0.0.0",\n' + - ' "type": "module",\n' + ' "private": true,\n' + ' "scripts": {\n' + ' "start": "node ./bin/www.mjs"\n' + @@ -561,7 +559,8 @@ describe('express(1)', function () { ' "http-errors": "~1.7.2",\n' + ' "jade": "~1.11.0",\n' + ' "morgan": "~1.10.0"\n' + - ' }\n' + + ' },\n' + + ' "type": "module"\n' + '}\n') }) From 462d202a12a810fc98a3d8b7393a86384b41e045 Mon Sep 17 00:00:00 2001 From: drjeffjackson <6119466+drjeffjackson@users.noreply.github.com> Date: Wed, 10 Apr 2024 12:54:37 -0400 Subject: [PATCH 11/12] Remove node: from generated files The node: URL scheme was not available in v14 until v14.13.1. So, removing this scheme to stay compatible with all releases of v14. --- templates/mjs/app.js.ejs | 4 ++-- templates/mjs/www.ejs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/templates/mjs/app.js.ejs b/templates/mjs/app.js.ejs index e3ee123f..127338c8 100644 --- a/templates/mjs/app.js.ejs +++ b/templates/mjs/app.js.ejs @@ -2,8 +2,8 @@ import createError from 'http-errors'; <% } -%> import express from 'express'; -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; +import path from 'path'; +import { fileURLToPath } from 'url'; <% Object.keys(modules).sort().forEach(function (variable) { -%> import <%- variable %> from '<%- modules[variable] %>'; <% }); -%> diff --git a/templates/mjs/www.ejs b/templates/mjs/www.ejs index a6b562d0..47d6ccd6 100644 --- a/templates/mjs/www.ejs +++ b/templates/mjs/www.ejs @@ -5,7 +5,7 @@ */ import app from '../app.mjs'; -import http from 'node:http'; +import http from 'http'; import debugFunction from 'debug'; const debug = debugFunction('<%- name %>:server'); From a225474dffcb18bc6b7777db133025eaf3d3ac6d Mon Sep 17 00:00:00 2001 From: drjeffjackson <6119466+drjeffjackson@users.noreply.github.com> Date: Tue, 20 Aug 2024 11:32:40 -0400 Subject: [PATCH 12/12] Group Node imports at top of files. Per recommendation by aagamezl. --- templates/mjs/app.js.ejs | 3 ++- templates/mjs/www.ejs | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/templates/mjs/app.js.ejs b/templates/mjs/app.js.ejs index 127338c8..ae5e7341 100644 --- a/templates/mjs/app.js.ejs +++ b/templates/mjs/app.js.ejs @@ -1,9 +1,10 @@ <% if (view) { -%> import createError from 'http-errors'; <% } -%> -import express from 'express'; import path from 'path'; import { fileURLToPath } from 'url'; + +import express from 'express'; <% Object.keys(modules).sort().forEach(function (variable) { -%> import <%- variable %> from '<%- modules[variable] %>'; <% }); -%> diff --git a/templates/mjs/www.ejs b/templates/mjs/www.ejs index 47d6ccd6..bf48377a 100644 --- a/templates/mjs/www.ejs +++ b/templates/mjs/www.ejs @@ -4,8 +4,9 @@ * Module dependencies. */ -import app from '../app.mjs'; import http from 'http'; + +import app from '../app.mjs'; import debugFunction from 'debug'; const debug = debugFunction('<%- name %>:server');