shokupan 0.0.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1628,21 +1628,22 @@ Container.clear();
1628
1628
  - ✅ **OpenTelemetry** - Built-in [OpenTelemetry](https://opentelemetry.io/) traces
1629
1629
  - ✅ **OAuth2** - Built-in [OAuth2](https://oauth.net/2/) support
1630
1630
  - ✅ **Request-Scoped Globals** - Request-scoped values via [AsyncLocalStorage](https://docs.deno.com/api/node/async_hooks/~/AsyncLocalStorage)
1631
-
1631
+ - ✅ **Runtime Compatibility** - Support for [Deno](https://deno.com/) and [Node.js](https://nodejs.org/)
1632
+ - ✅ **Deep Introspection** - Type analysis for enhanced OpenAPI generation
1633
+ - ✅ **Controller Mode** - Option for controller-only mode
1634
+ - ✅ **Supports Node/Deno** - Shokupan can run on Node.js or Deno
1635
+ - ✅ **OpenAPI Validation** - Built-in [OpenAPI](https://www.openapis.org/) validation
1632
1636
 
1633
1637
  ### Future Features
1634
1638
 
1635
- - 🔄 **Runtime Compatibility** - Support for [Deno](https://deno.com/) and [Node.js](https://nodejs.org/)
1636
- - 🔌 **Framework Plugins** - Drop-in adapters for [Express](https://expressjs.com/), [Koa](https://koajs.com/), and [Elysia](https://elysiajs.com/)
1637
- - 📡 **Enhanced WebSockets** - Event support and HTTP simulation
1638
- - 🔍 **Deep Introspection** - Type analysis for enhanced OpenAPI generation
1639
- - 📊 **Benchmarks** - Comprehensive performance comparisons
1640
- - ⚖️ **Scaling** - Automatic clustering support
1641
- - 🔗 **RPC Support** - [tRPC](https://trpc.io/) and [gRPC](https://grpc.io/) integration
1642
- - 📦 **Binary Formats** - [Protobuf](https://protobuf.dev/) and [MessagePack](https://msgpack.org/) support
1643
- - 🛡️ **Reliability** - Circuit breaker pattern for resilience
1644
- - 👮 **Strict Mode** - Enforced controller patterns
1645
- - ⚠️ **Standardized Errors** - Consistent 4xx/5xx error formats
1639
+ - 🚧 **Framework Plugins** - Drop-in adapters for [Express](https://expressjs.com/), [Koa](https://koajs.com/), and [Elysia](https://elysiajs.com/)
1640
+ - 🚧 **Enhanced WebSockets** - Event support and HTTP simulation
1641
+ - 🚧 **Benchmarks** - Comprehensive performance comparisons
1642
+ - 🚧 **Scaling** - Automatic clustering support
1643
+ - 🚧 **RPC Support** - [tRPC](https://trpc.io/) and [gRPC](https://grpc.io/) integration
1644
+ - 🚧 **Binary Formats** - [Protobuf](https://protobuf.dev/) and [MessagePack](https://msgpack.org/) support
1645
+ - 🚧 **Reliability** - Circuit breaker pattern for resilience
1646
+ - 🚧 **Standardized Errors** - Consistent 4xx/5xx error formats
1646
1647
 
1647
1648
  ## 🤝 Contributing
1648
1649
 
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Route information extracted from AST
3
+ */
4
+ export interface RouteInfo {
5
+ method: string;
6
+ path: string;
7
+ handlerName?: string;
8
+ handlerSource?: string;
9
+ requestTypes?: {
10
+ body?: any;
11
+ query?: Record<string, string>;
12
+ params?: Record<string, string>;
13
+ headers?: Record<string, string>;
14
+ };
15
+ responseType?: string;
16
+ responseSchema?: any;
17
+ summary?: string;
18
+ description?: string;
19
+ tags?: string[];
20
+ operationId?: string;
21
+ }
22
+ /**
23
+ * Dependency information
24
+ */
25
+ interface DependencyInfo {
26
+ packageName: string;
27
+ version?: string;
28
+ importPath: string;
29
+ isExternal: boolean;
30
+ }
31
+ /**
32
+ * Application/Router instance found in code
33
+ */
34
+ export interface ApplicationInstance {
35
+ name: string;
36
+ filePath: string;
37
+ className: 'Shokupan' | 'ShokupanRouter' | 'Controller';
38
+ routes: RouteInfo[];
39
+ mounted: MountInfo[];
40
+ }
41
+ interface MountInfo {
42
+ prefix: string;
43
+ target: string;
44
+ dependency?: DependencyInfo;
45
+ }
46
+ /**
47
+ * Main analyzer class
48
+ */
49
+ export declare class OpenAPIAnalyzer {
50
+ private rootDir;
51
+ private files;
52
+ private applications;
53
+ private program?;
54
+ private entrypoint?;
55
+ constructor(rootDir: string, entrypoint?: string);
56
+ /**
57
+ * Main analysis entry point
58
+ */
59
+ /**
60
+ * Main analysis entry point
61
+ */
62
+ analyze(): Promise<{
63
+ applications: ApplicationInstance[];
64
+ }>;
65
+ /**
66
+ * Recursively scan directory for TypeScript/JavaScript files
67
+ */
68
+ private scanDirectory;
69
+ /**
70
+ * Process source maps to reconstruct TypeScript
71
+ */
72
+ private processSourceMaps;
73
+ /**
74
+ * Parse TypeScript files and create AST
75
+ */
76
+ private parseTypeScriptFiles;
77
+ /**
78
+ * Find all Shokupan/ShokupanRouter instances
79
+ */
80
+ private findApplications;
81
+ /**
82
+ * Visit AST node to find application instances
83
+ */
84
+ private visitNode;
85
+ /**
86
+ * Extract route information from applications
87
+ */
88
+ private extractRoutes;
89
+ /**
90
+ * Extract routes from a Controller class
91
+ */
92
+ private extractRoutesFromController;
93
+ /**
94
+ * Extract routes from a specific file
95
+ */
96
+ private extractRoutesFromFile;
97
+ /**
98
+ * Extract route information from a route call (e.g., app.get('/path', handler))
99
+ */
100
+ private extractRouteFromCall;
101
+ /**
102
+ * Analyze a route handler to extract type information
103
+ */
104
+ private analyzeHandler;
105
+ /**
106
+ * Convert an Expression node to an OpenAPI schema (best effort)
107
+ */
108
+ private convertExpressionToSchema;
109
+ /**
110
+ * Check if an expression is a call to ctx.body()
111
+ */
112
+ private isCtxBodyCall;
113
+ /**
114
+ * Convert a TypeScript TypeNode to an OpenAPI schema
115
+ */
116
+ private convertTypeNodeToSchema;
117
+ /**
118
+ * Extract mount information from mount call
119
+ */
120
+ private extractMountFromCall;
121
+ /**
122
+ * Check if a reference is to an external dependency
123
+ */
124
+ private checkIfExternalDependency;
125
+ /**
126
+ * Get package version from package.json
127
+ */
128
+ private getPackageVersion;
129
+ /**
130
+ * Generate OpenAPI specification
131
+ */
132
+ generateOpenAPISpec(): any;
133
+ /**
134
+ * Convert a type string to an OpenAPI schema
135
+ */
136
+ private typeToSchema;
137
+ }
138
+ /**
139
+ * Analyze a directory and generate OpenAPI spec
140
+ */
141
+ export declare function analyzeDirectory(directory: string): Promise<any>;
142
+ export {};
package/dist/cli.cjs CHANGED
@@ -4,6 +4,7 @@ const p = require("@clack/prompts");
4
4
  const fs = require("node:fs");
5
5
  const path = require("node:path");
6
6
  const promises = require("node:timers/promises");
7
+ const openapiAnalyzer = require("./openapi-analyzer-BN0wFCML.cjs");
7
8
  function _interopNamespaceDefault(e) {
8
9
  const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
9
10
  if (e) {
@@ -71,7 +72,7 @@ export class ${name}Plugin extends ShokupanRouter {
71
72
  }
72
73
  `
73
74
  };
74
- async function main() {
75
+ async function scaffold() {
75
76
  console.clear();
76
77
  p__namespace.intro(`Shokupan CLI Scaffolder`);
77
78
  if (!fs.existsSync("package.json")) {
@@ -148,7 +149,66 @@ async function main() {
148
149
  const nextSteps = ` -> ${finalPath}
149
150
  Make sure to register it in your main application file if necessary.`;
150
151
  p__namespace.note(nextSteps, "Next steps");
151
- p__namespace.outro(`Problems? Open an issue at https://github.com/dotglitch/express.ts`);
152
+ p__namespace.outro(`Problems? Open an issue at https://github.com/dotglitch/shokupan`);
153
+ }
154
+ async function analyze() {
155
+ console.clear();
156
+ p__namespace.intro(`Shokupan OpenAPI Analyzer`);
157
+ const args = process.argv.slice(2);
158
+ let directory = process.cwd();
159
+ let outputPath = "openapi.json";
160
+ const analyzeIndex = args.indexOf("analyze");
161
+ if (analyzeIndex !== -1 && args.length > analyzeIndex + 1) {
162
+ const nextArg = args[analyzeIndex + 1];
163
+ if (!nextArg.startsWith("--")) {
164
+ directory = path.resolve(nextArg);
165
+ }
166
+ }
167
+ const outputIndex = args.indexOf("--output");
168
+ if (outputIndex !== -1 && args.length > outputIndex + 1) {
169
+ outputPath = args[outputIndex + 1];
170
+ }
171
+ if (!fs.existsSync(directory)) {
172
+ p__namespace.cancel(`Directory not found: ${directory}`);
173
+ process.exit(1);
174
+ }
175
+ const s = p__namespace.spinner();
176
+ s.start(`Analyzing directory: ${directory}`);
177
+ try {
178
+ const spec = await openapiAnalyzer.analyzeDirectory(directory);
179
+ s.stop("Analysis complete");
180
+ const fullOutputPath = path.resolve(outputPath);
181
+ fs.writeFileSync(fullOutputPath, JSON.stringify(spec, null, 2));
182
+ p__namespace.note(`OpenAPI spec written to: ${fullOutputPath}`, "Success");
183
+ const pathCount = Object.keys(spec.paths || {}).length;
184
+ p__namespace.note(`Found ${pathCount} unique paths`, "Summary");
185
+ p__namespace.outro("Done!");
186
+ } catch (error) {
187
+ s.stop("Analysis failed");
188
+ p__namespace.cancel(`Error: ${error.message}`);
189
+ console.error(error);
190
+ process.exit(1);
191
+ }
192
+ }
193
+ async function main() {
194
+ const args = process.argv.slice(2);
195
+ const command = args[0];
196
+ if (command === "analyze") {
197
+ await analyze();
198
+ } else if (command === "scaffold" || !command) {
199
+ await scaffold();
200
+ } else {
201
+ console.log("Shokupan CLI");
202
+ console.log("");
203
+ console.log("Commands:");
204
+ console.log(" scaffold (default) - Scaffold controllers, middleware, or plugins");
205
+ console.log(" analyze <directory> - Analyze a Shokupan application and generate OpenAPI spec");
206
+ console.log("");
207
+ console.log("Usage:");
208
+ console.log(" shokupan scaffold");
209
+ console.log(" shokupan analyze <directory> [--output openapi.json]");
210
+ process.exit(0);
211
+ }
152
212
  }
153
213
  main().catch(console.error);
154
214
  //# sourceMappingURL=cli.cjs.map
package/dist/cli.cjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.cjs","sources":["../src/cli/index.ts"],"sourcesContent":["#!/usr/bin/env bun\nimport * as p from '@clack/prompts';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport { setTimeout } from 'node:timers/promises';\n\nconst templates = {\n controller: (name: string) => `import { Controller, Get, Ctx } from 'shokupan';\nimport { ShokupanContext } from 'shokupan';\n\n@Controller('/${name.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase()}')\nexport class ${name}Controller {\n @Get('/')\n public index(@Ctx() ctx: ShokupanContext) {\n return { message: 'Hello from ${name}Controller' };\n }\n}\n`,\n middleware: (name: string) => `import { ShokupanContext, NextFn } from 'shokupan';\n\n/**\n * ${name} Middleware\n */\nexport const ${name}Middleware = async (ctx: ShokupanContext, next: NextFn) => {\n // Before next\n // console.log('${name} Middleware - Request');\n\n const result = await next();\n\n // After next\n // console.log('${name} Middleware - Response');\n \n return result;\n};\n`,\n plugin: (name: string) => `import { ShokupanRouter } from 'shokupan';\nimport { ShokupanContext } from 'shokupan';\n\nexport interface ${name}Options {\n // Define options here\n}\n\nexport class ${name}Plugin extends ShokupanRouter {\n constructor(private options: ${name}Options = {}) {\n super();\n this.init();\n }\n\n private init() {\n this.get('/', (ctx: ShokupanContext) => {\n return { message: '${name} Plugin Active' };\n });\n }\n}\n`\n};\n\nasync function main() {\n console.clear();\n p.intro(`Shokupan CLI Scaffolder`);\n\n // Check if running in a project root\n if (!fs.existsSync('package.json')) {\n p.note('Warning: No package.json found in current directory. Are you in the project root?');\n }\n\n const project = await p.group(\n {\n type: () => p.select({\n message: 'What do you want to scaffold?',\n options: [\n { value: 'controller', label: 'Controller' },\n { value: 'middleware', label: 'Middleware' },\n { value: 'plugin', label: 'Plugin' },\n ],\n }),\n name: () => p.text({\n message: 'Name (PascalCase, e.g. UserAuth):',\n validate: (value) => {\n if (!value) return 'Name is required';\n if (!/^[A-Z][a-zA-Z0-9]*$/.test(value)) return 'Please use PascalCase';\n return undefined;\n },\n }),\n dir: () => p.text({\n message: 'Output directory (leave empty for default):',\n placeholder: 'src/controllers',\n }),\n },\n {\n onCancel: () => {\n p.cancel('Operation cancelled.');\n process.exit(0);\n },\n }\n );\n\n const type = project.type as keyof typeof templates;\n const name = project.name;\n let dir = project.dir;\n\n if (!dir || dir.trim() === '') {\n switch (type) {\n case 'controller': dir = 'src/controllers'; break;\n case 'middleware': dir = 'src/middleware'; break;\n case 'plugin': dir = 'src/plugins'; break;\n }\n }\n\n // Convert PascalCase to kebab-case for filename\n const kebabName = name.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();\n const fileName = `${kebabName}.ts`;\n\n const finalPath = path.join(process.cwd(), dir, fileName);\n\n // Ensure directory exists\n if (!fs.existsSync(path.dirname(finalPath))) {\n fs.mkdirSync(path.dirname(finalPath), { recursive: true });\n }\n\n // Check for overwrite\n if (fs.existsSync(finalPath)) {\n const overwrite = await p.confirm({\n message: `File ${finalPath} already exists. Overwrite?`,\n initialValue: false\n });\n\n if (p.isCancel(overwrite) || !overwrite) {\n p.cancel('Operation cancelled.');\n process.exit(0);\n }\n }\n\n const s = p.spinner();\n s.start(`Creating ${type}...`);\n\n await setTimeout(500); // Artificial delay to show spinner\n\n const content = templates[type](name);\n fs.writeFileSync(finalPath, content);\n\n s.stop(`Created ${type}`);\n\n const nextSteps = ` -> ${finalPath}\nMake sure to register it in your main application file if necessary.`;\n\n p.note(nextSteps, 'Next steps');\n\n p.outro(`Problems? Open an issue at https://github.com/dotglitch/express.ts`);\n}\n\nmain().catch(console.error);;\n"],"names":["p","setTimeout"],"mappings":"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"}
1
+ {"version":3,"file":"cli.cjs","sources":["../src/cli/index.ts"],"sourcesContent":["#!/usr/bin/env bun\nimport * as p from '@clack/prompts';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport { setTimeout } from 'node:timers/promises';\nimport { analyzeDirectory } from '../analysis/openapi-analyzer';\n\nconst templates = {\n controller: (name: string) => `import { Controller, Get, Ctx } from 'shokupan';\nimport { ShokupanContext } from 'shokupan';\n\n@Controller('/${name.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase()}')\nexport class ${name}Controller {\n @Get('/')\n public index(@Ctx() ctx: ShokupanContext) {\n return { message: 'Hello from ${name}Controller' };\n }\n}\n`,\n middleware: (name: string) => `import { ShokupanContext, NextFn } from 'shokupan';\n\n/**\n * ${name} Middleware\n */\nexport const ${name}Middleware = async (ctx: ShokupanContext, next: NextFn) => {\n // Before next\n // console.log('${name} Middleware - Request');\n\n const result = await next();\n\n // After next\n // console.log('${name} Middleware - Response');\n \n return result;\n};\n`,\n plugin: (name: string) => `import { ShokupanRouter } from 'shokupan';\nimport { ShokupanContext } from 'shokupan';\n\nexport interface ${name}Options {\n // Define options here\n}\n\nexport class ${name}Plugin extends ShokupanRouter {\n constructor(private options: ${name}Options = {}) {\n super();\n this.init();\n }\n\n private init() {\n this.get('/', (ctx: ShokupanContext) => {\n return { message: '${name} Plugin Active' };\n });\n }\n}\n`\n};\n\nasync function scaffold() {\n console.clear();\n p.intro(`Shokupan CLI Scaffolder`);\n\n // Check if running in a project root\n if (!fs.existsSync('package.json')) {\n p.note('Warning: No package.json found in current directory. Are you in the project root?');\n }\n\n const project = await p.group(\n {\n type: () => p.select({\n message: 'What do you want to scaffold?',\n options: [\n { value: 'controller', label: 'Controller' },\n { value: 'middleware', label: 'Middleware' },\n { value: 'plugin', label: 'Plugin' },\n ],\n }),\n name: () => p.text({\n message: 'Name (PascalCase, e.g. UserAuth):',\n validate: (value) => {\n if (!value) return 'Name is required';\n if (!/^[A-Z][a-zA-Z0-9]*$/.test(value)) return 'Please use PascalCase';\n return undefined;\n },\n }),\n dir: () => p.text({\n message: 'Output directory (leave empty for default):',\n placeholder: 'src/controllers',\n }),\n },\n {\n onCancel: () => {\n p.cancel('Operation cancelled.');\n process.exit(0);\n },\n }\n );\n\n const type = project.type as keyof typeof templates;\n const name = project.name;\n let dir = project.dir;\n\n if (!dir || dir.trim() === '') {\n switch (type) {\n case 'controller': dir = 'src/controllers'; break;\n case 'middleware': dir = 'src/middleware'; break;\n case 'plugin': dir = 'src/plugins'; break;\n }\n }\n\n // Convert PascalCase to kebab-case for filename\n const kebabName = name.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();\n const fileName = `${kebabName}.ts`;\n\n const finalPath = path.join(process.cwd(), dir, fileName);\n\n // Ensure directory exists\n if (!fs.existsSync(path.dirname(finalPath))) {\n fs.mkdirSync(path.dirname(finalPath), { recursive: true });\n }\n\n // Check for overwrite\n if (fs.existsSync(finalPath)) {\n const overwrite = await p.confirm({\n message: `File ${finalPath} already exists. Overwrite?`,\n initialValue: false\n });\n\n if (p.isCancel(overwrite) || !overwrite) {\n p.cancel('Operation cancelled.');\n process.exit(0);\n }\n }\n\n const s = p.spinner();\n s.start(`Creating ${type}...`);\n\n await setTimeout(500); // Artificial delay to show spinner\n\n const content = templates[type](name);\n fs.writeFileSync(finalPath, content);\n\n s.stop(`Created ${type}`);\n\n const nextSteps = ` -> ${finalPath}\nMake sure to register it in your main application file if necessary.`;\n\n p.note(nextSteps, 'Next steps');\n\n p.outro(`Problems? Open an issue at https://github.com/dotglitch/shokupan`);\n}\n\nasync function analyze() {\n console.clear();\n p.intro(`Shokupan OpenAPI Analyzer`);\n\n const args = process.argv.slice(2);\n let directory = process.cwd();\n let outputPath = 'openapi.json';\n\n // Parse command line arguments\n // analyze [directory] [--output file.json]\n const analyzeIndex = args.indexOf('analyze');\n if (analyzeIndex !== -1 && args.length > analyzeIndex + 1) {\n const nextArg = args[analyzeIndex + 1];\n if (!nextArg.startsWith('--')) {\n directory = path.resolve(nextArg);\n }\n }\n\n const outputIndex = args.indexOf('--output');\n if (outputIndex !== -1 && args.length > outputIndex + 1) {\n outputPath = args[outputIndex + 1];\n }\n\n // Verify directory exists\n if (!fs.existsSync(directory)) {\n p.cancel(`Directory not found: ${directory}`);\n process.exit(1);\n }\n\n const s = p.spinner();\n s.start(`Analyzing directory: ${directory}`);\n\n try {\n const spec = await analyzeDirectory(directory);\n\n s.stop('Analysis complete');\n\n // Write to file\n const fullOutputPath = path.resolve(outputPath);\n fs.writeFileSync(fullOutputPath, JSON.stringify(spec, null, 2));\n\n p.note(`OpenAPI spec written to: ${fullOutputPath}`, 'Success');\n\n // Show summary\n const pathCount = Object.keys(spec.paths || {}).length;\n p.note(`Found ${pathCount} unique paths`, 'Summary');\n\n p.outro('Done!');\n } catch (error: any) {\n s.stop('Analysis failed');\n p.cancel(`Error: ${error.message}`);\n console.error(error);\n process.exit(1);\n }\n}\n\nasync function main() {\n const args = process.argv.slice(2);\n const command = args[0];\n\n if (command === 'analyze') {\n await analyze();\n } else if (command === 'scaffold' || !command) {\n // Default to scaffold for backwards compatibility\n await scaffold();\n } else {\n console.log('Shokupan CLI');\n console.log('');\n console.log('Commands:');\n console.log(' scaffold (default) - Scaffold controllers, middleware, or plugins');\n console.log(' analyze <directory> - Analyze a Shokupan application and generate OpenAPI spec');\n console.log('');\n console.log('Usage:');\n console.log(' shokupan scaffold');\n console.log(' shokupan analyze <directory> [--output openapi.json]');\n process.exit(0);\n }\n}\n\nmain().catch(console.error);\n"],"names":["p","setTimeout","analyzeDirectory"],"mappings":"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"}
package/dist/cli.js CHANGED
@@ -3,6 +3,7 @@ import * as p from "@clack/prompts";
3
3
  import fs from "node:fs";
4
4
  import path from "node:path";
5
5
  import { setTimeout } from "node:timers/promises";
6
+ import { analyzeDirectory } from "./openapi-analyzer-BTExMLX4.js";
6
7
  const templates = {
7
8
  controller: (name) => `import { Controller, Get, Ctx } from 'shokupan';
8
9
  import { ShokupanContext } from 'shokupan';
@@ -53,7 +54,7 @@ export class ${name}Plugin extends ShokupanRouter {
53
54
  }
54
55
  `
55
56
  };
56
- async function main() {
57
+ async function scaffold() {
57
58
  console.clear();
58
59
  p.intro(`Shokupan CLI Scaffolder`);
59
60
  if (!fs.existsSync("package.json")) {
@@ -130,7 +131,66 @@ async function main() {
130
131
  const nextSteps = ` -> ${finalPath}
131
132
  Make sure to register it in your main application file if necessary.`;
132
133
  p.note(nextSteps, "Next steps");
133
- p.outro(`Problems? Open an issue at https://github.com/dotglitch/express.ts`);
134
+ p.outro(`Problems? Open an issue at https://github.com/dotglitch/shokupan`);
135
+ }
136
+ async function analyze() {
137
+ console.clear();
138
+ p.intro(`Shokupan OpenAPI Analyzer`);
139
+ const args = process.argv.slice(2);
140
+ let directory = process.cwd();
141
+ let outputPath = "openapi.json";
142
+ const analyzeIndex = args.indexOf("analyze");
143
+ if (analyzeIndex !== -1 && args.length > analyzeIndex + 1) {
144
+ const nextArg = args[analyzeIndex + 1];
145
+ if (!nextArg.startsWith("--")) {
146
+ directory = path.resolve(nextArg);
147
+ }
148
+ }
149
+ const outputIndex = args.indexOf("--output");
150
+ if (outputIndex !== -1 && args.length > outputIndex + 1) {
151
+ outputPath = args[outputIndex + 1];
152
+ }
153
+ if (!fs.existsSync(directory)) {
154
+ p.cancel(`Directory not found: ${directory}`);
155
+ process.exit(1);
156
+ }
157
+ const s = p.spinner();
158
+ s.start(`Analyzing directory: ${directory}`);
159
+ try {
160
+ const spec = await analyzeDirectory(directory);
161
+ s.stop("Analysis complete");
162
+ const fullOutputPath = path.resolve(outputPath);
163
+ fs.writeFileSync(fullOutputPath, JSON.stringify(spec, null, 2));
164
+ p.note(`OpenAPI spec written to: ${fullOutputPath}`, "Success");
165
+ const pathCount = Object.keys(spec.paths || {}).length;
166
+ p.note(`Found ${pathCount} unique paths`, "Summary");
167
+ p.outro("Done!");
168
+ } catch (error) {
169
+ s.stop("Analysis failed");
170
+ p.cancel(`Error: ${error.message}`);
171
+ console.error(error);
172
+ process.exit(1);
173
+ }
174
+ }
175
+ async function main() {
176
+ const args = process.argv.slice(2);
177
+ const command = args[0];
178
+ if (command === "analyze") {
179
+ await analyze();
180
+ } else if (command === "scaffold" || !command) {
181
+ await scaffold();
182
+ } else {
183
+ console.log("Shokupan CLI");
184
+ console.log("");
185
+ console.log("Commands:");
186
+ console.log(" scaffold (default) - Scaffold controllers, middleware, or plugins");
187
+ console.log(" analyze <directory> - Analyze a Shokupan application and generate OpenAPI spec");
188
+ console.log("");
189
+ console.log("Usage:");
190
+ console.log(" shokupan scaffold");
191
+ console.log(" shokupan analyze <directory> [--output openapi.json]");
192
+ process.exit(0);
193
+ }
134
194
  }
135
195
  main().catch(console.error);
136
196
  //# sourceMappingURL=cli.js.map
package/dist/cli.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.js","sources":["../src/cli/index.ts"],"sourcesContent":["#!/usr/bin/env bun\nimport * as p from '@clack/prompts';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport { setTimeout } from 'node:timers/promises';\n\nconst templates = {\n controller: (name: string) => `import { Controller, Get, Ctx } from 'shokupan';\nimport { ShokupanContext } from 'shokupan';\n\n@Controller('/${name.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase()}')\nexport class ${name}Controller {\n @Get('/')\n public index(@Ctx() ctx: ShokupanContext) {\n return { message: 'Hello from ${name}Controller' };\n }\n}\n`,\n middleware: (name: string) => `import { ShokupanContext, NextFn } from 'shokupan';\n\n/**\n * ${name} Middleware\n */\nexport const ${name}Middleware = async (ctx: ShokupanContext, next: NextFn) => {\n // Before next\n // console.log('${name} Middleware - Request');\n\n const result = await next();\n\n // After next\n // console.log('${name} Middleware - Response');\n \n return result;\n};\n`,\n plugin: (name: string) => `import { ShokupanRouter } from 'shokupan';\nimport { ShokupanContext } from 'shokupan';\n\nexport interface ${name}Options {\n // Define options here\n}\n\nexport class ${name}Plugin extends ShokupanRouter {\n constructor(private options: ${name}Options = {}) {\n super();\n this.init();\n }\n\n private init() {\n this.get('/', (ctx: ShokupanContext) => {\n return { message: '${name} Plugin Active' };\n });\n }\n}\n`\n};\n\nasync function main() {\n console.clear();\n p.intro(`Shokupan CLI Scaffolder`);\n\n // Check if running in a project root\n if (!fs.existsSync('package.json')) {\n p.note('Warning: No package.json found in current directory. Are you in the project root?');\n }\n\n const project = await p.group(\n {\n type: () => p.select({\n message: 'What do you want to scaffold?',\n options: [\n { value: 'controller', label: 'Controller' },\n { value: 'middleware', label: 'Middleware' },\n { value: 'plugin', label: 'Plugin' },\n ],\n }),\n name: () => p.text({\n message: 'Name (PascalCase, e.g. UserAuth):',\n validate: (value) => {\n if (!value) return 'Name is required';\n if (!/^[A-Z][a-zA-Z0-9]*$/.test(value)) return 'Please use PascalCase';\n return undefined;\n },\n }),\n dir: () => p.text({\n message: 'Output directory (leave empty for default):',\n placeholder: 'src/controllers',\n }),\n },\n {\n onCancel: () => {\n p.cancel('Operation cancelled.');\n process.exit(0);\n },\n }\n );\n\n const type = project.type as keyof typeof templates;\n const name = project.name;\n let dir = project.dir;\n\n if (!dir || dir.trim() === '') {\n switch (type) {\n case 'controller': dir = 'src/controllers'; break;\n case 'middleware': dir = 'src/middleware'; break;\n case 'plugin': dir = 'src/plugins'; break;\n }\n }\n\n // Convert PascalCase to kebab-case for filename\n const kebabName = name.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();\n const fileName = `${kebabName}.ts`;\n\n const finalPath = path.join(process.cwd(), dir, fileName);\n\n // Ensure directory exists\n if (!fs.existsSync(path.dirname(finalPath))) {\n fs.mkdirSync(path.dirname(finalPath), { recursive: true });\n }\n\n // Check for overwrite\n if (fs.existsSync(finalPath)) {\n const overwrite = await p.confirm({\n message: `File ${finalPath} already exists. Overwrite?`,\n initialValue: false\n });\n\n if (p.isCancel(overwrite) || !overwrite) {\n p.cancel('Operation cancelled.');\n process.exit(0);\n }\n }\n\n const s = p.spinner();\n s.start(`Creating ${type}...`);\n\n await setTimeout(500); // Artificial delay to show spinner\n\n const content = templates[type](name);\n fs.writeFileSync(finalPath, content);\n\n s.stop(`Created ${type}`);\n\n const nextSteps = ` -> ${finalPath}\nMake sure to register it in your main application file if necessary.`;\n\n p.note(nextSteps, 'Next steps');\n\n p.outro(`Problems? Open an issue at https://github.com/dotglitch/express.ts`);\n}\n\nmain().catch(console.error);;\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"cli.js","sources":["../src/cli/index.ts"],"sourcesContent":["#!/usr/bin/env bun\nimport * as p from '@clack/prompts';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport { setTimeout } from 'node:timers/promises';\nimport { analyzeDirectory } from '../analysis/openapi-analyzer';\n\nconst templates = {\n controller: (name: string) => `import { Controller, Get, Ctx } from 'shokupan';\nimport { ShokupanContext } from 'shokupan';\n\n@Controller('/${name.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase()}')\nexport class ${name}Controller {\n @Get('/')\n public index(@Ctx() ctx: ShokupanContext) {\n return { message: 'Hello from ${name}Controller' };\n }\n}\n`,\n middleware: (name: string) => `import { ShokupanContext, NextFn } from 'shokupan';\n\n/**\n * ${name} Middleware\n */\nexport const ${name}Middleware = async (ctx: ShokupanContext, next: NextFn) => {\n // Before next\n // console.log('${name} Middleware - Request');\n\n const result = await next();\n\n // After next\n // console.log('${name} Middleware - Response');\n \n return result;\n};\n`,\n plugin: (name: string) => `import { ShokupanRouter } from 'shokupan';\nimport { ShokupanContext } from 'shokupan';\n\nexport interface ${name}Options {\n // Define options here\n}\n\nexport class ${name}Plugin extends ShokupanRouter {\n constructor(private options: ${name}Options = {}) {\n super();\n this.init();\n }\n\n private init() {\n this.get('/', (ctx: ShokupanContext) => {\n return { message: '${name} Plugin Active' };\n });\n }\n}\n`\n};\n\nasync function scaffold() {\n console.clear();\n p.intro(`Shokupan CLI Scaffolder`);\n\n // Check if running in a project root\n if (!fs.existsSync('package.json')) {\n p.note('Warning: No package.json found in current directory. Are you in the project root?');\n }\n\n const project = await p.group(\n {\n type: () => p.select({\n message: 'What do you want to scaffold?',\n options: [\n { value: 'controller', label: 'Controller' },\n { value: 'middleware', label: 'Middleware' },\n { value: 'plugin', label: 'Plugin' },\n ],\n }),\n name: () => p.text({\n message: 'Name (PascalCase, e.g. UserAuth):',\n validate: (value) => {\n if (!value) return 'Name is required';\n if (!/^[A-Z][a-zA-Z0-9]*$/.test(value)) return 'Please use PascalCase';\n return undefined;\n },\n }),\n dir: () => p.text({\n message: 'Output directory (leave empty for default):',\n placeholder: 'src/controllers',\n }),\n },\n {\n onCancel: () => {\n p.cancel('Operation cancelled.');\n process.exit(0);\n },\n }\n );\n\n const type = project.type as keyof typeof templates;\n const name = project.name;\n let dir = project.dir;\n\n if (!dir || dir.trim() === '') {\n switch (type) {\n case 'controller': dir = 'src/controllers'; break;\n case 'middleware': dir = 'src/middleware'; break;\n case 'plugin': dir = 'src/plugins'; break;\n }\n }\n\n // Convert PascalCase to kebab-case for filename\n const kebabName = name.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();\n const fileName = `${kebabName}.ts`;\n\n const finalPath = path.join(process.cwd(), dir, fileName);\n\n // Ensure directory exists\n if (!fs.existsSync(path.dirname(finalPath))) {\n fs.mkdirSync(path.dirname(finalPath), { recursive: true });\n }\n\n // Check for overwrite\n if (fs.existsSync(finalPath)) {\n const overwrite = await p.confirm({\n message: `File ${finalPath} already exists. Overwrite?`,\n initialValue: false\n });\n\n if (p.isCancel(overwrite) || !overwrite) {\n p.cancel('Operation cancelled.');\n process.exit(0);\n }\n }\n\n const s = p.spinner();\n s.start(`Creating ${type}...`);\n\n await setTimeout(500); // Artificial delay to show spinner\n\n const content = templates[type](name);\n fs.writeFileSync(finalPath, content);\n\n s.stop(`Created ${type}`);\n\n const nextSteps = ` -> ${finalPath}\nMake sure to register it in your main application file if necessary.`;\n\n p.note(nextSteps, 'Next steps');\n\n p.outro(`Problems? Open an issue at https://github.com/dotglitch/shokupan`);\n}\n\nasync function analyze() {\n console.clear();\n p.intro(`Shokupan OpenAPI Analyzer`);\n\n const args = process.argv.slice(2);\n let directory = process.cwd();\n let outputPath = 'openapi.json';\n\n // Parse command line arguments\n // analyze [directory] [--output file.json]\n const analyzeIndex = args.indexOf('analyze');\n if (analyzeIndex !== -1 && args.length > analyzeIndex + 1) {\n const nextArg = args[analyzeIndex + 1];\n if (!nextArg.startsWith('--')) {\n directory = path.resolve(nextArg);\n }\n }\n\n const outputIndex = args.indexOf('--output');\n if (outputIndex !== -1 && args.length > outputIndex + 1) {\n outputPath = args[outputIndex + 1];\n }\n\n // Verify directory exists\n if (!fs.existsSync(directory)) {\n p.cancel(`Directory not found: ${directory}`);\n process.exit(1);\n }\n\n const s = p.spinner();\n s.start(`Analyzing directory: ${directory}`);\n\n try {\n const spec = await analyzeDirectory(directory);\n\n s.stop('Analysis complete');\n\n // Write to file\n const fullOutputPath = path.resolve(outputPath);\n fs.writeFileSync(fullOutputPath, JSON.stringify(spec, null, 2));\n\n p.note(`OpenAPI spec written to: ${fullOutputPath}`, 'Success');\n\n // Show summary\n const pathCount = Object.keys(spec.paths || {}).length;\n p.note(`Found ${pathCount} unique paths`, 'Summary');\n\n p.outro('Done!');\n } catch (error: any) {\n s.stop('Analysis failed');\n p.cancel(`Error: ${error.message}`);\n console.error(error);\n process.exit(1);\n }\n}\n\nasync function main() {\n const args = process.argv.slice(2);\n const command = args[0];\n\n if (command === 'analyze') {\n await analyze();\n } else if (command === 'scaffold' || !command) {\n // Default to scaffold for backwards compatibility\n await scaffold();\n } else {\n console.log('Shokupan CLI');\n console.log('');\n console.log('Commands:');\n console.log(' scaffold (default) - Scaffold controllers, middleware, or plugins');\n console.log(' analyze <directory> - Analyze a Shokupan application and generate OpenAPI spec');\n console.log('');\n console.log('Usage:');\n console.log(' shokupan scaffold');\n console.log(' shokupan analyze <directory> [--output openapi.json]');\n process.exit(0);\n }\n}\n\nmain().catch(console.error);\n"],"names":[],"mappings":"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"}
package/dist/context.d.ts CHANGED
@@ -1,15 +1,27 @@
1
- import { BodyInit } from 'bun';
1
+ import { BodyInit, Server } from 'bun';
2
2
  import { ShokupanRequest } from './request';
3
3
  import { ShokupanResponse } from './response';
4
- import { CookieOptions } from './types';
4
+ import { Shokupan } from './shokupan';
5
+ import { CookieOptions, JSXRenderer } from './types';
5
6
  type HeadersInit = Headers | Record<string, string> | [string, string][];
7
+ export interface HandlerStackItem {
8
+ name: string;
9
+ file: string;
10
+ line: number;
11
+ stateChanges?: Record<string, any>;
12
+ }
6
13
  export declare class ShokupanContext<State extends Record<string, any> = Record<string, any>> {
7
14
  readonly request: ShokupanRequest<any>;
8
- readonly url: URL;
15
+ readonly server?: Server;
16
+ readonly app?: Shokupan;
17
+ private _url;
9
18
  params: Record<string, string>;
10
19
  state: State;
20
+ handlerStack: HandlerStackItem[];
11
21
  readonly response: ShokupanResponse;
12
- constructor(request: ShokupanRequest<any>, state?: State);
22
+ _finalResponse?: Response;
23
+ constructor(request: ShokupanRequest<any>, server?: Server, state?: State, app?: Shokupan, enableMiddlewareTracking?: boolean);
24
+ get url(): URL;
13
25
  /**
14
26
  * Base request
15
27
  */
@@ -21,23 +33,54 @@ export declare class ShokupanContext<State extends Record<string, any> = Record<
21
33
  /**
22
34
  * Request path
23
35
  */
24
- get path(): string;
36
+ get path(): any;
25
37
  /**
26
38
  * Request query params
27
39
  */
28
40
  get query(): {
29
41
  [k: string]: string;
30
42
  };
43
+ /**
44
+ * Client IP address
45
+ */
46
+ get ip(): Bun.SocketAddress;
47
+ /**
48
+ * Request hostname (e.g. "localhost")
49
+ */
50
+ get hostname(): string;
51
+ /**
52
+ * Request host (e.g. "localhost:3000")
53
+ */
54
+ get host(): string;
55
+ /**
56
+ * Request protocol (e.g. "http:", "https:")
57
+ */
58
+ get protocol(): string;
59
+ /**
60
+ * Whether request is secure (https)
61
+ */
62
+ get secure(): boolean;
63
+ /**
64
+ * Request origin (e.g. "http://localhost:3000")
65
+ */
66
+ get origin(): string;
31
67
  /**
32
68
  * Request headers
33
69
  */
34
70
  get headers(): any;
71
+ /**
72
+ * Get a request header
73
+ * @param name Header name
74
+ */
75
+ get(name: string): any;
35
76
  /**
36
77
  * Base response object
37
78
  */
38
79
  get res(): ShokupanResponse;
39
80
  /**
40
81
  * Helper to set a header on the response
82
+ * @param key Header key
83
+ * @param value Header value
41
84
  */
42
85
  set(key: string, value: string): this;
43
86
  /**
@@ -84,5 +127,16 @@ export declare class ShokupanContext<State extends Record<string, any> = Record<
84
127
  * Respond with a file
85
128
  */
86
129
  file(path: string, fileOptions?: BlobPropertyBag, responseOptions?: ResponseInit): Response;
130
+ /**
131
+ * JSX Rendering Function
132
+ */
133
+ renderer?: JSXRenderer;
134
+ /**
135
+ * Render a JSX element
136
+ * @param element JSX Element
137
+ * @param status HTTP Status
138
+ * @param headers HTTP Headers
139
+ */
140
+ jsx(element: any, args?: Parameters<JSXRenderer>[1], status?: number, headers?: HeadersInit): Promise<Response>;
87
141
  }
88
142
  export {};
@@ -1,4 +1,4 @@
1
- import { Middleware } from './types';
1
+ import { GuardAPISpec, MethodAPISpec, Middleware } from './types';
2
2
  /**
3
3
  * Class Decorator: Defines the base path for a controller.
4
4
  */
@@ -13,6 +13,10 @@ export declare const Query: (name?: string) => (target: any, propertyKey: string
13
13
  export declare const Headers: (name?: string) => (target: any, propertyKey: string, parameterIndex: number) => void;
14
14
  export declare const Req: (name?: string) => (target: any, propertyKey: string, parameterIndex: number) => void;
15
15
  export declare const Ctx: (name?: string) => (target: any, propertyKey: string, parameterIndex: number) => void;
16
+ /**
17
+ * Decorator: Overrides the OpenAPI specification for a route.
18
+ */
19
+ export declare function Spec(spec: MethodAPISpec | GuardAPISpec): (target: any, propertyKey: string, descriptor: PropertyDescriptor) => void;
16
20
  export declare const Get: (path?: string) => (target: any, propertyKey: string, descriptor: PropertyDescriptor) => void;
17
21
  export declare const Post: (path?: string) => (target: any, propertyKey: string, descriptor: PropertyDescriptor) => void;
18
22
  export declare const Put: (path?: string) => (target: any, propertyKey: string, descriptor: PropertyDescriptor) => void;