vitek-plugin 0.1.2-beta → 0.1.2-beta.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.
Files changed (33) hide show
  1. package/README.md +33 -3
  2. package/dist/adapters/vite/dev-server.d.ts +2 -0
  3. package/dist/adapters/vite/dev-server.d.ts.map +1 -1
  4. package/dist/adapters/vite/dev-server.js +34 -0
  5. package/dist/core/middleware/compose.test.d.ts +2 -0
  6. package/dist/core/middleware/compose.test.d.ts.map +1 -0
  7. package/dist/core/middleware/compose.test.js +177 -0
  8. package/dist/core/normalize/normalize-path.test.d.ts +2 -0
  9. package/dist/core/normalize/normalize-path.test.d.ts.map +1 -0
  10. package/dist/core/normalize/normalize-path.test.js +130 -0
  11. package/dist/core/openapi/generate.d.ts +65 -0
  12. package/dist/core/openapi/generate.d.ts.map +1 -0
  13. package/dist/core/openapi/generate.js +464 -0
  14. package/dist/core/routing/route-matcher.test.d.ts +2 -0
  15. package/dist/core/routing/route-matcher.test.d.ts.map +1 -0
  16. package/dist/core/routing/route-matcher.test.js +231 -0
  17. package/dist/core/routing/route-parser.test.d.ts +2 -0
  18. package/dist/core/routing/route-parser.test.d.ts.map +1 -0
  19. package/dist/core/routing/route-parser.test.js +142 -0
  20. package/dist/core/server/request-handler.d.ts.map +1 -1
  21. package/dist/core/server/request-handler.js +5 -2
  22. package/dist/index.d.ts +1 -0
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/plugin.d.ts +3 -0
  25. package/dist/plugin.d.ts.map +1 -1
  26. package/dist/plugin.js +3 -1
  27. package/dist/shared/errors.test.d.ts +2 -0
  28. package/dist/shared/errors.test.d.ts.map +1 -0
  29. package/dist/shared/errors.test.js +174 -0
  30. package/dist/shared/response-helpers.test.d.ts +2 -0
  31. package/dist/shared/response-helpers.test.d.ts.map +1 -0
  32. package/dist/shared/response-helpers.test.js +167 -0
  33. package/package.json +10 -5
package/README.md CHANGED
@@ -16,11 +16,11 @@
16
16
 
17
17
  Vitek is a Vite plugin that turns a folder of files into an HTTP API.
18
18
 
19
- **Note:** The API runs with the Vite **development server** (`npm run dev` / `pnpm dev`). For **production**, run `vite build` then **vitek-serve** (add `"start": "vitek-serve"` to your scripts and run `pnpm start`)—this serves both static assets and the API from one process. `vite preview` is for quick local preview of the static build only; for static + API use vitek-serve. Set `buildApi: false` if you do not want the API in build/production. Write endpoints as `[name].[method].ts` (or `.js`) under `src/api`, and get automatic routing, type generation, and typed client helpers.
19
+ **Note:** The API runs with the Vite **development server** (`npm run dev` / `pnpm dev`). For **production**, run `vite build` then **vitek-serve** (add `"start": "vitek-serve"` to your scripts and run `pnpm start`)—this serves both static assets and the API from one process. `vite preview` is for quick local preview of the static build only; for static + API use vitek-serve. Set `buildApi: false` if you do not want the API in build/production. Write endpoints as `[name].[method].ts` (or `.js`) under `src/api`, and get automatic routing, type generation, typed client helpers, and **OpenAPI/Swagger documentation**.
20
20
 
