From 3ef2d80729230308204c8e91d966863d6f285d9e Mon Sep 17 00:00:00 2001 From: Keshav Malik Date: Wed, 24 Jun 2026 19:13:07 +0530 Subject: [PATCH 1/5] Ignore local agent guide --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index ff9fabf..eae8703 100644 --- a/.gitignore +++ b/.gitignore @@ -105,4 +105,5 @@ dist # Prettier File +AGENTS.md package-lock.json From bb2820a4bd3d09c7cb022fa4faf0e7e51c903203 Mon Sep 17 00:00:00 2001 From: Keshav Malik Date: Wed, 24 Jun 2026 19:13:13 +0530 Subject: [PATCH 2/5] Add Hono scaffold generation --- bin/servergen.js | 15 ++- lib/app_generator.js | 32 +++-- lib/config.js | 3 +- lib/constants.js | 2 + lib/file_generator.js | 147 ++++++++++++++++----- lib/interactive.js | 130 +++++++++++------- lib/validator.js | 19 +-- templates/hono/.dockerignore | 7 + templates/hono/.env.example | 3 + templates/hono/gitignore | 36 +++++ templates/hono/typescript/Dockerfile | 19 +++ templates/hono/typescript/README.md | 66 +++++++++ templates/hono/typescript/src/index.ts | 82 ++++++++++++ templates/hono/typescript/test/app.test.ts | 27 ++++ templates/hono/typescript/tsconfig.json | 14 ++ 15 files changed, 493 insertions(+), 109 deletions(-) create mode 100644 templates/hono/.dockerignore create mode 100644 templates/hono/.env.example create mode 100644 templates/hono/gitignore create mode 100644 templates/hono/typescript/Dockerfile create mode 100644 templates/hono/typescript/README.md create mode 100644 templates/hono/typescript/src/index.ts create mode 100644 templates/hono/typescript/test/app.test.ts create mode 100644 templates/hono/typescript/tsconfig.json diff --git a/bin/servergen.js b/bin/servergen.js index c424b2d..1ca21cb 100644 --- a/bin/servergen.js +++ b/bin/servergen.js @@ -1,7 +1,7 @@ #!/usr/bin/env node /** - * ServerGen CLI - Generates Node.js/Express application scaffolding. + * ServerGen CLI - Generates Node.js, Express, and Hono application scaffolding. * @module bin/servergen */ @@ -28,15 +28,15 @@ const config = getConfig(__dirname, process.cwd()); program .name('servergen') - .description('Scaffold a Node.js or Express application.') + .description('Scaffold a Node.js, Express, or Hono application.') .version(pkg.version) .argument('[name]', 'name of the app to create (alternative to --name)') .option('-n, --name ', 'name of the app to create') - .option('-f, --framework ', 'framework: express | node', 'express') + .option('-f, --framework ', 'framework: express | node | hono', 'express') .option('-v, --view ', 'view engine (express only): ejs | pug | hbs') .option('--db', 'add Mongoose and a MongoDB config (express only)') - .option('--openapi', 'generate an OpenAPI spec file (express only)') - .option('--typescript', 'generate an Express TypeScript app') + .option('--openapi', 'generate an OpenAPI spec file (express and hono)') + .option('--typescript', 'generate a TypeScript app where supported') .option('-p, --port ', 'port for the generated app (1-65535)', '3000') .option('--skip-install', 'skip the npm install step') .option('--debug', 'enable debug logging') @@ -46,10 +46,11 @@ program Examples: $ servergen my-api create an Express app (default) $ servergen my-api -f node create a Node app + $ servergen my-api -f hono create a Hono app $ servergen my-api -v ejs Express app with the EJS view engine $ servergen my-api --db Express app with Mongoose/MongoDB - $ servergen my-api --openapi Express app with docs/openapi.yaml - $ servergen my-api --typescript Express app with TypeScript + $ servergen my-api --openapi API app with docs/openapi.yaml + $ servergen my-api --typescript TypeScript where supported $ servergen my-api -p 8080 use a custom port $ servergen my-api --skip-install scaffold without running npm install $ servergen --name my-api name via flag (equivalent to positional) diff --git a/lib/app_generator.js b/lib/app_generator.js index 3e281a2..423220d 100644 --- a/lib/app_generator.js +++ b/lib/app_generator.js @@ -15,7 +15,7 @@ class AppGenerator { * Creates an AppGenerator instance. * @param {Object} options - Generation options. * @param {string} options.appName - The application name. - * @param {string} options.framework - The framework type ('node' or 'express'). + * @param {string} options.framework - The framework type ('node', 'express', or 'hono'). * @param {string|null} options.view - The view engine name. * @param {boolean} options.db - Whether to include database configuration. * @param {boolean} options.openapi - Whether to generate an OpenAPI spec. @@ -46,9 +46,8 @@ class AppGenerator { this.logger = dependencies.logger; this.folderDir = path.join(this.config.paths.cwd, this.appName); - this.templatesDir = this.framework === 'node' - ? this.config.paths.templates.node - : this.config.paths.templates.express; + this.templatesDir = this.config.paths.templates[this.framework] + || this.config.paths.templates.express; } /** @@ -100,6 +99,16 @@ class AppGenerator { this.view, this.db ); + } else if (this.framework === 'hono') { + if (typeof this.fileCreator.createHonoApp !== 'function') { + throw new Error('The hono framework is recognized, but Hono generation is not available in this build.'); + } + this.fileCreator.createHonoApp( + this.templatesDir, + this.folderDir, + this.appName, + { typescript: this.typescript } + ); } else { this.fileCreator.createExpressApp( this.templatesDir, @@ -118,7 +127,7 @@ class AppGenerator { configurePort() { if (this.port !== 3000) { this.logger?.debug('Configuring custom port', { port: this.port }); - const indexPath = this.typescript + const indexPath = this.typescript || this.framework === 'hono' ? path.join(this.folderDir, 'src', 'index.ts') : path.join(this.folderDir, 'index.js'); try { @@ -136,11 +145,10 @@ class AppGenerator { } /** - * Sets up view engine if specified. View engines are express-only, so this - * is a no-op for the node framework. + * Sets up view engine if specified. View engines are express-only. */ setupViews() { - if (this.framework === 'node') { + if (this.framework !== 'express') { return; } this.fileCreator.handleViews( @@ -155,7 +163,7 @@ class AppGenerator { * Sets up database configuration if enabled. */ setupDatabase() { - if (this.db) { + if (this.framework === 'express' && this.db) { this.fileCreator.handleConfig( this.folderDir, this.config.paths.templates.express, @@ -168,11 +176,12 @@ class AppGenerator { * Adds gitignore and Docker support files. */ addSupportFiles() { + const typescript = this.typescript || this.framework === 'hono'; const supportOptions = { db: Boolean(this.db), openapi: Boolean(this.openapi), port: this.port, - typescript: this.typescript, + typescript, }; this.fileCreator.addGitIgnore(this.folderDir, this.templatesDir); @@ -182,9 +191,10 @@ class AppGenerator { appName: this.appName, }); this.fileCreator.addEnvExample(this.folderDir, this.templatesDir, supportOptions); - if (this.framework === 'express' && this.openapi) { + if ((this.framework === 'express' || this.framework === 'hono') && this.openapi) { this.fileCreator.addOpenApiSpec(this.folderDir, { appName: this.appName, + framework: this.framework, port: this.port, view: this.view, }); diff --git a/lib/config.js b/lib/config.js index 5345780..f9bf164 100644 --- a/lib/config.js +++ b/lib/config.js @@ -16,13 +16,14 @@ export const getConfig = (baseDir, cwd) => { paths: { templates: { express: path.join(baseDir, '..', 'templates', 'express'), + hono: path.join(baseDir, '..', 'templates', 'hono'), node: path.join(baseDir, '..', 'templates', 'node'), views: path.join(baseDir, '..', 'templates', 'express', 'views'), }, cwd, }, validation: { - frameworks: ['node', 'express'], + frameworks: ['node', 'express', 'hono'], views: ['ejs', 'pug', 'hbs'], }, defaults: { diff --git a/lib/constants.js b/lib/constants.js index 67ea8b6..1b067e1 100644 --- a/lib/constants.js +++ b/lib/constants.js @@ -39,6 +39,8 @@ export const DEPENDENCY_VERSIONS = { nodemon: '^3.1.14', cors: '^2.8.6', express: '^5.2.1', + hono: '^4.12.27', + '@hono/node-server': '^2.0.6', mongoose: '^9.7.0', dotenv: '^17.4.2', supertest: '^7.2.2', diff --git a/lib/file_generator.js b/lib/file_generator.js index c6679c1..a891a66 100644 --- a/lib/file_generator.js +++ b/lib/file_generator.js @@ -56,7 +56,7 @@ const getGeneratedPaths = (folderDir, typescript = false) => { * @param {string} appName - The application name. * @param {string|null} view - The view engine name (ejs, pug, hbs). * @param {boolean} config - Whether to include mongoose configuration. - * @param {string} framework - The framework type ('node' or 'express'). + * @param {string} framework - The framework type ('node', 'express', or 'hono'). */ const generatePackage = (folderDir, appName, view, config, framework, options = {}) => { const typescript = Boolean(options.typescript); @@ -98,6 +98,12 @@ const generatePackage = (folderDir, appName, view, config, framework, options = pkg.devDependencies['@types/cors'] = DEPENDENCY_VERSIONS['@types/cors']; pkg.devDependencies['@types/supertest'] = DEPENDENCY_VERSIONS['@types/supertest']; } + } else if (framework === 'hono') { + pkg.type = 'module'; + pkg.dependencies['@hono/node-server'] = DEPENDENCY_VERSIONS['@hono/node-server']; + pkg.dependencies.dotenv = DEPENDENCY_VERSIONS.dotenv; + pkg.dependencies.hono = DEPENDENCY_VERSIONS.hono; + pkg.scripts.test = 'node --import tsx --test test/**/*.test.ts'; } if (view && VIEW_ENGINES[view]) { @@ -149,6 +155,28 @@ const createExpressApp = (templatesDir, folderDir, appName, view, config, option console.log(typescript ? 'Generating TypeScript Express application..' : 'Generating Express application..'); }; +/** + * Creates a TypeScript Hono API application. + * @param {string} templatesDir - Path to Hono templates. + * @param {string} folderDir - The application directory path. + * @param {string} appName - The application name. + */ +const createHonoApp = (templatesDir, folderDir, appName) => { + const appTemplatesDir = path.join(templatesDir, 'typescript'); + const generatedPaths = getGeneratedPaths(folderDir, true); + + fs.ensureDirSync(generatedPaths.sourceRoot); + fs_helper.buildFilewithContents( + path.join(appTemplatesDir, 'src', 'index.ts'), + generatedPaths.sourceRoot, + 'index.ts' + ); + generatePackage(folderDir, appName, null, false, 'hono', { typescript: true }); + generateTypeScriptConfig(folderDir, appTemplatesDir, { typescript: true }); + generateAppTest(folderDir, appTemplatesDir, { typescript: true }); + console.log('Generating TypeScript Hono application..'); +}; + /** * Copies the generated app's integration test into a test/ directory. * @param {string} folderDir - The application directory path. @@ -406,6 +434,8 @@ const yamlSingleQuoted = (value) => `'${String(value).replace(/'/g, "''")}'`; const addOpenApiSpec = (folderDir, options = {}) => { const appName = options.appName || 'servergen-app'; const port = options.port || 3000; + const framework = options.framework || 'express'; + const frameworkName = framework === 'hono' ? 'Hono' : 'Express'; const hasView = Boolean(options.view); const rootGetResponse = hasView ? ` description: Rendered welcome page @@ -423,28 +453,9 @@ const addOpenApiSpec = (folderDir, options = {}) => { welcome: value: message: Welcome to ServerGen!`; - - const spec = `openapi: 3.0.3 -info: - title: ${yamlSingleQuoted(`${appName} API`)} - version: 1.0.0 - description: ${yamlSingleQuoted(`API specification for the ${appName} Express app generated by ServerGen.`)} -servers: - - url: http://localhost:${port} - description: Local development server -paths: - /: - get: - summary: Root endpoint - operationId: getRoot - responses: - '200': -${rootGetResponse} - '404': - $ref: '#/components/responses/NotFound' - '500': - $ref: '#/components/responses/InternalServerError' - post: + const postRootPath = framework === 'hono' + ? '' + : ` post: summary: Echo a JSON payload with the welcome response operationId: postRoot requestBody: @@ -479,7 +490,78 @@ ${rootGetResponse} $ref: '#/components/responses/NotFound' '500': $ref: '#/components/responses/InternalServerError' - /health: +`; + const honoInfoPaths = framework === 'hono' + ? ` /about: + get: + summary: About endpoint + operationId: getAbout + responses: + '200': + description: About response + content: + application/json: + schema: + $ref: '#/components/schemas/MessageResponse' + examples: + about: + value: + message: About this ServerGen app + /contact: + get: + summary: Contact endpoint + operationId: getContact + responses: + '200': + description: Contact response + content: + application/json: + schema: + $ref: '#/components/schemas/MessageResponse' + examples: + contact: + value: + message: Contact this ServerGen app +` + : ''; + const notFoundContent = framework === 'hono' + ? ` content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + notFound: + value: + error: Not Found` + : ` content: + text/html: + schema: + type: string + examples: + notFound: + value: Cannot GET /missing`; + + const spec = `openapi: 3.0.3 +info: + title: ${yamlSingleQuoted(`${appName} API`)} + version: 1.0.0 + description: ${yamlSingleQuoted(`API specification for the ${appName} ${frameworkName} app generated by ServerGen.`)} +servers: + - url: http://localhost:${port} + description: Local development server +paths: + /: + get: + summary: Root endpoint + operationId: getRoot + responses: + '200': +${rootGetResponse} + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/InternalServerError' +${postRootPath}${honoInfoPaths} /health: get: summary: Health check operationId: getHealth @@ -502,13 +584,7 @@ components: responses: NotFound: description: Route not found - content: - text/html: - schema: - type: string - examples: - notFound: - value: Cannot GET /missing +${notFoundContent} InternalServerError: description: Internal server error content: @@ -528,6 +604,14 @@ components: message: type: string example: Welcome to ServerGen! + MessageResponse: + type: object + required: + - message + properties: + message: + type: string + example: About this ServerGen app PostRootResponse: type: object required: @@ -588,6 +672,7 @@ const addEnvExample = (folderDir, templatesDir, options = {}) => { export { createExpressApp, + createHonoApp, createNodeApp, handleViews, handleConfig, diff --git a/lib/interactive.js b/lib/interactive.js index 25741cb..c9c40f2 100644 --- a/lib/interactive.js +++ b/lib/interactive.js @@ -87,65 +87,92 @@ export const promptForInteractiveOptions = async ({ errorMessage: 'Project name is required.', }); - const typescript = await askChoice({ + const framework = await askChoice({ question, output, - prompt: 'Language (TypeScript/JavaScript) [TypeScript]: ', + prompt: 'Framework (express/node/hono) [express]: ', choices: { - ts: true, - typescript: true, - js: false, - javascript: false, + express: 'express', + node: 'node', + hono: 'hono', }, - defaultValue: true, - errorMessage: 'Please choose TypeScript or JavaScript.', + defaultValue: 'express', + errorMessage: 'Please choose express, node, or hono.', }); - const openapi = await askChoice({ - question, - output, - prompt: 'OpenAPI spec? (Y/n): ', - choices: { - y: true, - yes: true, - n: false, - no: false, - }, - defaultValue: true, - errorMessage: 'Please answer yes or no.', - }); + let typescript = false; + if (framework === 'express') { + typescript = await askChoice({ + question, + output, + prompt: 'Language (TypeScript/JavaScript) [TypeScript]: ', + choices: { + ts: true, + typescript: true, + js: false, + javascript: false, + }, + defaultValue: true, + errorMessage: 'Please choose TypeScript or JavaScript.', + }); + } else if (framework === 'hono') { + typescript = true; + } - const db = await askChoice({ - question, - output, - prompt: 'Database (none/mongodb) [none]: ', - choices: { - none: false, - no: false, - n: false, - mongo: true, - mongodb: true, - mongoose: true, - }, - defaultValue: false, - errorMessage: 'Please choose none or mongodb.', - }); + let openapi = false; + if (framework !== 'node') { + openapi = await askChoice({ + question, + output, + prompt: 'OpenAPI spec? (Y/n): ', + choices: { + y: true, + yes: true, + n: false, + no: false, + }, + defaultValue: true, + errorMessage: 'Please answer yes or no.', + }); + } - const view = await askChoice({ - question, - output, - prompt: 'View engine (none/ejs/pug/hbs) [none]: ', - choices: { - none: undefined, - no: undefined, - n: undefined, - ejs: 'ejs', - pug: 'pug', - hbs: 'hbs', - }, - defaultValue: undefined, - errorMessage: 'Please choose none, ejs, pug, or hbs.', - }); + let db = false; + if (framework === 'express') { + db = await askChoice({ + question, + output, + prompt: 'Database (none/mongodb) [none]: ', + choices: { + none: false, + no: false, + n: false, + mongo: true, + mongodb: true, + mongoose: true, + }, + defaultValue: false, + errorMessage: 'Please choose none or mongodb.', + }); + } + + let view; + if (framework === 'express') { + view = await askChoice({ + question, + output, + prompt: 'View engine (none/ejs/pug/hbs) [none]: ', + choices: { + none: undefined, + no: undefined, + n: undefined, + ejs: 'ejs', + pug: 'pug', + hbs: 'hbs', + }, + defaultValue: undefined, + errorMessage: 'Please choose none, ejs, pug, or hbs.', + }); + } const port = await askPort({ question, output, defaultPort }); @@ -165,6 +192,7 @@ export const promptForInteractiveOptions = async ({ return { name, + framework, typescript, openapi, db, diff --git a/lib/validator.js b/lib/validator.js index a7be8e7..aab3fa6 100644 --- a/lib/validator.js +++ b/lib/validator.js @@ -13,29 +13,32 @@ */ export const validateOptions = (options, validationRules) => { const errors = []; + const framework = options.framework; + const hasValidFramework = !framework || validationRules.frameworks.includes(framework); + const isExpress = framework === 'express' || !framework; - if (options.framework && !validationRules.frameworks.includes(options.framework)) { - errors.push(`Invalid framework: ${options.framework}. Valid options: ${validationRules.frameworks.join(', ')}`); + if (framework && !validationRules.frameworks.includes(framework)) { + errors.push(`Invalid framework: ${framework}. Valid options: ${validationRules.frameworks.join(', ')}`); } if (options.view && !validationRules.views.includes(options.view)) { errors.push(`Invalid view engine: ${options.view}. Valid options: ${validationRules.views.join(', ')}`); } - if (options.framework === 'node' && options.view) { + if (hasValidFramework && !isExpress && options.view) { errors.push('View engines are only supported with the express framework. Use --framework express or remove --view.'); } - if (options.framework === 'node' && options.db) { + if (hasValidFramework && !isExpress && options.db) { errors.push('The --db option (Mongoose) is only supported with the express framework. Use --framework express or remove --db.'); } - if (options.framework === 'node' && options.openapi) { - errors.push('The --openapi option is only supported with the express framework. Use --framework express or remove --openapi.'); + if (framework === 'node' && options.openapi) { + errors.push('The --openapi option is only supported with the express and hono frameworks. Use --framework express, --framework hono, or remove --openapi.'); } - if (options.framework === 'node' && options.typescript) { - errors.push('The --typescript option is only supported with the express framework. Use --framework express or remove --typescript.'); + if (framework === 'node' && options.typescript) { + errors.push('The --typescript option is only supported with the express and hono frameworks. Use --framework express, --framework hono, or remove --typescript.'); } if (options.port !== undefined && options.port !== null) { diff --git a/templates/hono/.dockerignore b/templates/hono/.dockerignore new file mode 100644 index 0000000..83e31b5 --- /dev/null +++ b/templates/hono/.dockerignore @@ -0,0 +1,7 @@ +node_modules +dist +npm-debug.log +Dockerfile +.dockerignore +.env +.env.test diff --git a/templates/hono/.env.example b/templates/hono/.env.example new file mode 100644 index 0000000..02b9ad2 --- /dev/null +++ b/templates/hono/.env.example @@ -0,0 +1,3 @@ +# Server Configuration +PORT=3000 +NODE_ENV=development diff --git a/templates/hono/gitignore b/templates/hono/gitignore new file mode 100644 index 0000000..74535a1 --- /dev/null +++ b/templates/hono/gitignore @@ -0,0 +1,36 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Diagnostic reports +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Coverage +coverage +*.lcov +.nyc_output + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript +dist/ +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# dotenv environment variables file +.env +.env.test diff --git a/templates/hono/typescript/Dockerfile b/templates/hono/typescript/Dockerfile new file mode 100644 index 0000000..7f07faf --- /dev/null +++ b/templates/hono/typescript/Dockerfile @@ -0,0 +1,19 @@ +FROM node:20-alpine + +RUN mkdir -p /home/node/app/node_modules && chown -R node:node /home/node/app + +WORKDIR /home/node/app + +COPY package*.json ./ + +USER node + +RUN npm install + +COPY --chown=node:node . . + +RUN npm run build && npm prune --omit=dev + +EXPOSE 3000 + +CMD [ "node", "dist/index.js" ] diff --git a/templates/hono/typescript/README.md b/templates/hono/typescript/README.md new file mode 100644 index 0000000..81e63f8 --- /dev/null +++ b/templates/hono/typescript/README.md @@ -0,0 +1,66 @@ +# Project Name + +A TypeScript Hono API generated with [ServerGen](https://github.com/theinfosecguy/ServerGen). The application entry point is `src/index.ts`. + +## Getting Started + +### Prerequisites + +- Node.js 20 or higher +- npm + +### Installation + +```bash +npm install +``` + +### Running the Application + +```bash +npm run dev +``` + +The development server will start on http://localhost:3000 + +### Production Build + +```bash +npm run build +npm start +``` + +### Test + +```bash +npm test +``` + +### Endpoints + +- `GET /` returns `{ "message": "Welcome to ServerGen!" }` +- `GET /about` returns a short about message +- `GET /contact` returns a short contact message +- `GET /health` returns `{ "status": "ok" }` + +## Project Structure + +``` +. +├── src/ +│ └── index.ts # Hono API entry point +├── tsconfig.json +└── package.json +``` + +## Environment Variables + +Copy `.env.example` to `.env` and update the values: + +```bash +cp .env.example .env +``` + +## License + +MIT diff --git a/templates/hono/typescript/src/index.ts b/templates/hono/typescript/src/index.ts new file mode 100644 index 0000000..f6fd3f2 --- /dev/null +++ b/templates/hono/typescript/src/index.ts @@ -0,0 +1,82 @@ +/** + * Hono API entry point. + * @description TypeScript API server with JSON routes and a health check. + */ + +import { serve } from '@hono/node-server'; +import dotenv from 'dotenv'; +import { Hono } from 'hono'; +import { logger } from 'hono/logger'; +import { pathToFileURL } from 'node:url'; + +dotenv.config({ quiet: true }); + +const app = new Hono(); +const port = Number(process.env.PORT || 3000); + +app.use('*', logger()); + +app.get('/', (c) => { + return c.json({ message: 'Welcome to ServerGen!' }); +}); + +app.get('/about', (c) => { + return c.json({ message: 'About this ServerGen app' }); +}); + +app.get('/contact', (c) => { + return c.json({ message: 'Contact this ServerGen app' }); +}); + +app.get('/health', (c) => { + return c.json({ status: 'ok' }); +}); + +app.notFound((c) => { + return c.json({ error: 'Not Found' }, 404); +}); + +app.onError((err, c) => { + console.error(err); + return c.json({ error: 'Internal Server Error' }, 500); +}); + +export const startServer = () => { + const server = serve( + { + fetch: app.fetch, + hostname: '0.0.0.0', + port, + }, + (info) => { + console.log(`Hono server running at http://0.0.0.0:${info.port}/`); + } + ); + + const shutdown = (signal: string) => { + console.log(`${signal} received, shutting down gracefully`); + server.close(() => { + console.log('Server closed'); + process.exit(0); + }); + }; + + process.on('SIGINT', () => { + shutdown('SIGINT'); + }); + process.on('SIGTERM', () => { + shutdown('SIGTERM'); + }); + + return server; +}; + +const isMain = Boolean( + process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href +); + +if (isMain) { + startServer(); +} + +export default app; diff --git a/templates/hono/typescript/test/app.test.ts b/templates/hono/typescript/test/app.test.ts new file mode 100644 index 0000000..4596d75 --- /dev/null +++ b/templates/hono/typescript/test/app.test.ts @@ -0,0 +1,27 @@ +import assert from 'node:assert'; +import test from 'node:test'; +import app from '../src/index'; + +test('GET /health returns 200 with an ok status', async () => { + const res = await app.request('/health'); + const body = await res.json(); + + assert.strictEqual(res.status, 200); + assert.deepStrictEqual(body, { status: 'ok' }); +}); + +test('GET / returns 200 with the welcome message', async () => { + const res = await app.request('/'); + const body = await res.json(); + + assert.strictEqual(res.status, 200); + assert.deepStrictEqual(body, { message: 'Welcome to ServerGen!' }); +}); + +test('GET /missing returns a JSON 404', async () => { + const res = await app.request('/missing'); + const body = await res.json(); + + assert.strictEqual(res.status, 404); + assert.deepStrictEqual(body, { error: 'Not Found' }); +}); diff --git a/templates/hono/typescript/tsconfig.json b/templates/hono/typescript/tsconfig.json new file mode 100644 index 0000000..f26b5a0 --- /dev/null +++ b/templates/hono/typescript/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "rootDir": "src", + "outDir": "dist", + "strict": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true + }, + "include": ["src/**/*.ts"] +} From e45d03e900ce3d49f4d72c73b40cdeadd9d9ad01 Mon Sep 17 00:00:00 2001 From: Keshav Malik Date: Wed, 24 Jun 2026 19:13:20 +0530 Subject: [PATCH 3/5] Add Hono scaffold test coverage --- tests/integration/integration.test.js | 107 +++++++++++++++++++++++++- tests/integration/packaged.test.js | 27 +++++++ tests/smoke/package.smoke.test.js | 92 +++++++++++++++++++++- tests/unit/app_generator.test.js | 104 ++++++++++++++++++++++++- tests/unit/config.test.js | 8 ++ tests/unit/file_generator.test.js | 78 +++++++++++++++++++ tests/unit/interactive.test.js | 58 +++++++++++++- tests/unit/validator.test.js | 34 +++++++- 8 files changed, 501 insertions(+), 7 deletions(-) diff --git a/tests/integration/integration.test.js b/tests/integration/integration.test.js index 151ad00..ed2b94a 100644 --- a/tests/integration/integration.test.js +++ b/tests/integration/integration.test.js @@ -48,6 +48,7 @@ describe('CLI Integration', () => { expect(output).toContain('Usage:'); expect(output).toContain('-n, --name'); expect(output).toContain('-f, --framework'); + expect(output).toContain('express | node | hono'); expect(output).toContain('--openapi'); expect(output).toContain('--typescript'); }); @@ -57,6 +58,7 @@ describe('CLI Integration', () => { expect(output).toContain('Examples:'); expect(output).toContain('servergen my-api'); expect(output).toContain('-f node'); + expect(output).toContain('-f hono'); expect(output).toContain('--db'); expect(output).toContain('--openapi'); expect(output).toContain('--typescript'); @@ -260,6 +262,92 @@ describe('CLI Integration', () => { }); }); + describe('Hono app generation', () => { + it('generates a TypeScript Hono app with correct structure and scripts', () => { + runCLI('-n honotest -f hono --skip-install'); + + const appDir = path.join(testOutput, 'honotest'); + const pkg = JSON.parse( + fs.readFileSync(path.join(appDir, 'package.json'), 'utf-8') + ); + + expect(fs.existsSync(path.join(appDir, 'src', 'index.ts'))).toBe(true); + expect(fs.existsSync(path.join(appDir, 'test', 'app.test.ts'))).toBe(true); + expect(fs.existsSync(path.join(appDir, 'tsconfig.json'))).toBe(true); + expect(fs.existsSync(path.join(appDir, 'Dockerfile'))).toBe(true); + expect(fs.existsSync(path.join(appDir, '.dockerignore'))).toBe(true); + expect(fs.existsSync(path.join(appDir, '.gitignore'))).toBe(true); + expect(fs.existsSync(path.join(appDir, '.env.example'))).toBe(true); + expect(fs.existsSync(path.join(appDir, 'index.js'))).toBe(false); + + expect(pkg.type).toBe('module'); + expect(pkg.dependencies.hono).toBeDefined(); + expect(pkg.dependencies['@hono/node-server']).toBeDefined(); + expect(pkg.dependencies.dotenv).toBeDefined(); + expect(pkg.devDependencies.typescript).toBeDefined(); + expect(pkg.devDependencies.tsx).toBeDefined(); + expect(pkg.scripts.dev).toBe('tsx watch src/index.ts'); + expect(pkg.scripts.build).toBe('tsc'); + expect(pkg.scripts.start).toBe('node dist/index.js'); + expect(pkg.scripts.test).toBe('node --import tsx --test test/**/*.test.ts'); + }); + + it('generates JSON routes, import-safe startup, and custom support docs', () => { + runCLI('-n honoport -f hono -p 8787 --skip-install'); + + const appDir = path.join(testOutput, 'honoport'); + const index = fs.readFileSync(path.join(appDir, 'src', 'index.ts'), 'utf-8'); + const env = fs.readFileSync(path.join(appDir, '.env.example'), 'utf-8'); + const readme = fs.readFileSync(path.join(appDir, 'README.md'), 'utf-8'); + const dockerfile = fs.readFileSync(path.join(appDir, 'Dockerfile'), 'utf-8'); + const testFile = fs.readFileSync( + path.join(appDir, 'test', 'app.test.ts'), + 'utf-8' + ); + + expect(index).toContain('process.env.PORT || 8787'); + expect(index).toContain("app.get('/about'"); + expect(index).toContain("app.get('/contact'"); + expect(index).toContain("app.get('/health'"); + expect(index).toContain('app.notFound'); + expect(index).toContain('export const startServer'); + expect(index).toContain('pathToFileURL(process.argv[1])'); + expect(env).toContain('PORT=8787'); + expect(readme).toContain('# honoport'); + expect(readme).toContain('http://localhost:8787'); + expect(readme).toContain('npm test'); + expect(dockerfile).toContain('EXPOSE 8787'); + expect(testFile).toContain("app.request('/health')"); + }); + + it('generates an OpenAPI spec for Hono when --openapi is used', () => { + runCLI('-n honoopenapi -f hono --openapi -p 8788 --skip-install'); + + const appDir = path.join(testOutput, 'honoopenapi'); + const specPath = path.join(appDir, 'docs', 'openapi.yaml'); + const readme = fs.readFileSync(path.join(appDir, 'README.md'), 'utf-8'); + const spec = fs.readFileSync(specPath, 'utf-8'); + + expect(fs.existsSync(specPath)).toBe(true); + expect(spec).toContain("title: 'honoopenapi API'"); + expect(spec).toContain('Hono app generated by ServerGen'); + expect(spec).toContain('url: http://localhost:8788'); + expect(spec).toContain('operationId: getRoot'); + expect(spec).toContain('operationId: getAbout'); + expect(spec).toContain('operationId: getContact'); + expect(spec).toContain('operationId: getHealth'); + expect(spec).not.toContain('operationId: postRoot'); + expect(readme).toContain('docs/openapi.yaml'); + }); + + it('accepts --typescript for Hono as a compatibility alias', () => { + runCLI('-n honots -f hono --typescript --skip-install'); + + expect(fs.existsSync(path.join(testOutput, 'honots', 'src', 'index.ts'))).toBe(true); + expect(fs.existsSync(path.join(testOutput, 'honots', 'tsconfig.json'))).toBe(true); + }); + }); + describe('support files', () => { it('includes .gitignore', () => { runCLI('-n gittest -f express --skip-install'); @@ -365,11 +453,19 @@ describe('CLI Integration', () => { it('rejects --openapi with the node framework', () => { expectCLIError( '-n nodeopenapi -f node --openapi --skip-install', - 'only supported with the express framework' + 'only supported with the express and hono frameworks' ); expect(fs.existsSync(path.join(testOutput, 'nodeopenapi'))).toBe(false); }); + it('rejects --view with the hono framework', () => { + expectCLIError( + '-n honoview -f hono -v ejs --skip-install', + 'only supported with the express framework' + ); + expect(fs.existsSync(path.join(testOutput, 'honoview'))).toBe(false); + }); + it('rejects --typescript with the node framework', () => { expect(() => runCLI('-n nodets -f node --typescript --skip-install')).toThrow(); expect(fs.existsSync(path.join(testOutput, 'nodets'))).toBe(false); @@ -480,6 +576,7 @@ describe('CLI Integration', () => { const output = runCLI('debugapp --debug --skip-install'); expect(output).toContain('[DEBUG'); }); + }); describe('invalid options', () => { @@ -491,6 +588,14 @@ describe('CLI Integration', () => { expect(fs.existsSync(path.join(testOutput, 'nodedb'))).toBe(false); }); + it('rejects --db with the hono framework', () => { + expectCLIError( + 'honodb -f hono --db --skip-install', + 'only supported with the express framework' + ); + expect(fs.existsSync(path.join(testOutput, 'honodb'))).toBe(false); + }); + it('rejects an invalid framework', () => { expectCLIError('badfw -f flask --skip-install', 'Invalid framework'); }); diff --git a/tests/integration/packaged.test.js b/tests/integration/packaged.test.js index c4603b2..755c588 100644 --- a/tests/integration/packaged.test.js +++ b/tests/integration/packaged.test.js @@ -95,6 +95,33 @@ describe('packaged CLI (from npm tarball)', () => { expect(spec).toContain('url: http://localhost:8080'); }, 120000); + it('ships Hono templates and OpenAPI docs in the packaged CLI', () => { + const appDir = generateFromTarball( + 'pack-hono', + 'hono', + '--openapi -p 8787' + ); + + expect(fs.existsSync(path.join(appDir, 'src', 'index.ts'))).toBe(true); + expect(fs.existsSync(path.join(appDir, 'test', 'app.test.ts'))).toBe(true); + expect(fs.existsSync(path.join(appDir, 'tsconfig.json'))).toBe(true); + + const pkg = JSON.parse( + fs.readFileSync(path.join(appDir, 'package.json'), 'utf-8') + ); + expect(pkg.dependencies.hono).toBeDefined(); + expect(pkg.dependencies['@hono/node-server']).toBeDefined(); + expect(pkg.scripts.test).toBe('node --import tsx --test test/**/*.test.ts'); + + const spec = fs.readFileSync( + path.join(appDir, 'docs', 'openapi.yaml'), + 'utf-8' + ); + expect(spec).toContain("title: 'pack-hono API'"); + expect(spec).toContain('operationId: getAbout'); + expect(spec).toContain('operationId: getContact'); + }, 120000); + it('ships a real .gitignore in a generated node app', () => { const appDir = generateFromTarball('pack-test-node', 'node'); diff --git a/tests/smoke/package.smoke.test.js b/tests/smoke/package.smoke.test.js index 30abcdf..a2ead3e 100644 --- a/tests/smoke/package.smoke.test.js +++ b/tests/smoke/package.smoke.test.js @@ -17,7 +17,7 @@ const createPackageRoot = path.join(projectRoot, 'packages', 'create-servergen') // assert a live HTTP response. This exercises exactly what a consumer gets from // `npm install servergen`, not the repo working tree. -const PORTS = { express: 5310, node: 5311, typescript: 5312 }; +const PORTS = { express: 5310, node: 5311, typescript: 5312, hono: 5313 }; const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm'; const createBinName = process.platform === 'win32' ? 'create-servergen.cmd' : 'create-servergen'; @@ -356,6 +356,96 @@ describe('published package smoke test', () => { 240000 ); + it( + 'generates, builds, tests, boots a Hono app, serves HTTP, and includes OpenAPI docs', + async () => { + const port = PORTS.hono; + const appDir = generate('smokehono', 'hono', port, ['--openapi']); + + expect(fs.existsSync(path.join(appDir, 'src', 'index.ts'))).toBe(true); + expect(fs.existsSync(path.join(appDir, 'package.json'))).toBe(true); + expect(fs.existsSync(path.join(appDir, 'test', 'app.test.ts'))).toBe(true); + expect(fs.existsSync(path.join(appDir, 'tsconfig.json'))).toBe(true); + expect(fs.readFileSync(path.join(appDir, 'Dockerfile'), 'utf-8')).toContain( + `EXPOSE ${port}` + ); + expect(fs.readFileSync(path.join(appDir, 'README.md'), 'utf-8')).toContain( + `http://localhost:${port}` + ); + + const pkg = fs.readJsonSync(path.join(appDir, 'package.json')); + expect(pkg.dependencies.hono).toBeDefined(); + expect(pkg.dependencies['@hono/node-server']).toBeDefined(); + expect(pkg.devDependencies.typescript).toBeDefined(); + + const spec = fs.readFileSync(path.join(appDir, 'docs', 'openapi.yaml'), 'utf-8'); + expect(spec).toContain("description: 'API specification for the smokehono Hono app generated by ServerGen.'"); + expect(spec).toContain('/about:'); + expect(spec).toContain('/contact:'); + expect(spec).toContain(`http://localhost:${port}`); + + execFileSync(npmCmd, ['install', '--no-audit', '--no-fund'], { + cwd: appDir, + encoding: 'utf-8', + timeout: 180000, + }); + + execFileSync(npmCmd, ['run', 'build'], { + cwd: appDir, + encoding: 'utf-8', + timeout: 120000, + }); + + execFileSync(npmCmd, ['test'], { + cwd: appDir, + encoding: 'utf-8', + timeout: 120000, + }); + + child = spawn('node', ['dist/index.js'], { + cwd: appDir, + env: { ...process.env, PORT: String(port) }, + stdio: 'ignore', + }); + + try { + const root = await waitForHttp(port, '/'); + expect(root.status).toBe(200); + expect(JSON.parse(root.body)).toEqual({ + message: 'Welcome to ServerGen!', + }); + + const about = await httpGet(port, '/about'); + expect(about.status).toBe(200); + expect(JSON.parse(about.body)).toEqual({ + message: 'About this ServerGen app', + }); + + const contact = await httpGet(port, '/contact'); + expect(contact.status).toBe(200); + expect(JSON.parse(contact.body)).toEqual({ + message: 'Contact this ServerGen app', + }); + + const health = await httpGet(port, '/health'); + expect(health.status).toBe(200); + expect(JSON.parse(health.body)).toEqual({ status: 'ok' }); + + const missing = await httpGet(port, '/missing'); + expect(missing.status).toBe(404); + expect(JSON.parse(missing.body)).toEqual({ error: 'Not Found' }); + } finally { + if (child && !child.killed) { + const { code, signal } = await stopProcess(child, 'SIGTERM'); + expect(signal).toBeNull(); + expect(code).toBe(0); + } + child = undefined; + } + }, + 240000 + ); + it( 'generates, builds, boots a Node app and serves JSON routes with 404s', async () => { diff --git a/tests/unit/app_generator.test.js b/tests/unit/app_generator.test.js index af1f37d..f398aa0 100644 --- a/tests/unit/app_generator.test.js +++ b/tests/unit/app_generator.test.js @@ -44,6 +44,7 @@ function makeConfig() { cwd: testDir, templates: { express: path.join(testDir, 'templates', 'express'), + hono: path.join(testDir, 'templates', 'hono'), node: path.join(testDir, 'templates', 'node'), views: path.join(testDir, 'templates', 'views'), }, @@ -121,6 +122,14 @@ describe('AppGenerator', () => { expect(gen.templatesDir).toBe(config.paths.templates.node); }); + it('selects the hono templates dir for hono framework', () => { + const gen = new AppGenerator( + { appName: 'myapp', framework: 'hono', config }, + deps + ); + expect(gen.templatesDir).toBe(config.paths.templates.hono); + }); + it('wires the injected dependencies onto the instance', () => { const gen = new AppGenerator({ appName: 'myapp', config }, deps); expect(gen.fsHelper).toBe(deps.fsHelper); @@ -312,6 +321,33 @@ describe('AppGenerator', () => { expect(deps.fileCreator.createNodeApp).toHaveBeenCalledTimes(1); expect(deps.fileCreator.createExpressApp).not.toHaveBeenCalled(); }); + + it('calls createHonoApp for hono framework when available', () => { + deps.fileCreator.createHonoApp = vi.fn(); + const gen = new AppGenerator( + { appName: 'myapp', framework: 'hono', typescript: true, config }, + deps + ); + gen.createAppStructure(); + expect(deps.fileCreator.createHonoApp).toHaveBeenCalledWith( + config.paths.templates.hono, + path.join(testDir, 'myapp'), + 'myapp', + { typescript: true } + ); + expect(deps.fileCreator.createExpressApp).not.toHaveBeenCalled(); + expect(deps.fileCreator.createNodeApp).not.toHaveBeenCalled(); + }); + + it('fails clearly when hono generation is not available', () => { + const gen = new AppGenerator( + { appName: 'myapp', framework: 'hono', config }, + deps + ); + expect(() => gen.createAppStructure()).toThrow('Hono generation is not available'); + expect(deps.fileCreator.createExpressApp).not.toHaveBeenCalled(); + expect(deps.fileCreator.createNodeApp).not.toHaveBeenCalled(); + }); }); describe('setupViews', () => { @@ -324,6 +360,15 @@ describe('AppGenerator', () => { expect(deps.fileCreator.handleViews).not.toHaveBeenCalled(); }); + it('is a no-op for the hono framework', () => { + const gen = new AppGenerator( + { appName: 'myapp', framework: 'hono', view: 'ejs', config }, + deps + ); + gen.setupViews(); + expect(deps.fileCreator.handleViews).not.toHaveBeenCalled(); + }); + it('calls handleViews for the express framework', () => { const gen = new AppGenerator( { appName: 'myapp', framework: 'express', view: 'ejs', config }, @@ -400,6 +445,15 @@ describe('AppGenerator', () => { gen.setupDatabase(); expect(deps.fileCreator.handleConfig).not.toHaveBeenCalled(); }); + + it('does not call handleConfig for the hono framework', () => { + const gen = new AppGenerator( + { appName: 'myapp', framework: 'hono', db: true, config }, + deps + ); + gen.setupDatabase(); + expect(deps.fileCreator.handleConfig).not.toHaveBeenCalled(); + }); }); describe('addSupportFiles', () => { @@ -429,7 +483,7 @@ describe('AppGenerator', () => { ); }); - it('adds an OpenAPI spec only for express apps when requested', () => { + it('adds an OpenAPI spec for express apps when requested', () => { const gen = new AppGenerator( { appName: 'myapp', @@ -446,7 +500,7 @@ describe('AppGenerator', () => { expect(deps.fileCreator.addOpenApiSpec).toHaveBeenCalledWith( path.join(testDir, 'myapp'), - { appName: 'myapp', port: 8080, view: 'ejs' } + { appName: 'myapp', framework: 'express', port: 8080, view: 'ejs' } ); expect(deps.fileCreator.addReadme).toHaveBeenCalledWith( path.join(testDir, 'myapp'), @@ -455,6 +509,31 @@ describe('AppGenerator', () => { ); }); + it('adds an OpenAPI spec for hono apps when requested', () => { + const gen = new AppGenerator( + { + appName: 'myapp', + framework: 'hono', + openapi: true, + port: 8081, + config, + }, + deps + ); + + gen.addSupportFiles(); + + expect(deps.fileCreator.addOpenApiSpec).toHaveBeenCalledWith( + path.join(testDir, 'myapp'), + { appName: 'myapp', framework: 'hono', port: 8081, view: undefined } + ); + expect(deps.fileCreator.addReadme).toHaveBeenCalledWith( + path.join(testDir, 'myapp'), + config.paths.templates.hono, + { appName: 'myapp', db: false, openapi: true, port: 8081, typescript: true } + ); + }); + it('does not add an OpenAPI spec for node apps', () => { const gen = new AppGenerator( { @@ -527,6 +606,27 @@ describe('AppGenerator', () => { expect(updated).not.toContain('process.env.PORT || 3000'); }); + it('rewrites the port in src/index.ts for Hono apps', () => { + const folderDir = path.join(testDir, 'myapp'); + fs.ensureDirSync(path.join(folderDir, 'src')); + const indexPath = path.join(folderDir, 'src', 'index.ts'); + fs.writeFileSync( + indexPath, + 'const port = Number(process.env.PORT || 3000);\n', + 'utf8' + ); + + const gen = new AppGenerator( + { appName: 'myapp', framework: 'hono', port: 8787, config }, + deps + ); + gen.configurePort(); + + const updated = fs.readFileSync(indexPath, 'utf8'); + expect(updated).toContain('process.env.PORT || 8787'); + expect(updated).not.toContain('process.env.PORT || 3000'); + }); + it('replaces every occurrence of the default port token', () => { const folderDir = path.join(testDir, 'myapp'); fs.ensureDirSync(folderDir); diff --git a/tests/unit/config.test.js b/tests/unit/config.test.js index 5ed8cf0..75cfb18 100644 --- a/tests/unit/config.test.js +++ b/tests/unit/config.test.js @@ -29,6 +29,13 @@ describe('getConfig', () => { ); }); + it('sets hono template path', () => { + const config = getConfig(baseDir, cwd); + expect(config.paths.templates.hono).toBe( + path.join(baseDir, '..', 'templates', 'hono') + ); + }); + it('sets views path', () => { const config = getConfig(baseDir, cwd); expect(config.paths.templates.views).toBe( @@ -47,6 +54,7 @@ describe('getConfig', () => { const config = getConfig(baseDir, cwd); expect(config.validation.frameworks).toContain('node'); expect(config.validation.frameworks).toContain('express'); + expect(config.validation.frameworks).toContain('hono'); }); it('includes valid views', () => { diff --git a/tests/unit/file_generator.test.js b/tests/unit/file_generator.test.js index 36f7f4a..6457d2c 100644 --- a/tests/unit/file_generator.test.js +++ b/tests/unit/file_generator.test.js @@ -5,6 +5,7 @@ import path from 'path'; import { fileURLToPath } from 'url'; import { createExpressApp, + createHonoApp, createNodeApp, handleViews, handleConfig, @@ -18,6 +19,7 @@ import { VIEW_ENGINES, DEPENDENCY_VERSIONS } from '../../lib/constants.js'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const templatesDir = path.join(__dirname, '..', '..', 'templates', 'express'); +const honoTemplatesDir = path.join(__dirname, '..', '..', 'templates', 'hono'); const nodeTemplatesDir = path.join(__dirname, '..', '..', 'templates', 'node'); const viewsDir = path.join(templatesDir, 'views'); let testDir; @@ -289,6 +291,62 @@ describe('file_generator', () => { }); }); + describe('createHonoApp', () => { + it('generates TypeScript Hono source, test and tsconfig files', () => { + createHonoApp(honoTemplatesDir, testDir, 'hono-app'); + + expect(fs.existsSync(path.join(testDir, 'src', 'index.ts'))).toBe(true); + expect(fs.existsSync(path.join(testDir, 'test', 'app.test.ts'))).toBe(true); + expect(fs.existsSync(path.join(testDir, 'tsconfig.json'))).toBe(true); + expect(fs.existsSync(path.join(testDir, 'index.js'))).toBe(false); + }); + + it('writes Hono package scripts and dependencies', () => { + createHonoApp(honoTemplatesDir, testDir, 'hono-app'); + const pkg = readPkg(testDir); + + expect(pkg.name).toBe('hono-app'); + expect(pkg.type).toBe('module'); + expect(pkg.main).toBe('dist/index.js'); + expect(pkg.scripts.dev).toBe('tsx watch src/index.ts'); + expect(pkg.scripts.build).toBe('tsc'); + expect(pkg.scripts.start).toBe('node dist/index.js'); + expect(pkg.scripts.test).toBe('node --import tsx --test test/**/*.test.ts'); + expect(pkg.dependencies.hono).toBe(DEPENDENCY_VERSIONS.hono); + expect(pkg.dependencies['@hono/node-server']).toBe( + DEPENDENCY_VERSIONS['@hono/node-server'] + ); + expect(pkg.dependencies.dotenv).toBe(DEPENDENCY_VERSIONS.dotenv); + expect(pkg.devDependencies.typescript).toBe(DEPENDENCY_VERSIONS.typescript); + expect(pkg.devDependencies.tsx).toBe(DEPENDENCY_VERSIONS.tsx); + expect(pkg.devDependencies['@types/node']).toBe( + DEPENDENCY_VERSIONS['@types/node'] + ); + expect(pkg.devDependencies.nodemon).toBeUndefined(); + }); + + it('uses an import-safe server entry point with JSON routes', () => { + createHonoApp(honoTemplatesDir, testDir, 'hono-app'); + const index = fs.readFileSync( + path.join(testDir, 'src', 'index.ts'), + 'utf-8' + ); + + expect(index).toContain('new Hono()'); + expect(index).toContain("app.get('/about'"); + expect(index).toContain("app.get('/contact'"); + expect(index).toContain("app.get('/health'"); + expect(index).toContain('app.notFound'); + expect(index).toContain('export const startServer'); + expect(index).toContain('pathToFileURL(process.argv[1])'); + }); + + it('logs the Hono generation message', () => { + createHonoApp(honoTemplatesDir, testDir, 'hono-app'); + expect(logSpy).toHaveBeenCalledWith('Generating TypeScript Hono application..'); + }); + }); + describe('handleViews', () => { it('logs "No Views Selected" and creates a views/ dir when no view is provided', () => { // manageViews(false) reads index.js, so provide one with the marker. @@ -566,6 +624,26 @@ describe('file_generator', () => { expect(spec).toContain("InternalServerError:"); }); + it('writes Hono OpenAPI docs with Hono routes and JSON 404s', () => { + addOpenApiSpec(testDir, { + appName: 'hono-api', + framework: 'hono', + port: 8787, + }); + const spec = fs.readFileSync( + path.join(testDir, 'docs', 'openapi.yaml'), + 'utf-8' + ); + + expect(spec).toContain('Hono app generated by ServerGen'); + expect(spec).toContain('url: http://localhost:8787'); + expect(spec).toContain('operationId: getAbout'); + expect(spec).toContain('operationId: getContact'); + expect(spec).toContain('application/json:'); + expect(spec).not.toContain('operationId: postRoot'); + expect(spec).not.toContain('text/html:'); + }); + it('documents rendered HTML for root GET when a view is selected', () => { addOpenApiSpec(testDir, { appName: 'view-api', port: 3000, view: 'pug' }); const spec = fs.readFileSync( diff --git a/tests/unit/interactive.test.js b/tests/unit/interactive.test.js index 2f6b4d5..dd6b2c9 100644 --- a/tests/unit/interactive.test.js +++ b/tests/unit/interactive.test.js @@ -35,12 +35,14 @@ describe('interactive prompts', () => { '', '', '', + '', ]); const result = await promptForInteractiveOptions(harness); expect(result).toEqual({ name: 'My API', + framework: 'express', typescript: true, openapi: true, db: false, @@ -50,6 +52,7 @@ describe('interactive prompts', () => { }); expect(harness.prompts).toEqual([ 'Project name: ', + 'Framework (express/node/hono) [express]: ', 'Language (TypeScript/JavaScript) [TypeScript]: ', 'OpenAPI spec? (Y/n): ', 'Database (none/mongodb) [none]: ', @@ -62,6 +65,7 @@ describe('interactive prompts', () => { it('maps explicit answers to generator options', async () => { const harness = createPromptHarness([ 'api', + 'express', 'javascript', 'no', 'mongoose', @@ -74,6 +78,7 @@ describe('interactive prompts', () => { expect(result).toEqual({ name: 'api', + framework: 'express', typescript: false, openapi: false, db: true, @@ -87,6 +92,8 @@ describe('interactive prompts', () => { const harness = createPromptHarness([ '', 'api', + 'rails', + 'express', 'ruby', 'ts', 'maybe', @@ -106,6 +113,7 @@ describe('interactive prompts', () => { expect(result).toMatchObject({ name: 'api', + framework: 'express', typescript: true, openapi: true, db: true, @@ -114,6 +122,7 @@ describe('interactive prompts', () => { skipInstall: false, }); expect(harness.messages.join('')).toContain('Project name is required.'); + expect(harness.messages.join('')).toContain('Please choose express, node, or hono.'); expect(harness.messages.join('')).toContain('Please choose TypeScript or JavaScript.'); expect(harness.messages.join('')).toContain('Please answer yes or no.'); expect(harness.messages.join('')).toContain('Please choose none or mongodb.'); @@ -122,7 +131,7 @@ describe('interactive prompts', () => { }); it('allows custom default ports', async () => { - const harness = createPromptHarness(['api', '', '', '', '', '', '']); + const harness = createPromptHarness(['api', '', '', '', '', '', '', '']); const result = await promptForInteractiveOptions({ ...harness, @@ -132,6 +141,53 @@ describe('interactive prompts', () => { expect(result.port).toBe(8088); expect(harness.prompts).toContain('Port [8088]: '); }); + + it('uses implicit TypeScript and skips Express-only prompts for Hono', async () => { + const harness = createPromptHarness(['api', 'hono', 'y', '8787', 'n']); + + const result = await promptForInteractiveOptions(harness); + + expect(result).toEqual({ + name: 'api', + framework: 'hono', + typescript: true, + openapi: true, + db: false, + view: undefined, + port: 8787, + skipInstall: true, + }); + expect(harness.prompts).toEqual([ + 'Project name: ', + 'Framework (express/node/hono) [express]: ', + 'OpenAPI spec? (Y/n): ', + 'Port [3000]: ', + 'Install dependencies? (Y/n): ', + ]); + }); + + it('skips TypeScript and Express-only prompts for Node', async () => { + const harness = createPromptHarness(['api', 'node', '8081', 'n']); + + const result = await promptForInteractiveOptions(harness); + + expect(result).toEqual({ + name: 'api', + framework: 'node', + typescript: false, + openapi: false, + db: false, + view: undefined, + port: 8081, + skipInstall: true, + }); + expect(harness.prompts).toEqual([ + 'Project name: ', + 'Framework (express/node/hono) [express]: ', + 'Port [3000]: ', + 'Install dependencies? (Y/n): ', + ]); + }); }); describe('isInteractiveTerminal', () => { diff --git a/tests/unit/validator.test.js b/tests/unit/validator.test.js index 79ede60..8b12116 100644 --- a/tests/unit/validator.test.js +++ b/tests/unit/validator.test.js @@ -2,7 +2,7 @@ import { describe, it, expect } from 'vitest'; import { validateOptions } from '../../lib/validator.js'; const validationRules = { - frameworks: ['node', 'express'], + frameworks: ['node', 'express', 'hono'], views: ['ejs', 'jade', 'pug', 'hbs'], }; @@ -19,6 +19,11 @@ describe('validateOptions', () => { expect(result.isValid).toBe(true); }); + it('accepts valid framework: hono', () => { + const result = validateOptions({ framework: 'hono' }, validationRules); + expect(result.isValid).toBe(true); + }); + it('rejects invalid framework', () => { const result = validateOptions({ framework: 'invalid' }, validationRules); expect(result.isValid).toBe(false); @@ -128,6 +133,15 @@ describe('validateOptions', () => { expect(result.errors.some((e) => e.includes('express framework'))).toBe(true); }); + it('rejects --db with the hono framework', () => { + const result = validateOptions( + { framework: 'hono', db: true }, + validationRules + ); + expect(result.isValid).toBe(false); + expect(result.errors.some((e) => e.includes('express framework'))).toBe(true); + }); + it('allows --db with the express framework', () => { const result = validateOptions( { framework: 'express', db: true }, @@ -145,6 +159,14 @@ describe('validateOptions', () => { expect(result.errors.some((e) => e.includes('--openapi option'))).toBe(true); }); + it('allows --openapi with the hono framework', () => { + const result = validateOptions( + { framework: 'hono', openapi: true }, + validationRules + ); + expect(result.isValid).toBe(true); + }); + it('allows --openapi with the express framework', () => { const result = validateOptions( { framework: 'express', openapi: true }, @@ -159,7 +181,7 @@ describe('validateOptions', () => { validationRules ); expect(result.isValid).toBe(false); - expect(result.errors.some((e) => e.includes('express framework'))).toBe(true); + expect(result.errors.some((e) => e.includes('express and hono frameworks'))).toBe(true); }); it('allows --typescript with the express framework', () => { @@ -169,6 +191,14 @@ describe('validateOptions', () => { ); expect(result.isValid).toBe(true); }); + + it('allows --typescript with the hono framework', () => { + const result = validateOptions( + { framework: 'hono', typescript: true }, + validationRules + ); + expect(result.isValid).toBe(true); + }); }); describe('combined validation', () => { From 41e74c6edc06e99d4624dee78bff520453526f53 Mon Sep 17 00:00:00 2001 From: Keshav Malik Date: Wed, 24 Jun 2026 19:13:25 +0530 Subject: [PATCH 4/5] Document Hono scaffold usage --- .github/workflows/release.yml | 2 +- README.md | 23 +++++++++-------- docs/examples/README.md | 4 ++- docs/examples/hono.md | 48 +++++++++++++++++++++++++++++++++++ 4 files changed, 65 insertions(+), 12 deletions(-) create mode 100644 docs/examples/hono.md diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index be2750b..ed5a02f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,7 +1,7 @@ name: Release # Release pipeline: -# 1) smoke: pack, install the tarball, scaffold Node and Express apps, boot their +# 1) smoke: pack, install the tarball, scaffold Node, Express, and Hono apps, boot their # servers and verify a live HTTP response. Kept out of the per-PR ci.yml job. # 2) publish: on a version tag, publish the CLI and npm-create wrapper to npm # using OIDC trusted publishing (no long-lived token; provenance is attached diff --git a/README.md b/README.md index 3e66a54..872a084 100644 --- a/README.md +++ b/README.md @@ -7,9 +7,10 @@ [![Node >=20](https://img.shields.io/badge/node-%3E%3D20-brightgreen.svg)](package.json) [![GitHub release](https://img.shields.io/github/v/release/theinfosecguy/ServerGen?display_name=tag)](https://github.com/theinfosecguy/ServerGen/releases/latest) -ServerGen is an npm CLI for scaffolding Node.js and Express API projects with -practical defaults: MVC-style folders, health checks, Docker files, ready-to-run -npm scripts, optional Express views, and optional Mongoose/MongoDB config. +ServerGen is an npm CLI for scaffolding Node.js, Express, and Hono API projects +with practical defaults: MVC-style folders, health checks, Docker files, +ready-to-run npm scripts, optional Express views, and optional +Mongoose/MongoDB config. ## 30-Second Quick Start @@ -48,14 +49,15 @@ npx servergen@latest my-api | Choice | Output | | --- | --- | | Default Express app | `index.js`, `routes/index.js`, `controllers/`, `model/`, `views/`, `.env.example`, `Dockerfile`, `.dockerignore`, `.gitignore`, generated `README.md`, `package.json`, and `test/app.test.js`. | -| `--typescript` | Express app with `src/index.ts`, `src/routes/index.ts`, `tsconfig.json`, `test/app.test.ts`, `tsx` for development, and `dist/` output for production start. | +| `--typescript` | TypeScript Express app with `src/index.ts`, `src/routes/index.ts`, `tsconfig.json`, `test/app.test.ts`, `tsx` for development, and `dist/` output for production start. Hono apps are TypeScript by default. | | `--framework node` | Plain Node.js HTTP server with `/`, `/about`, `/contact`, and `/health`, plus MVC folders, Docker files, `.gitignore`, generated `README.md`, and `package.json`. | +| `--framework hono` | TypeScript Hono API app with `src/index.ts`, `tsconfig.json`, `test/app.test.ts`, Docker files, `.gitignore`, generated `README.md`, and `package.json`. | | `--view ejs`, `pug`, or `hbs` | Adds the selected Express view template and renders it from `/`. | | `--db` | Adds Mongoose, `config/mongoose.js`, and `MONGODB_URI` in `.env.example` for Express apps. | -| `--openapi` | Adds `docs/openapi.yaml`, a static OpenAPI 3.0 spec for the generated Express routes. | +| `--openapi` | Adds `docs/openapi.yaml`, a static OpenAPI 3.0 spec for generated Express and Hono routes. | Generated apps include `npm start` and `npm run dev`. Express apps also include -`npm test`. TypeScript Express apps also include `npm run build`. +`npm test`. TypeScript Express and Hono apps also include `npm run build`. ## CLI Usage @@ -70,11 +72,11 @@ servergen [options] [name] ```text -V, --version output the version number -n, --name name of the app to create - -f, --framework framework: express | node (default: "express") + -f, --framework framework: express | node | hono (default: "express") -v, --view view engine (express only): ejs | pug | hbs --db add Mongoose and a MongoDB config (express only) - --openapi generate an OpenAPI spec file (express only) - --typescript generate an Express TypeScript app + --openapi generate an OpenAPI spec file (express and hono) + --typescript generate a TypeScript app where supported -p, --port port for the generated app (1-65535) (default: "3000") --skip-install skip the npm install step --debug enable debug logging @@ -87,6 +89,7 @@ servergen [options] [name] npm create servergen@latest npx servergen@latest my-api npx servergen@latest my-api --framework node +npx servergen@latest my-api --framework hono npx servergen@latest my-api --view ejs npx servergen@latest my-api --db npx servergen@latest my-api --openapi @@ -137,7 +140,7 @@ servergen my-api Tagged releases are published from GitHub Actions with npm trusted publishing and provenance, using OIDC instead of a long-lived npm token. Before publishing, the release workflow packs the package, installs that tarball in a throwaway -project, scaffolds Express and Node apps, starts them, and verifies live HTTP +project, scaffolds Express, Hono, and Node apps, starts them, and verifies live HTTP responses. The same workflow creates or updates the matching GitHub Release as `latest` diff --git a/docs/examples/README.md b/docs/examples/README.md index 1a2ab60..d3bfdbc 100644 --- a/docs/examples/README.md +++ b/docs/examples/README.md @@ -15,6 +15,7 @@ The command-by-command examples use `npx --yes servergen@latest` to follow the c - [Default Express app](./express.md) - [TypeScript Express app](./typescript.md) - [Plain Node app](./node.md) +- [Hono app](./hono.md) - [Express views with EJS, Pug, or HBS](./views.md) - [Express app with MongoDB/Mongoose config](./mongodb.md) - [Custom port and Docker notes](./custom-port-docker.md) @@ -27,5 +28,6 @@ The command-by-command examples use `npx --yes servergen@latest` to follow the c - Generated apps require Node.js 20 or newer. - Generation runs `npm install` unless you pass `--skip-install`. - When install is not skipped, npm also creates `node_modules/` and `package-lock.json` inside the generated app. -- Express-only options: `--view ejs|pug|hbs`, `--db`, `--openapi`, and `--typescript`. +- Express-only options: `--view ejs|pug|hbs` and `--db`. +- Hono accepts `--typescript` and `--openapi`; the Hono preset does not support `--view` or `--db`. - Generated apps include Docker support files. Express apps also include `.env.example`; Node apps do not. diff --git a/docs/examples/hono.md b/docs/examples/hono.md new file mode 100644 index 0000000..b18426c --- /dev/null +++ b/docs/examples/hono.md @@ -0,0 +1,48 @@ +# Hono App + +Use this when you want a Hono API preset. + +## Command + +```bash +npx --yes servergen@latest hello-hono --framework hono +``` + +Hono apps are TypeScript apps. Passing `--typescript` is accepted but not +required: + +```bash +npx --yes servergen@latest hello-hono --framework hono --typescript +``` + +## Option Compatibility + +Hono supports OpenAPI output: + +```bash +npx --yes servergen@latest hello-hono --framework hono --openapi +``` + +Hono does not support the Express-only view and database options: + +```bash +npx --yes servergen@latest hello-hono --framework hono --view ejs +npx --yes servergen@latest hello-hono --framework hono --db +``` + +Each command fails before creating the app directory. + +## Run + +```bash +cd hello-hono +npm run dev +``` + +## Verify + +```bash +npm test +npm run build +curl http://localhost:3000/health +``` From 6e7b7c8210017012314e529693824ef612587919 Mon Sep 17 00:00:00 2001 From: Keshav Malik Date: Wed, 24 Jun 2026 19:13:30 +0530 Subject: [PATCH 5/5] Bump packages to 2.4.0 --- CHANGELOG.md | 6 ++++++ package.json | 5 +++-- packages/create-servergen/package.json | 4 ++-- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f7f046..ebd28e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ ## Unreleased +## 2.4.0 - 2026-06-24 + +- Add Hono framework support with TypeScript-first generated apps, Docker files, generated tests, and JSON routes for `/`, `/about`, `/contact`, and `/health`. +- Add OpenAPI output for Hono apps while keeping `--view` and `--db` Express-only. +- Update interactive mode, docs, examples, tarball packaging tests, and release smoke coverage for Hono. + ## 2.3.0 - 2026-06-24 - Add TypeScript support for generated Express apps with `src/index.ts`, `tsconfig.json`, `tsx` development, `tsc` builds to `dist`, generated TypeScript tests, and Docker support. diff --git a/package.json b/package.json index 99559ac..25ddcf3 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "servergen", - "version": "2.3.0", - "description": "CLI that scaffolds production-ready Node.js and Express apps with MVC structure, optional view engines, MongoDB, and Docker support.", + "version": "2.4.0", + "description": "CLI that scaffolds production-ready Node.js, Express, and Hono apps with practical defaults, optional views, MongoDB, OpenAPI, and Docker support.", "type": "module", "main": "./index.js", "engines": { @@ -34,6 +34,7 @@ "docker", "express", "generator", + "hono", "mongodb", "mvc", "nodejs", diff --git a/packages/create-servergen/package.json b/packages/create-servergen/package.json index 743a18f..6ba198b 100644 --- a/packages/create-servergen/package.json +++ b/packages/create-servergen/package.json @@ -1,6 +1,6 @@ { "name": "create-servergen", - "version": "2.3.0", + "version": "2.4.0", "description": "npm create wrapper for the servergen CLI.", "type": "module", "bin": { @@ -32,6 +32,6 @@ }, "homepage": "https://github.com/theinfosecguy/ServerGen#readme", "dependencies": { - "servergen": "^2.3.0" + "servergen": "^2.4.0" } }