diff --git a/README.md b/README.md index ca18976e..11a5e33a 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 + --typescript add TypeScript support -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 d0c80b7f..b8dae952 100755 --- a/bin/express-cli.js +++ b/bin/express-cli.js @@ -57,6 +57,7 @@ program .option('-c, --css ', 'add stylesheet support (less|stylus|compass|sass) (defaults to plain css)') .option(' --git', 'add .gitignore') .option('-f, --force', 'force on non-empty directory') + .option(' --typescript', 'add TypeScript support') .parse(process.argv) if (!exit.exited) { @@ -150,9 +151,30 @@ function createApplication (name, dir) { } } + if (program.typescript) { + pkg.scripts['build'] = 'tsc' + + pkg.devDependencies = {} + pkg.devDependencies['typescript'] = '~3.7.5' + pkg.devDependencies['@types/node'] = '~13.7.0' + pkg.devDependencies['@types/debug'] = '~4.1.5' + pkg.devDependencies['@types/express'] = '~4.16.1' + + pkg.dependencies['tslib'] = '~1.10.0' + } + // JavaScript - var app = loadTemplate('js/app.js') - var www = loadTemplate('js/www') + var appTemplatePath + var wwwTemplatePath + if (program.typescript) { + appTemplatePath = 'ts/app.ts' + wwwTemplatePath = 'ts/www.ts' + } else { + appTemplatePath = 'js/app.js' + wwwTemplatePath = 'js/www' + } + var app = loadTemplate(appTemplatePath) + var www = loadTemplate(wwwTemplatePath) // App name www.locals.name = name @@ -167,6 +189,9 @@ function createApplication (name, dir) { app.locals.modules.logger = 'morgan' app.locals.uses.push("logger('dev')") pkg.dependencies.morgan = '~1.9.1' + if (program.typescript) { + pkg.devDependencies['@types/morgan'] = '~1.7.37' + } // Body parsers app.locals.uses.push('express.json()') @@ -176,6 +201,9 @@ function createApplication (name, dir) { app.locals.modules.cookieParser = 'cookie-parser' app.locals.uses.push('cookieParser()') pkg.dependencies['cookie-parser'] = '~1.4.4' + if (program.typescript) { + pkg.devDependencies['@types/cookie-parser'] = '~1.4.2' + } if (dir !== '.') { mkdir(dir, '.') @@ -206,13 +234,25 @@ function createApplication (name, dir) { } // copy route templates + var routeScriptsDir + var routeScriptsNameGlob + if (program.typescript) { + routeScriptsDir = 'ts/routes' + routeScriptsNameGlob = '*.ts' + } else { + routeScriptsDir = 'js/routes' + routeScriptsNameGlob = '*.js' + } mkdir(dir, 'routes') - copyTemplateMulti('js/routes', dir + '/routes', '*.js') + copyTemplateMulti(routeScriptsDir, dir + '/routes', routeScriptsNameGlob) if (program.view) { // Copy view templates mkdir(dir, 'views') pkg.dependencies['http-errors'] = '~1.6.3' + if (program.typescript) { + pkg.devDependencies['@types/http-errors'] = '~1.6.3' + } switch (program.view) { case 'dust': copyTemplateMulti('views', dir + '/views', '*.dust') @@ -250,21 +290,33 @@ function createApplication (name, dir) { app.locals.modules.compass = 'node-compass' app.locals.uses.push("compass({ mode: 'expanded' })") pkg.dependencies['node-compass'] = '0.2.3' + if (program.typescript) { + copyTemplate('declarations/node-compass.d.ts', path.join(dir, 'node-compass.d.ts')) + } 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' + if (program.typescript) { + pkg.devDependencies['@types/less-middleware'] = '~2.0.31' + } 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' + if (program.typescript) { + pkg.devDependencies['@types/node-sass-middleware'] = '0.0.31' + } break case 'stylus': app.locals.modules.stylus = 'stylus' app.locals.uses.push("stylus.middleware(path.join(__dirname, 'public'))") pkg.dependencies['stylus'] = '0.54.5' + if (program.typescript) { + pkg.devDependencies['@types/stylus'] = '0.48.32' + } break } @@ -325,15 +377,30 @@ function createApplication (name, dir) { if (program.git) { copyTemplate('js/gitignore', path.join(dir, '.gitignore')) } + if (program.typescript) { + copyTemplate('ts/tsconfig.json', path.join(dir, 'tsconfig.json')) + } // sort dependencies like npm(1) pkg.dependencies = sortedObject(pkg.dependencies) + if (pkg.devDependencies) { + pkg.devDependencies = sortedObject(pkg.devDependencies) + } // write files - write(path.join(dir, 'app.js'), app.render()) + var appScriptName + var wwwScriptName + if (program.typescript) { + appScriptName = 'app.ts' + wwwScriptName = 'bin/www.ts' + } else { + appScriptName = 'app.js' + wwwScriptName = 'bin/www' + } + write(path.join(dir, appScriptName), 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, wwwScriptName), www.render(), MODE_0755) var prompt = launchedFromCmd() ? '>' : '$' @@ -346,6 +413,11 @@ function createApplication (name, dir) { console.log() console.log(' install dependencies:') console.log(' %s npm install', prompt) + if (program.typescript) { + console.log() + console.log(' compile code:') + console.log(' %s npm run build', prompt) + } console.log() console.log(' run the app:') diff --git a/templates/ts/app.ts.ejs b/templates/ts/app.ts.ejs new file mode 100644 index 00000000..1c21171d --- /dev/null +++ b/templates/ts/app.ts.ejs @@ -0,0 +1,51 @@ +<% if (view) { -%> +import createError from 'http-errors'; +<% } -%> +import express from 'express'; +import * as path from 'path'; +<% Object.keys(modules).sort().forEach(function (variable) { -%> +import <%- variable %> from '<%- modules[variable] %>'; +<% }); -%> + +<% Object.keys(localModules).sort().forEach(function (variable) { -%> +import <%- variable %> from '<%- localModules[variable] %>'; +<% }); -%> + +let 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'); +}) as express.ErrorRequestHandler); + +<% } -%> +export default app; diff --git a/templates/ts/declarations/node-compass.d.ts b/templates/ts/declarations/node-compass.d.ts new file mode 100644 index 00000000..36ae2438 --- /dev/null +++ b/templates/ts/declarations/node-compass.d.ts @@ -0,0 +1,4 @@ +declare module 'node-compass' { + const compass: any; + export default compass; +} diff --git a/templates/ts/routes/index.ts b/templates/ts/routes/index.ts new file mode 100644 index 00000000..de994eaf --- /dev/null +++ b/templates/ts/routes/index.ts @@ -0,0 +1,9 @@ +import * as 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/ts/routes/users.ts b/templates/ts/routes/users.ts new file mode 100644 index 00000000..f4166100 --- /dev/null +++ b/templates/ts/routes/users.ts @@ -0,0 +1,9 @@ +import * as 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/ts/tsconfig.json b/templates/ts/tsconfig.json new file mode 100644 index 00000000..f6a6ea84 --- /dev/null +++ b/templates/ts/tsconfig.json @@ -0,0 +1,66 @@ +{ + "compilerOptions": { + /* Basic Options */ + // "incremental": true, /* Enable incremental compilation */ + "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ + "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ + // "lib": [], /* Specify library files to be included in the compilation. */ + // "allowJs": true, /* Allow javascript files to be compiled. */ + // "checkJs": true, /* Report errors in .js files. */ + // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ + // "declaration": true, /* Generates corresponding '.d.ts' file. */ + // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ + // "sourceMap": true, /* Generates corresponding '.map' file. */ + // "outFile": "./", /* Concatenate and emit output to single file. */ + // "outDir": "./", /* Redirect output structure to the directory. */ + // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ + // "composite": true, /* Enable project compilation */ + // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ + // "removeComments": true, /* Do not emit comments to output. */ + // "noEmit": true, /* Do not emit outputs. */ + "importHelpers": true, /* Import emit helpers from 'tslib'. */ + // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ + // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ + + /* Strict Type-Checking Options */ + "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* Enable strict null checks. */ + // "strictFunctionTypes": true, /* Enable strict checking of function types. */ + // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ + // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ + // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ + // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ + + /* Additional Checks */ + // "noUnusedLocals": true, /* Report errors on unused locals. */ + // "noUnusedParameters": true, /* Report errors on unused parameters. */ + // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + + /* Module Resolution Options */ + "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ + // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ + // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ + // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ + // "typeRoots": [], /* List of folders to include type definitions from. */ + // "types": [], /* Type declaration files to be included in compilation. */ + // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ + "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ + // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + + /* Source Map Options */ + // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ + // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ + + /* Experimental Options */ + // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ + // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + + /* Advanced Options */ + "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ + } +} diff --git a/templates/ts/www.ts.ejs b/templates/ts/www.ts.ejs new file mode 100644 index 00000000..97900578 --- /dev/null +++ b/templates/ts/www.ts.ejs @@ -0,0 +1,92 @@ +#!/usr/bin/env node + +/** + * Module dependencies. + */ + +import app from '../app'; +import debug from 'debug'; +import * as http from 'http'; + +debug('<%- name %>:server'); + +/** + * Get port from environment and store in Express. + */ + +let port = normalizePort(process.env.PORT || '3000'); +app.set('port', port); + +/** + * Create HTTP server. + */ + +let 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: string): string | number | boolean { + let 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: NodeJS.ErrnoException): void { + if (error.syscall !== 'listen') { + throw error; + } + + let 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(): void { + let addr = server.address(); + let bind = typeof addr === 'string' + ? 'pipe ' + addr + : 'port ' + addr!.port; + debug('Listening on ' + bind); +}