21
21
  **Full documentation:** [docs/](./docs/) · [View online](https://martinsbicudo.github.io/vitek-plugin/) (VitePress — run `npm run docs:dev` or `pnpm docs:dev` to view locally).
22
22
 
23
- **Examples:** [examples/](./examples/) — `basic-js`, `js-react`, `typescript-react`, and `docker`.
23
+ **Examples:** [examples/](./examples/) — `basic-js`, `js-react`, `typescript-react`, `docker`, and `openapi-docs`.
24
24
 
25
25
  ---
26
26
 
@@ -55,10 +55,40 @@ Then `npm run dev` and open `http://localhost:5173/api/health`.
55
55
 
56
56
  ---
57
57
 
58
+ ## OpenAPI / Swagger Documentation (New ✨)
59
+
60
+ Vitek can automatically generate OpenAPI 3.0 specifications and serve interactive Swagger UI documentation:
61
+
62
+ ```typescript
63
+ import { defineConfig } from "vite";
64
+ import { vitek } from "vitek-plugin";
65
+
66
+ export default defineConfig({
67
+ plugins: [
68
+ vitek({
69
+ openApi: true, // Enable with defaults
70
+ // or: openApi: { info: { title: "My API" } }
71
+ }),
72
+ ],
73
+ });
74
+ ```
75
+
76
+ Then open `http://localhost:5173/api-docs.html` for interactive API documentation.
77
+
78
+ - **Automatic generation** from your route files
79
+ - **JSDoc support** - Document with `@summary`, `@tag`, `@response`, etc.
80
+ - **Type extraction** - Body and Query types become schemas
81
+ - **Zero config required** - Works out of the box with sensible defaults
82
+
83
+ [Learn more →](./docs/guide/openapi.md)
84
+
85
+ ---
86
+
58
87
  ## Links
59
88
 
60
89
  - [Documentation](./docs/) — [view online](https://martinsbicudo.github.io/vitek-plugin/) · guides, API reference, configuration, examples
61
- - [Examples](./examples/) — basic-js, js-react, typescript-react, docker
90
+ - [OpenAPI/Swagger Guide](./docs/guide/openapi.md) — Auto-generate API documentation
91
+ - [Examples](./examples/) — basic-js, js-react, typescript-react, docker, openapi-docs
62
92
  - [GitHub](https://github.com/martinsbicudo/vitek-plugin)
63
93
  - [NPM](https://www.npmjs.com/package/vitek-plugin)
64
94
  - [License](LICENSE)
@@ -3,6 +3,7 @@
3
3
  * Thin layer that connects core → Vite
4
4
  */
5
5
  import type { ViteDevServer } from 'vite';
6
+ import type { OpenApiOptions } from '../../core/openapi/generate.js';
6
7
  import type { VitekLogger } from './logger.js';
7
8
  export interface ViteDevServerOptions {
8
9
  root: string;
@@ -10,6 +11,7 @@ export interface ViteDevServerOptions {
10
11
  logger: VitekLogger;
11
12
  viteServer: ViteDevServer;
12
13
  enableValidation?: boolean;
14
+ openApi?: OpenApiOptions | boolean;
13
15
  }
14
16
  /**
15
17
  * Creates middleware for Vite development server
@@ -1 +1 @@
1
- {"version":3,"file":"dev-server.d.ts","sourceRoot":"","sources":["../../../src/adapters/vite/dev-server.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,MAAM,CAAC;AAoB1C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE/C,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,WAAW,CAAC;IACpB,UAAU,EAAE,aAAa,CAAC;IAC1B,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAsPD;;GAEG;AACH,wBAAgB,6BAA6B,CAAC,OAAO,EAAE,oBAAoB;;;;EAe1E"}
1
+ {"version":3,"file":"dev-server.d.ts","sourceRoot":"","sources":["../../../src/adapters/vite/dev-server.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,MAAM,CAAC;AAmB1C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAC;AAGrE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE/C,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,WAAW,CAAC;IACpB,UAAU,EAAE,aAAa,CAAC;IAC1B,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,OAAO,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC;CACpC;AA6RD;;GAEG;AACH,wBAAgB,6BAA6B,CAAC,OAAO,EAAE,oBAAoB;;;;EAe1E"}
@@ -17,6 +17,7 @@ import { createRoute } from '../../core/routing/route-parser.js';
17
17
  import { createRequestHandler } from '../../core/server/request-handler.js';
18
18
  import { routesToSchema } from '../../core/types/schema.js';
19
19
  import { generateTypesFile, generateServicesFile } from '../../core/types/generate.js';
20
+ import { generateOpenApiFile, generateSwaggerUiHtml } from '../../core/openapi/generate.js';
20
21
  import { API_BASE_PATH, GENERATED_TYPES_FILE, GENERATED_SERVICES_FILE } from '../../shared/constants.js';
21
22
  /**
22
23
  * Development server state
@@ -128,11 +129,44 @@ class DevServerState {
128
129
  await generateServicesFile(servicesPath, schema, API_BASE_PATH, isTypeScript);
129
130
  const relativeServicesPath = path.relative(this.options.root, servicesPath);
130
131
  this.options.logger.servicesGenerated(`./${relativeServicesPath.replace(/\\/g, '/')}`);
132
+ // Generate OpenAPI spec if enabled
133
+ await this.generateOpenApi();
131
134
  }
132
135
  catch (error) {
133
136
  this.options.logger.error(`Failed to generate types: ${error instanceof Error ? error.message : String(error)}`);
134
137
  }
135
138
  }
139
+ /**
140
+ * Generates OpenAPI specification and Swagger UI
141
+ */
142
+ async generateOpenApi() {
143
+ const { openApi } = this.options;
144
+ if (!openApi) {
145
+ return;
146
+ }
147
+ try {
148
+ const openApiOptions = typeof openApi === 'boolean'
149
+ ? {
150
+ apiBasePath: API_BASE_PATH,
151
+ }
152
+ : { ...openApi, apiBasePath: API_BASE_PATH };
153
+ // Generate openapi.json
154
+ const openApiPath = path.join(this.options.root, 'public', 'openapi.json');
155
+ await generateOpenApiFile(openApiPath, this.routes, openApiOptions);
156
+ const relativeOpenApiPath = path.relative(this.options.root, openApiPath);
157
+ this.options.logger.info(`OpenAPI spec generated: ./${relativeOpenApiPath.replace(/\\/g, '/')}`);
158
+ // Generate Swagger UI HTML
159
+ const swaggerUiPath = path.join(this.options.root, 'public', 'api-docs.html');
160
+ const title = openApiOptions.info?.title || 'Vitek API';
161
+ const swaggerHtml = generateSwaggerUiHtml('/openapi.json', title);
162
+ fs.writeFileSync(swaggerUiPath, swaggerHtml, 'utf-8');
163
+ const relativeSwaggerPath = path.relative(this.options.root, swaggerUiPath);
164
+ this.options.logger.info(`Swagger UI available at: ./${relativeSwaggerPath.replace(/\\/g, '/')} → http://localhost:${this.options.viteServer.config.server?.port || 5173}/api-docs.html`);
165
+ }
166
+ catch (error) {
167
+ this.options.logger.warn(`Failed to generate OpenAPI spec: ${error instanceof Error ? error.message : String(error)}`);
168
+ }
169
+ }
136
170
  /**
137
171
  * Cleans up resources
138
172
  */
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=compose.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compose.test.d.ts","sourceRoot":"","sources":["../../../src/core/middleware/compose.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,177 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { compose } from './compose.js';
3
+ function createMockContext() {
4
+ return {
5
+ url: '/test',
6
+ method: 'get',
7
+ path: '/test',
8
+ query: {},
9
+ params: {},
10
+ headers: {},
11
+ };
12
+ }
13
+ describe('compose', () => {
14
+ it('should execute middleware in order', async () => {
15
+ const order = [];
16
+ const middleware1 = async (_ctx, next) => {
17
+ order.push('middleware1-before');
18
+ await next();
19
+ order.push('middleware1-after');
20
+ };
21
+ const middleware2 = async (_ctx, next) => {
22
+ order.push('middleware2-before');
23
+ await next();
24
+ order.push('middleware2-after');
25
+ };
26
+ const handler = async () => {
27
+ order.push('handler');
28
+ };
29
+ const composed = compose([middleware1, middleware2]);
30
+ await composed(createMockContext(), handler);
31
+ expect(order).toEqual([
32
+ 'middleware1-before',
33
+ 'middleware2-before',
34
+ 'handler',
35
+ 'middleware2-after',
36
+ 'middleware1-after',
37
+ ]);
38
+ });
39
+ it('should execute handler when no middlewares', async () => {
40
+ const handler = vi.fn().mockResolvedValue(undefined);
41
+ const composed = compose([]);
42
+ await composed(createMockContext(), handler);
43
+ expect(handler).toHaveBeenCalledTimes(1);
44
+ });
45
+ it('should pass context to all middlewares', async () => {
46
+ const context = createMockContext();
47
+ const middleware1 = vi.fn().mockImplementation((_ctx, next) => next());
48
+ const middleware2 = vi.fn().mockImplementation((_ctx, next) => next());
49
+ const handler = vi.fn().mockResolvedValue(undefined);
50
+ const composed = compose([middleware1, middleware2]);
51
+ await composed(context, handler);
52
+ expect(middleware1).toHaveBeenCalledWith(context, expect.any(Function));
53
+ expect(middleware2).toHaveBeenCalledWith(context, expect.any(Function));
54
+ });
55
+ it('should allow middleware to modify context', async () => {
56
+ const context = createMockContext();
57
+ const middleware = async (ctx, next) => {
58
+ ctx.customProperty = 'modified';
59
+ await next();
60
+ };
61
+ const handler = vi.fn().mockImplementation(() => {
62
+ expect(context.customProperty).toBe('modified');
63
+ });
64
+ const composed = compose([middleware]);
65
+ await composed(context, handler);
66
+ expect(handler).toHaveBeenCalled();
67
+ });
68
+ it('should handle async middlewares', async () => {
69
+ const order = [];
70
+ const middleware = async (_ctx, next) => {
71
+ await new Promise((resolve) => setTimeout(resolve, 10));
72
+ order.push('middleware');
73
+ await next();
74
+ };
75
+ const handler = async () => {
76
+ await new Promise((resolve) => setTimeout(resolve, 10));
77
+ order.push('handler');
78
+ };
79
+ const composed = compose([middleware]);
80
+ await composed(createMockContext(), handler);
81
+ expect(order).toEqual(['middleware', 'handler']);
82
+ });
83
+ it('should handle errors in middleware', async () => {
84
+ const error = new Error('Middleware error');
85
+ const middleware = async () => {
86
+ throw error;
87
+ };
88
+ const composed = compose([middleware]);
89
+ await expect(composed(createMockContext(), vi.fn())).rejects.toThrow('Middleware error');
90
+ });
91
+ it('should handle errors in handler', async () => {
92
+ const error = new Error('Handler error');
93
+ const middleware = async (_ctx, next) => {
94
+ await next();
95
+ };
96
+ const composed = compose([middleware]);
97
+ await expect(composed(createMockContext(), () => Promise.reject(error))).rejects.toThrow('Handler error');
98
+ });
99
+ it('should throw when next() is called multiple times', async () => {
100
+ const badMiddleware = async (_ctx, next) => {
101
+ await next();
102
+ await next(); // Should throw
103
+ };
104
+ const composed = compose([badMiddleware]);
105
+ await expect(composed(createMockContext(), vi.fn())).rejects.toThrow('next() called multiple times');
106
+ });
107
+ it('should short-circuit when middleware does not call next', async () => {
108
+ const order = [];
109
+ const middleware1 = async (_ctx, _next) => {
110
+ order.push('middleware1');
111
+ // Does not call next
112
+ };
113
+ const middleware2 = async (_ctx, next) => {
114
+ order.push('middleware2');
115
+ await next();
116
+ };
117
+ const handler = vi.fn().mockResolvedValue(undefined);
118
+ const composed = compose([middleware1, middleware2]);
119
+ await composed(createMockContext(), handler);
120
+ expect(order).toEqual(['middleware1']);
121
+ expect(handler).not.toHaveBeenCalled();
122
+ });
123
+ it('should support multiple middlewares with different patterns', async () => {
124
+ const results = [];
125
+ const authMiddleware = async (ctx, next) => {
126
+ results.push('auth-check');
127
+ ctx.user = { id: '123' };
128
+ await next();
129
+ };
130
+ const loggingMiddleware = async (ctx, next) => {
131
+ results.push('log-start');
132
+ await next();
133
+ results.push('log-end');
134
+ };
135
+ const validationMiddleware = async (ctx, next) => {
136
+ results.push('validate');
137
+ await next();
138
+ };
139
+ const handler = async () => {
140
+ results.push('handler');
141
+ };
142
+ const composed = compose([authMiddleware, loggingMiddleware, validationMiddleware]);
143
+ await composed(createMockContext(), handler);
144
+ expect(results).toEqual([
145
+ 'auth-check',
146
+ 'log-start',
147
+ 'validate',
148
+ 'handler',
149
+ 'log-end',
150
+ ]);
151
+ });
152
+ it('should allow middleware to catch errors from downstream', async () => {
153
+ const error = new Error('Downstream error');
154
+ const caughtError = [];
155
+ const errorHandler = async (_ctx, next) => {
156
+ try {
157
+ await next();
158
+ }
159
+ catch (err) {
160
+ caughtError.push(err);
161
+ }
162
+ };
163
+ const composed = compose([errorHandler]);
164
+ await composed(createMockContext(), () => Promise.reject(error));
165
+ expect(caughtError).toHaveLength(1);
166
+ expect(caughtError[0].message).toBe('Downstream error');
167
+ });
168
+ it('should execute handler exactly once', async () => {
169
+ const handler = vi.fn().mockResolvedValue(undefined);
170
+ const middleware = async (_ctx, next) => {
171
+ await next();
172
+ };
173
+ const composed = compose([middleware, middleware, middleware]);
174
+ await composed(createMockContext(), handler);
175
+ expect(handler).toHaveBeenCalledTimes(1);
176
+ });
177
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=normalize-path.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"normalize-path.test.d.ts","sourceRoot":"","sources":["../../../src/core/normalize/normalize-path.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,130 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { normalizeRoutePath, extractParamsFromPattern, patternToRegex, } from './normalize-path.js';
3
+ import { normalizePath } from '../../shared/utils.js';
4
+ describe('normalizeRoutePath', () => {
5
+ it('should convert [id] to :id', () => {
6
+ expect(normalizeRoutePath('users/[id]')).toBe('users/:id');
7
+ });
8
+ it('should convert [...ids] to *ids', () => {
9
+ expect(normalizeRoutePath('files/[...path]')).toBe('files/*path');
10
+ });
11
+ it('should handle multiple parameters', () => {
12
+ expect(normalizeRoutePath('users/[userId]/posts/[postId]')).toBe('users/:userId/posts/:postId');
13
+ });
14
+ it('should handle index files', () => {
15
+ expect(normalizeRoutePath('users/index')).toBe('users');
16
+ });
17
+ it('should handle root index', () => {
18
+ expect(normalizeRoutePath('index')).toBe('');
19
+ });
20
+ it('should remove file extensions', () => {
21
+ expect(normalizeRoutePath('users.ts')).toBe('users');
22
+ expect(normalizeRoutePath('users.js')).toBe('users');
23
+ });
24
+ it('should handle mixed parameters and static segments', () => {
25
+ expect(normalizeRoutePath('api/v1/users/[id]/profile')).toBe('api/v1/users/:id/profile');
26
+ });
27
+ it('should handle empty string', () => {
28
+ expect(normalizeRoutePath('')).toBe('');
29
+ });
30
+ it('should handle Windows-style separators', () => {
31
+ expect(normalizeRoutePath('users\\[id]')).toBe('users/:id');
32
+ });
33
+ it('should remove leading/trailing slashes', () => {
34
+ expect(normalizeRoutePath('/users/[id]/')).toBe('users/:id');
35
+ });
36
+ });
37
+ describe('extractParamsFromPattern', () => {
38
+ it('should extract single parameter', () => {
39
+ expect(extractParamsFromPattern('users/:id')).toEqual(['id']);
40
+ });
41
+ it('should extract multiple parameters', () => {
42
+ expect(extractParamsFromPattern('users/:userId/posts/:postId')).toEqual(['userId', 'postId']);
43
+ });
44
+ it('should extract catch-all parameter', () => {
45
+ expect(extractParamsFromPattern('files/*path')).toEqual(['path']);
46
+ });
47
+ it('should return empty array for no parameters', () => {
48
+ expect(extractParamsFromPattern('health')).toEqual([]);
49
+ });
50
+ it('should extract mixed parameters', () => {
51
+ expect(extractParamsFromPattern('users/:id/files/*path')).toEqual(['id', 'path']);
52
+ });
53
+ it('should handle empty string', () => {
54
+ expect(extractParamsFromPattern('')).toEqual([]);
55
+ });
56
+ });
57
+ describe('patternToRegex', () => {
58
+ it('should match exact path', () => {
59
+ const regex = patternToRegex('health');
60
+ expect(regex.test('/health')).toBe(true);
61
+ expect(regex.test('/health/')).toBe(false);
62
+ expect(regex.test('/healthcheck')).toBe(false);
63
+ });
64
+ it('should match path with parameter', () => {
65
+ const regex = patternToRegex('users/:id');
66
+ expect(regex.test('/users/123')).toBe(true);
67
+ expect(regex.test('/users/abc')).toBe(true);
68
+ expect(regex.test('/users')).toBe(false);
69
+ expect(regex.test('/users/')).toBe(false);
70
+ });
71
+ it('should capture parameter value', () => {
72
+ const regex = patternToRegex('users/:id');
73
+ const match = '/users/123'.match(regex);
74
+ expect(match?.[1]).toBe('123');
75
+ });
76
+ it('should match catch-all parameter', () => {
77
+ const regex = patternToRegex('files/*path');
78
+ expect(regex.test('/files/docs/report.pdf')).toBe(true);
79
+ expect(regex.test('/files')).toBe(false);
80
+ });
81
+ it('should capture catch-all value', () => {
82
+ const regex = patternToRegex('files/*path');
83
+ const match = '/files/docs/folder/file.txt'.match(regex);
84
+ expect(match?.[1]).toBe('docs/folder/file.txt');
85
+ });
86
+ it('should match multiple parameters', () => {
87
+ const regex = patternToRegex('users/:userId/posts/:postId');
88
+ const match = '/users/42/posts/99'.match(regex);
89
+ expect(match).not.toBeNull();
90
+ expect(match?.[1]).toBe('42');
91
+ expect(match?.[2]).toBe('99');
92
+ });
93
+ it('should handle special characters in parameters', () => {
94
+ const regex = patternToRegex('files/:filename');
95
+ expect(regex.test('/files/doc.pdf')).toBe(true);
96
+ expect(regex.test('/files/image_v2.png')).toBe(true);
97
+ });
98
+ it('should not match partial segments', () => {
99
+ const regex = patternToRegex('users/:id');
100
+ expect(regex.test('/users/123/profile')).toBe(false);
101
+ });
102
+ it('should handle empty pattern', () => {
103
+ const regex = patternToRegex('');
104
+ expect(regex.test('/')).toBe(true);
105
+ expect(regex.test('/anything')).toBe(false);
106
+ });
107
+ it('should anchor to start and end', () => {
108
+ const regex = patternToRegex('users');
109
+ expect(regex.test('/users')).toBe(true);
110
+ expect(regex.test('/api/users')).toBe(false);
111
+ expect(regex.test('/users/api')).toBe(false);
112
+ });
113
+ });
114
+ describe('normalizePath (from utils)', () => {
115
+ it('should normalize double slashes', () => {
116
+ expect(normalizePath('users//profile')).toBe('users/profile');
117
+ });
118
+ it('should remove trailing slash', () => {
119
+ expect(normalizePath('/users/')).toBe('/users');
120
+ });
121
+ it('should handle root path', () => {
122
+ expect(normalizePath('/')).toBe('/');
123
+ });
124
+ it('should handle empty string', () => {
125
+ expect(normalizePath('')).toBe('/');
126
+ });
127
+ it('should not modify already normalized paths', () => {
128
+ expect(normalizePath('/api/v1/users')).toBe('/api/v1/users');
129
+ });
130
+ });
@@ -0,0 +1,65 @@
1
+ /**
2
+ * OpenAPI/Swagger specification generation
3
+ * Core logic - no Vite dependencies
4
+ */
5
+ import type { Route } from '../routing/route-types.js';
6
+ /**
7
+ * OpenAPI Info object
8
+ */
9
+ export interface OpenApiInfo {
10
+ title: string;
11
+ version: string;
12
+ description?: string;
13
+ }
14
+ /**
15
+ * OpenAPI Server object
16
+ */
17
+ export interface OpenApiServer {
18
+ url: string;
19
+ description?: string;
20
+ }
21
+ /**
22
+ * OpenAPI specification options
23
+ */
24
+ export interface OpenApiOptions {
25
+ /** API information. Uses defaults if not provided. */
26
+ info?: OpenApiInfo;
27
+ /** Server URLs for the API. Defaults to current URL if not provided. */
28
+ servers?: OpenApiServer[];
29
+ /** Base path for API routes. Defaults to "/api". */
30
+ apiBasePath?: string;
31
+ }
32
+ /**
33
+ * JSDoc metadata extracted from a route file
34
+ */
35
+ export interface RouteMetadata {
36
+ summary?: string;
37
+ description?: string;
38
+ tags?: string[];
39
+ deprecated?: boolean;
40
+ responses?: Record<string, ResponseMetadata>;
41
+ bodyDescription?: string;
42
+ queryDescription?: string;
43
+ paramDescriptions?: Record<string, string>;
44
+ }
45
+ /**
46
+ * Response metadata from JSDoc
47
+ */
48
+ export interface ResponseMetadata {
49
+ description: string;
50
+ type?: string;
51
+ example?: unknown;
52
+ }
53
+ /**
54
+ * Generates a complete OpenAPI 3.0 specification
55
+ */
56
+ export declare function generateOpenApiSpec(routes: Route[], options: OpenApiOptions): object;
57
+ /**
58
+ * Generates the OpenAPI JSON file
59
+ */
60
+ export declare function generateOpenApiFile(outputPath: string, routes: Route[], options: OpenApiOptions): Promise<void>;
61
+ /**
62
+ * Generates Swagger UI HTML
63
+ */
64
+ export declare function generateSwaggerUiHtml(apiJsonPath: string, title?: string): string;
65
+ //# sourceMappingURL=generate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"generate.d.ts","sourceRoot":"","sources":["../../../src/core/openapi/generate.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,2BAA2B,CAAC;AAEvD;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,sDAAsD;IACtD,IAAI,CAAC,EAAE,WAAW,CAAC;IACnB,wEAAwE;IACxE,OAAO,CAAC,EAAE,aAAa,EAAE,CAAC;IAC1B,oDAAoD;IACpD,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;IAC7C,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,iBAAiB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC5C;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AASD;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,OAAO,EAAE,cAAc,GAAG,MAAM,CAiBpF;AAgdD;;GAEG;AACH,wBAAsB,mBAAmB,CACvC,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,KAAK,EAAE,EACf,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,IAAI,CAAC,CAUf;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,GAAE,MAA4B,GAAG,MAAM,CAwBtG"}