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.
- package/README.md +33 -3
- package/dist/adapters/vite/dev-server.d.ts +2 -0
- package/dist/adapters/vite/dev-server.d.ts.map +1 -1
- package/dist/adapters/vite/dev-server.js +34 -0
- package/dist/core/middleware/compose.test.d.ts +2 -0
- package/dist/core/middleware/compose.test.d.ts.map +1 -0
- package/dist/core/middleware/compose.test.js +177 -0
- package/dist/core/normalize/normalize-path.test.d.ts +2 -0
- package/dist/core/normalize/normalize-path.test.d.ts.map +1 -0
- package/dist/core/normalize/normalize-path.test.js +130 -0
- package/dist/core/openapi/generate.d.ts +65 -0
- package/dist/core/openapi/generate.d.ts.map +1 -0
- package/dist/core/openapi/generate.js +464 -0
- package/dist/core/routing/route-matcher.test.d.ts +2 -0
- package/dist/core/routing/route-matcher.test.d.ts.map +1 -0
- package/dist/core/routing/route-matcher.test.js +231 -0
- package/dist/core/routing/route-parser.test.d.ts +2 -0
- package/dist/core/routing/route-parser.test.d.ts.map +1 -0
- package/dist/core/routing/route-parser.test.js +142 -0
- package/dist/core/server/request-handler.d.ts.map +1 -1
- package/dist/core/server/request-handler.js +5 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/plugin.d.ts +3 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +3 -1
- package/dist/shared/errors.test.d.ts +2 -0
- package/dist/shared/errors.test.d.ts.map +1 -0
- package/dist/shared/errors.test.js +174 -0
- package/dist/shared/response-helpers.test.d.ts +2 -0
- package/dist/shared/response-helpers.test.d.ts.map +1 -0
- package/dist/shared/response-helpers.test.js +167 -0
- 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,
|
|
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 `
|
|
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
|
-
- [
|
|
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;
|
|
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 @@
|
|
|
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 @@
|
|
|
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"}
|