vitek-plugin 0.1.0-beta
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/LICENSE +21 -0
- package/README.md +908 -0
- package/dist/adapters/vite/dev-server.d.ts +23 -0
- package/dist/adapters/vite/dev-server.d.ts.map +1 -0
- package/dist/adapters/vite/dev-server.js +428 -0
- package/dist/adapters/vite/logger.d.ts +33 -0
- package/dist/adapters/vite/logger.d.ts.map +1 -0
- package/dist/adapters/vite/logger.js +112 -0
- package/dist/core/context/create-context.d.ts +39 -0
- package/dist/core/context/create-context.d.ts.map +1 -0
- package/dist/core/context/create-context.js +34 -0
- package/dist/core/file-system/scan-api-dir.d.ts +26 -0
- package/dist/core/file-system/scan-api-dir.d.ts.map +1 -0
- package/dist/core/file-system/scan-api-dir.js +83 -0
- package/dist/core/file-system/watch-api-dir.d.ts +18 -0
- package/dist/core/file-system/watch-api-dir.d.ts.map +1 -0
- package/dist/core/file-system/watch-api-dir.js +40 -0
- package/dist/core/middleware/compose.d.ts +12 -0
- package/dist/core/middleware/compose.d.ts.map +1 -0
- package/dist/core/middleware/compose.js +27 -0
- package/dist/core/middleware/get-applicable-middlewares.d.ts +17 -0
- package/dist/core/middleware/get-applicable-middlewares.d.ts.map +1 -0
- package/dist/core/middleware/get-applicable-middlewares.js +47 -0
- package/dist/core/middleware/load-global.d.ts +13 -0
- package/dist/core/middleware/load-global.d.ts.map +1 -0
- package/dist/core/middleware/load-global.js +37 -0
- package/dist/core/normalize/normalize-path.d.ts +25 -0
- package/dist/core/normalize/normalize-path.d.ts.map +1 -0
- package/dist/core/normalize/normalize-path.js +76 -0
- package/dist/core/routing/route-matcher.d.ts +10 -0
- package/dist/core/routing/route-matcher.d.ts.map +1 -0
- package/dist/core/routing/route-matcher.js +32 -0
- package/dist/core/routing/route-parser.d.ts +28 -0
- package/dist/core/routing/route-parser.d.ts.map +1 -0
- package/dist/core/routing/route-parser.js +52 -0
- package/dist/core/routing/route-types.d.ts +43 -0
- package/dist/core/routing/route-types.d.ts.map +1 -0
- package/dist/core/routing/route-types.js +5 -0
- package/dist/core/types/extract-ast.d.ts +18 -0
- package/dist/core/types/extract-ast.d.ts.map +1 -0
- package/dist/core/types/extract-ast.js +26 -0
- package/dist/core/types/generate.d.ts +22 -0
- package/dist/core/types/generate.d.ts.map +1 -0
- package/dist/core/types/generate.js +576 -0
- package/dist/core/types/schema.d.ts +21 -0
- package/dist/core/types/schema.d.ts.map +1 -0
- package/dist/core/types/schema.js +17 -0
- package/dist/core/validation/types.d.ts +27 -0
- package/dist/core/validation/types.d.ts.map +1 -0
- package/dist/core/validation/types.js +5 -0
- package/dist/core/validation/validator.d.ts +22 -0
- package/dist/core/validation/validator.d.ts.map +1 -0
- package/dist/core/validation/validator.js +131 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/plugin.d.ts +27 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +54 -0
- package/dist/shared/constants.d.ts +13 -0
- package/dist/shared/constants.d.ts.map +1 -0
- package/dist/shared/constants.js +12 -0
- package/dist/shared/errors.d.ts +75 -0
- package/dist/shared/errors.d.ts.map +1 -0
- package/dist/shared/errors.js +118 -0
- package/dist/shared/response-helpers.d.ts +61 -0
- package/dist/shared/response-helpers.d.ts.map +1 -0
- package/dist/shared/response-helpers.js +100 -0
- package/dist/shared/utils.d.ts +17 -0
- package/dist/shared/utils.d.ts.map +1 -0
- package/dist/shared/utils.js +27 -0
- package/package.json +48 -0
|
@@ -0,0 +1,576 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypeScript type generation for the API
|
|
3
|
+
* Core logic - no Vite dependencies
|
|
4
|
+
*/
|
|
5
|
+
import * as fs from 'fs';
|
|
6
|
+
import * as path from 'path';
|
|
7
|
+
import { capitalize } from '../../shared/utils.js';
|
|
8
|
+
/**
|
|
9
|
+
* Generates the content of the generated types file
|
|
10
|
+
*/
|
|
11
|
+
export function generateTypesContent(routes, apiBasePath) {
|
|
12
|
+
const imports = `// Auto-generated by Vitek - DO NOT EDIT
|
|
13
|
+
// This file is automatically generated from your API routes
|
|
14
|
+
|
|
15
|
+
export type VitekParams = Record<string, string>;
|
|
16
|
+
export type VitekQuery = Record<string, string | string[]>;
|
|
17
|
+
`;
|
|
18
|
+
// Generate unique names for all types (Params, Body, Query)
|
|
19
|
+
const typeNameMaps = generateUniqueTypeNames(routes);
|
|
20
|
+
const paramTypeNameMap = typeNameMaps.params;
|
|
21
|
+
const bodyTypeNameMap = typeNameMaps.body;
|
|
22
|
+
const queryTypeNameMap = typeNameMaps.query;
|
|
23
|
+
// Generate body types for routes that have body type declared
|
|
24
|
+
const bodyTypes = routes
|
|
25
|
+
.filter(route => route.bodyType)
|
|
26
|
+
.map(route => {
|
|
27
|
+
const routeKey = `${route.method}:${route.pattern}`;
|
|
28
|
+
const bodyTypeName = bodyTypeNameMap.get(routeKey);
|
|
29
|
+
// route.bodyType was already checked in the filter above, so it's not undefined
|
|
30
|
+
const bodyType = route.bodyType;
|
|
31
|
+
// If bodyType already starts with '{', it means it's an extracted interface
|
|
32
|
+
// If not, it's a type alias, so use it directly
|
|
33
|
+
const bodyTypeDef = bodyType.trim().startsWith('{')
|
|
34
|
+
? bodyType
|
|
35
|
+
: bodyType;
|
|
36
|
+
return `export type ${bodyTypeName} = ${bodyTypeDef};`;
|
|
37
|
+
})
|
|
38
|
+
.filter((type, index, arr) => {
|
|
39
|
+
// Remove duplicates based on type name
|
|
40
|
+
const typeName = type.match(/^export type (\w+)/)?.[1];
|
|
41
|
+
return typeName && arr.findIndex(t => t.match(/^export type (\w+)/)?.[1] === typeName) === index;
|
|
42
|
+
})
|
|
43
|
+
.join('\n\n');
|
|
44
|
+
// Generate query types for routes that have query type declared
|
|
45
|
+
const queryTypes = routes
|
|
46
|
+
.filter(route => route.queryType)
|
|
47
|
+
.map(route => {
|
|
48
|
+
const routeKey = `${route.method}:${route.pattern}`;
|
|
49
|
+
const queryTypeName = queryTypeNameMap.get(routeKey);
|
|
50
|
+
// route.queryType was already checked in the filter above, so it's not undefined
|
|
51
|
+
const queryType = route.queryType;
|
|
52
|
+
const queryTypeDef = queryType.trim().startsWith('{')
|
|
53
|
+
? queryType
|
|
54
|
+
: queryType;
|
|
55
|
+
return `export type ${queryTypeName} = ${queryTypeDef};`;
|
|
56
|
+
})
|
|
57
|
+
.filter((type, index, arr) => {
|
|
58
|
+
// Remove duplicates based on type name
|
|
59
|
+
const typeName = type.match(/^export type (\w+)/)?.[1];
|
|
60
|
+
return typeName && arr.findIndex(t => t.match(/^export type (\w+)/)?.[1] === typeName) === index;
|
|
61
|
+
})
|
|
62
|
+
.join('\n\n');
|
|
63
|
+
// Generate types for each route (params)
|
|
64
|
+
const routeTypes = routes.map(route => {
|
|
65
|
+
const routeKey = `${route.method}:${route.pattern}`;
|
|
66
|
+
const typeName = paramTypeNameMap.get(routeKey);
|
|
67
|
+
const paramsType = generateParamsType(route.params);
|
|
68
|
+
return `export interface ${typeName} extends VitekParams {
|
|
69
|
+
${paramsType}
|
|
70
|
+
}`;
|
|
71
|
+
})
|
|
72
|
+
.filter((type, index, arr) => {
|
|
73
|
+
// Remove duplicates based on type name
|
|
74
|
+
const typeName = type.match(/^export interface (\w+)/)?.[1];
|
|
75
|
+
return typeName && arr.findIndex(t => t.match(/^export interface (\w+)/)?.[1] === typeName) === index;
|
|
76
|
+
})
|
|
77
|
+
.join('\n\n');
|
|
78
|
+
// Generate a type union with all routes
|
|
79
|
+
const routeDefinitions = routes.map(route => {
|
|
80
|
+
const routeKey = `${route.method}:${route.pattern}`;
|
|
81
|
+
const typeName = paramTypeNameMap.get(routeKey);
|
|
82
|
+
return ` | {
|
|
83
|
+
pattern: '${route.pattern}';
|
|
84
|
+
method: '${route.method}';
|
|
85
|
+
params: ${typeName};
|
|
86
|
+
}`;
|
|
87
|
+
}).join('\n');
|
|
88
|
+
const unionType = `export type VitekRoute =${routeDefinitions.length > 0 ? '\n' + routeDefinitions : ' never'};`;
|
|
89
|
+
return `${imports}
|
|
90
|
+
${bodyTypes ? '\n' + bodyTypes + '\n' : ''}${queryTypes ? '\n' + queryTypes + '\n' : ''}
|
|
91
|
+
${routeTypes}
|
|
92
|
+
|
|
93
|
+
${unionType}
|
|
94
|
+
|
|
95
|
+
export const API_BASE_PATH = '${apiBasePath}' as const;
|
|
96
|
+
`;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Generates unique names for all types (Params, Body, Query)
|
|
100
|
+
* Ensures there are no collisions within and between categories
|
|
101
|
+
*/
|
|
102
|
+
function generateUniqueTypeNames(routes) {
|
|
103
|
+
const paramNameMap = new Map();
|
|
104
|
+
const bodyNameMap = new Map();
|
|
105
|
+
const queryNameMap = new Map();
|
|
106
|
+
// All names already used (to ensure uniqueness between categories as well)
|
|
107
|
+
const allUsedNames = new Set();
|
|
108
|
+
// Process Params types
|
|
109
|
+
const paramGroups = new Map();
|
|
110
|
+
routes.forEach(route => {
|
|
111
|
+
const baseName = generateTypeNameBase(route, 'Params');
|
|
112
|
+
if (!paramGroups.has(baseName)) {
|
|
113
|
+
paramGroups.set(baseName, []);
|
|
114
|
+
}
|
|
115
|
+
paramGroups.get(baseName).push(route);
|
|
116
|
+
});
|
|
117
|
+
routes.forEach(route => {
|
|
118
|
+
const routeKey = `${route.method}:${route.pattern}`;
|
|
119
|
+
const baseName = generateTypeNameBase(route, 'Params');
|
|
120
|
+
const conflictingRoutes = paramGroups.get(baseName);
|
|
121
|
+
let uniqueName;
|
|
122
|
+
if (conflictingRoutes.length === 1) {
|
|
123
|
+
uniqueName = baseName;
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
uniqueName = generateUniqueTypeName(route, baseName, conflictingRoutes, allUsedNames, 'Params');
|
|
127
|
+
}
|
|
128
|
+
paramNameMap.set(routeKey, uniqueName);
|
|
129
|
+
allUsedNames.add(uniqueName);
|
|
130
|
+
});
|
|
131
|
+
// Process Body types
|
|
132
|
+
const bodyGroups = new Map();
|
|
133
|
+
routes.filter(r => r.bodyType).forEach(route => {
|
|
134
|
+
const baseName = generateTypeNameBase(route, 'Body');
|
|
135
|
+
if (!bodyGroups.has(baseName)) {
|
|
136
|
+
bodyGroups.set(baseName, []);
|
|
137
|
+
}
|
|
138
|
+
bodyGroups.get(baseName).push(route);
|
|
139
|
+
});
|
|
140
|
+
routes.filter(r => r.bodyType).forEach(route => {
|
|
141
|
+
const routeKey = `${route.method}:${route.pattern}`;
|
|
142
|
+
const baseName = generateTypeNameBase(route, 'Body');
|
|
143
|
+
const conflictingRoutes = bodyGroups.get(baseName);
|
|
144
|
+
let uniqueName;
|
|
145
|
+
if (conflictingRoutes.length === 1) {
|
|
146
|
+
uniqueName = baseName;
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
uniqueName = generateUniqueTypeName(route, baseName, conflictingRoutes, allUsedNames, 'Body');
|
|
150
|
+
}
|
|
151
|
+
bodyNameMap.set(routeKey, uniqueName);
|
|
152
|
+
allUsedNames.add(uniqueName);
|
|
153
|
+
});
|
|
154
|
+
// Process Query types
|
|
155
|
+
const queryGroups = new Map();
|
|
156
|
+
routes.filter(r => r.queryType).forEach(route => {
|
|
157
|
+
const baseName = generateTypeNameBase(route, 'Query');
|
|
158
|
+
if (!queryGroups.has(baseName)) {
|
|
159
|
+
queryGroups.set(baseName, []);
|
|
160
|
+
}
|
|
161
|
+
queryGroups.get(baseName).push(route);
|
|
162
|
+
});
|
|
163
|
+
routes.filter(r => r.queryType).forEach(route => {
|
|
164
|
+
const routeKey = `${route.method}:${route.pattern}`;
|
|
165
|
+
const baseName = generateTypeNameBase(route, 'Query');
|
|
166
|
+
const conflictingRoutes = queryGroups.get(baseName);
|
|
167
|
+
let uniqueName;
|
|
168
|
+
if (conflictingRoutes.length === 1) {
|
|
169
|
+
uniqueName = baseName;
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
uniqueName = generateUniqueTypeName(route, baseName, conflictingRoutes, allUsedNames, 'Query');
|
|
173
|
+
}
|
|
174
|
+
queryNameMap.set(routeKey, uniqueName);
|
|
175
|
+
allUsedNames.add(uniqueName);
|
|
176
|
+
});
|
|
177
|
+
return {
|
|
178
|
+
params: paramNameMap,
|
|
179
|
+
body: bodyNameMap,
|
|
180
|
+
query: queryNameMap,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Generates base type name from a route
|
|
185
|
+
* Example: "users/:id" + "get" + "Params" -> "UsersIdGetParams"
|
|
186
|
+
*/
|
|
187
|
+
function generateTypeNameBase(route, suffix) {
|
|
188
|
+
const parts = route.pattern
|
|
189
|
+
.split('/')
|
|
190
|
+
.filter(Boolean)
|
|
191
|
+
.map(part => {
|
|
192
|
+
// Remove : or * from the beginning
|
|
193
|
+
const clean = part.replace(/^[:*]/, '');
|
|
194
|
+
// Remove "index" from the end
|
|
195
|
+
const withoutIndex = clean === 'index' ? '' : clean;
|
|
196
|
+
// Capitalize each part
|
|
197
|
+
return withoutIndex
|
|
198
|
+
.split('-')
|
|
199
|
+
.map(word => capitalize(word))
|
|
200
|
+
.join('');
|
|
201
|
+
});
|
|
202
|
+
const patternPart = parts.filter(Boolean).join('');
|
|
203
|
+
return patternPart + capitalize(route.method) + suffix;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Generates a unique type name when there's a collision
|
|
207
|
+
*/
|
|
208
|
+
function generateUniqueTypeName(route, baseName, conflictingRoutes, allUsedNames, suffix) {
|
|
209
|
+
// Get all parts of the pattern (including parameters for context)
|
|
210
|
+
const patternParts = route.pattern.split('/').filter(Boolean);
|
|
211
|
+
// Remove "index" but keep all other parts (including params)
|
|
212
|
+
const allParts = patternParts
|
|
213
|
+
.filter(part => part !== 'index')
|
|
214
|
+
.map(part => {
|
|
215
|
+
// Include params in name to differentiate better
|
|
216
|
+
const clean = part.replace(/^[:*]/, '');
|
|
217
|
+
return capitalize(clean);
|
|
218
|
+
});
|
|
219
|
+
// Use all parts of the path to ensure uniqueness
|
|
220
|
+
if (allParts.length > 0) {
|
|
221
|
+
const fullPathName = allParts.join('');
|
|
222
|
+
const candidate = fullPathName + capitalize(route.method) + suffix;
|
|
223
|
+
if (!allUsedNames.has(candidate)) {
|
|
224
|
+
return candidate;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
// If it still collides, try adding suffix with the last parts
|
|
228
|
+
for (let i = allParts.length; i > 0; i--) {
|
|
229
|
+
const suffixParts = allParts.slice(-i).join('');
|
|
230
|
+
if (suffixParts) {
|
|
231
|
+
const candidate = `${baseName}${suffixParts}`;
|
|
232
|
+
if (!allUsedNames.has(candidate)) {
|
|
233
|
+
return candidate;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
// Last resort: add numeric suffix
|
|
238
|
+
let counter = 1;
|
|
239
|
+
let uniqueName = `${baseName}${counter}`;
|
|
240
|
+
while (allUsedNames.has(uniqueName)) {
|
|
241
|
+
counter++;
|
|
242
|
+
uniqueName = `${baseName}${counter}`;
|
|
243
|
+
}
|
|
244
|
+
return uniqueName;
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Generates the params definition for a type
|
|
248
|
+
*/
|
|
249
|
+
function generateParamsType(params) {
|
|
250
|
+
if (params.length === 0) {
|
|
251
|
+
return ' // No dynamic params';
|
|
252
|
+
}
|
|
253
|
+
return params.map(param => ` ${param}: string;`).join('\n');
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Writes the generated types file
|
|
257
|
+
*/
|
|
258
|
+
export async function generateTypesFile(outputPath, routes, apiBasePath) {
|
|
259
|
+
const content = generateTypesContent(routes, apiBasePath);
|
|
260
|
+
// Create directory if it doesn't exist
|
|
261
|
+
const dir = path.dirname(outputPath);
|
|
262
|
+
if (!fs.existsSync(dir)) {
|
|
263
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
264
|
+
}
|
|
265
|
+
fs.writeFileSync(outputPath, content, 'utf-8');
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Generates the content of the generated services file
|
|
269
|
+
*/
|
|
270
|
+
export function generateServicesContent(routes, apiBasePath, isTypeScript = true) {
|
|
271
|
+
// Generate unique names for all types (even in JS, we use them for parameter names)
|
|
272
|
+
const uniqueTypeNames = generateUniqueTypeNames(routes);
|
|
273
|
+
const paramTypeNameMap = uniqueTypeNames.params;
|
|
274
|
+
const bodyTypeNameMap = uniqueTypeNames.body;
|
|
275
|
+
const queryTypeNameMap = uniqueTypeNames.query;
|
|
276
|
+
// Imports and constants
|
|
277
|
+
let imports = '';
|
|
278
|
+
let apiBasePathConstant = '';
|
|
279
|
+
if (isTypeScript) {
|
|
280
|
+
// Collect all types needed to import (using unique names)
|
|
281
|
+
const paramTypeNames = Array.from(new Set(routes
|
|
282
|
+
.filter(route => route.params.length > 0)
|
|
283
|
+
.map(route => {
|
|
284
|
+
const routeKey = `${route.method}:${route.pattern}`;
|
|
285
|
+
return paramTypeNameMap.get(routeKey);
|
|
286
|
+
})));
|
|
287
|
+
const bodyTypeNames = Array.from(new Set(routes
|
|
288
|
+
.filter(route => route.bodyType)
|
|
289
|
+
.map(route => {
|
|
290
|
+
const routeKey = `${route.method}:${route.pattern}`;
|
|
291
|
+
return bodyTypeNameMap.get(routeKey);
|
|
292
|
+
})));
|
|
293
|
+
const queryTypeNames = Array.from(new Set(routes
|
|
294
|
+
.filter(route => route.queryType)
|
|
295
|
+
.map(route => {
|
|
296
|
+
const routeKey = `${route.method}:${route.pattern}`;
|
|
297
|
+
return queryTypeNameMap.get(routeKey);
|
|
298
|
+
})));
|
|
299
|
+
const allTypeNames = [...paramTypeNames, ...bodyTypeNames, ...queryTypeNames];
|
|
300
|
+
const typeImports = allTypeNames.length > 0
|
|
301
|
+
? `import type { ${allTypeNames.join(', ')} } from './api.types.js';\n`
|
|
302
|
+
: '';
|
|
303
|
+
imports = `// Auto-generated by Vitek - DO NOT EDIT
|
|
304
|
+
// This file is automatically generated from your API routes
|
|
305
|
+
|
|
306
|
+
import { API_BASE_PATH } from './api.types.js';
|
|
307
|
+
${typeImports}
|
|
308
|
+
`;
|
|
309
|
+
}
|
|
310
|
+
else {
|
|
311
|
+
// JavaScript: no type imports, just constant
|
|
312
|
+
imports = `// Auto-generated by Vitek - DO NOT EDIT
|
|
313
|
+
// This file is automatically generated from your API routes
|
|
314
|
+
|
|
315
|
+
const API_BASE_PATH = '${apiBasePath}';
|
|
316
|
+
`;
|
|
317
|
+
}
|
|
318
|
+
// Generate function for each route with unique names
|
|
319
|
+
const functionNameMap = generateUniqueFunctionNames(routes);
|
|
320
|
+
const functions = routes.map(route => {
|
|
321
|
+
const routeKey = `${route.method}:${route.pattern}`;
|
|
322
|
+
const functionName = functionNameMap.get(routeKey);
|
|
323
|
+
const typeName = paramTypeNameMap.get(routeKey);
|
|
324
|
+
const bodyTypeName = route.bodyType ? bodyTypeNameMap.get(routeKey) : undefined;
|
|
325
|
+
const queryTypeName = route.queryType ? queryTypeNameMap.get(routeKey) : undefined;
|
|
326
|
+
const urlPath = generateUrlPath(route.pattern, route.params);
|
|
327
|
+
// Determine function parameters - only add if they actually exist
|
|
328
|
+
const hasParams = route.params.length > 0;
|
|
329
|
+
const hasBody = !!route.bodyType; // Only if bodyType is declared
|
|
330
|
+
const hasQuery = !!route.queryType; // Only if queryType is declared
|
|
331
|
+
const methodLower = route.method.toLowerCase();
|
|
332
|
+
let params = [];
|
|
333
|
+
let returnType = isTypeScript ? ': Promise<any>' : '';
|
|
334
|
+
// Add params only if they exist
|
|
335
|
+
if (hasParams) {
|
|
336
|
+
if (isTypeScript) {
|
|
337
|
+
params.push(`params: ${typeName}`);
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
params.push(`params`);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
// Add body only if bodyType is declared
|
|
344
|
+
if (hasBody) {
|
|
345
|
+
if (isTypeScript) {
|
|
346
|
+
params.push(`body: ${bodyTypeName}`);
|
|
347
|
+
}
|
|
348
|
+
else {
|
|
349
|
+
params.push(`body`);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
// Add query only if queryType is declared
|
|
353
|
+
if (hasQuery) {
|
|
354
|
+
if (isTypeScript) {
|
|
355
|
+
params.push(`query: ${queryTypeName}`);
|
|
356
|
+
}
|
|
357
|
+
else {
|
|
358
|
+
params.push(`query`);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
// Always add options
|
|
362
|
+
if (isTypeScript) {
|
|
363
|
+
params.push(`options?: RequestInit`);
|
|
364
|
+
}
|
|
365
|
+
else {
|
|
366
|
+
params.push(`options`);
|
|
367
|
+
}
|
|
368
|
+
// Build fetch code
|
|
369
|
+
let fetchCode = '';
|
|
370
|
+
if (hasBody && hasQuery) {
|
|
371
|
+
// POST, PUT, PATCH with body and query string
|
|
372
|
+
fetchCode = ` const url = new URL(\`\${API_BASE_PATH}${urlPath}\`, window.location.origin);
|
|
373
|
+
Object.entries(query).forEach(([key, value]) => {
|
|
374
|
+
url.searchParams.append(key, String(value));
|
|
375
|
+
});
|
|
376
|
+
return fetch(url.pathname + url.search, {
|
|
377
|
+
method: '${route.method.toUpperCase()}',
|
|
378
|
+
headers: {
|
|
379
|
+
'Content-Type': 'application/json',
|
|
380
|
+
...options?.headers,
|
|
381
|
+
},
|
|
382
|
+
body: JSON.stringify(body),
|
|
383
|
+
...options,
|
|
384
|
+
}).then(res => res.json());`;
|
|
385
|
+
}
|
|
386
|
+
else if (hasBody) {
|
|
387
|
+
// POST, PUT, PATCH with body (no query)
|
|
388
|
+
fetchCode = ` return fetch(\`\${API_BASE_PATH}${urlPath}\`, {
|
|
389
|
+
method: '${route.method.toUpperCase()}',
|
|
390
|
+
headers: {
|
|
391
|
+
'Content-Type': 'application/json',
|
|
392
|
+
...options?.headers,
|
|
393
|
+
},
|
|
394
|
+
body: JSON.stringify(body),
|
|
395
|
+
...options,
|
|
396
|
+
}).then(res => res.json());`;
|
|
397
|
+
}
|
|
398
|
+
else if (hasQuery) {
|
|
399
|
+
// GET (or other method) with typed query string (no body)
|
|
400
|
+
fetchCode = ` const url = new URL(\`\${API_BASE_PATH}${urlPath}\`, window.location.origin);
|
|
401
|
+
Object.entries(query).forEach(([key, value]) => {
|
|
402
|
+
url.searchParams.append(key, String(value));
|
|
403
|
+
});
|
|
404
|
+
return fetch(url.pathname + url.search, {
|
|
405
|
+
method: '${route.method.toUpperCase()}',
|
|
406
|
+
...options,
|
|
407
|
+
}).then(res => res.json());`;
|
|
408
|
+
}
|
|
409
|
+
else {
|
|
410
|
+
// GET/DELETE/etc without body or query
|
|
411
|
+
fetchCode = ` return fetch(\`\${API_BASE_PATH}${urlPath}\`, {
|
|
412
|
+
method: '${route.method.toUpperCase()}',
|
|
413
|
+
...options,
|
|
414
|
+
}).then(res => res.json());`;
|
|
415
|
+
}
|
|
416
|
+
return `export async function ${functionName}(${params.join(', ')})${returnType} {
|
|
417
|
+
${fetchCode}
|
|
418
|
+
}`;
|
|
419
|
+
}).join('\n\n');
|
|
420
|
+
return `${imports}${functions}
|
|
421
|
+
`;
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Generates unique function names for all routes, ensuring there are no collisions
|
|
425
|
+
*/
|
|
426
|
+
function generateUniqueFunctionNames(routes) {
|
|
427
|
+
const nameMap = new Map(); // routeKey -> functionName
|
|
428
|
+
const nameToRoutes = new Map(); // functionName -> routes[]
|
|
429
|
+
// First pass: group routes by base name
|
|
430
|
+
routes.forEach(route => {
|
|
431
|
+
const baseName = generateFunctionNameBase(route);
|
|
432
|
+
if (!nameToRoutes.has(baseName)) {
|
|
433
|
+
nameToRoutes.set(baseName, []);
|
|
434
|
+
}
|
|
435
|
+
nameToRoutes.get(baseName).push(route);
|
|
436
|
+
});
|
|
437
|
+
// Second pass: resolve collisions
|
|
438
|
+
routes.forEach(route => {
|
|
439
|
+
const routeKey = `${route.method}:${route.pattern}`;
|
|
440
|
+
const baseName = generateFunctionNameBase(route);
|
|
441
|
+
const routesWithSameName = nameToRoutes.get(baseName);
|
|
442
|
+
if (routesWithSameName.length === 1) {
|
|
443
|
+
// Unique name, use directly
|
|
444
|
+
nameMap.set(routeKey, baseName);
|
|
445
|
+
}
|
|
446
|
+
else {
|
|
447
|
+
// There's a collision, we need to make it unique by including more context
|
|
448
|
+
const uniqueName = generateUniqueFunctionName(route, baseName, routesWithSameName, nameMap);
|
|
449
|
+
nameMap.set(routeKey, uniqueName);
|
|
450
|
+
}
|
|
451
|
+
});
|
|
452
|
+
return nameMap;
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Generates base function name from a route
|
|
456
|
+
* Example: "users/:id" + "get" -> "getUsersById"
|
|
457
|
+
* Example: "posts/index" + "get" -> "getPosts" (index is removed)
|
|
458
|
+
* Example: "" + "get" -> "get" (root route)
|
|
459
|
+
*/
|
|
460
|
+
function generateFunctionNameBase(route) {
|
|
461
|
+
const method = route.method.toLowerCase();
|
|
462
|
+
if (route.pattern === '') {
|
|
463
|
+
// Rota raiz
|
|
464
|
+
return method;
|
|
465
|
+
}
|
|
466
|
+
const parts = route.pattern
|
|
467
|
+
.split('/')
|
|
468
|
+
.filter(Boolean)
|
|
469
|
+
.filter(part => part !== 'index') // Remove "index" from function name
|
|
470
|
+
.map(part => {
|
|
471
|
+
// Remove : or * from the beginning
|
|
472
|
+
const clean = part.replace(/^[:*]/, '');
|
|
473
|
+
// Capitalize
|
|
474
|
+
return capitalize(clean);
|
|
475
|
+
});
|
|
476
|
+
const pathName = parts.join('');
|
|
477
|
+
return `${method}${pathName}`;
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Generates a unique function name when there's a collision
|
|
481
|
+
* Includes more context from the pattern to differentiate
|
|
482
|
+
*/
|
|
483
|
+
function generateUniqueFunctionName(route, baseName, conflictingRoutes, existingNames) {
|
|
484
|
+
// Get all parts of the pattern (including parameters as context)
|
|
485
|
+
const patternParts = route.pattern.split('/').filter(Boolean);
|
|
486
|
+
// Remove "index" but keep other parts (including params for context)
|
|
487
|
+
const allParts = patternParts
|
|
488
|
+
.filter(part => part !== 'index')
|
|
489
|
+
.map(part => {
|
|
490
|
+
// For parameters, include the name as context to differentiate
|
|
491
|
+
// Example: ":id" becomes "Id", "*ids" becomes "Ids"
|
|
492
|
+
const clean = part.replace(/^[:*]/, '');
|
|
493
|
+
return capitalize(clean);
|
|
494
|
+
});
|
|
495
|
+
// Use all parts of the path to ensure uniqueness from the start
|
|
496
|
+
if (allParts.length > 0) {
|
|
497
|
+
const fullPathName = allParts.join('');
|
|
498
|
+
const candidate = `${route.method.toLowerCase()}${fullPathName}`;
|
|
499
|
+
// Check if this name has already been assigned to another route
|
|
500
|
+
const usedNames = new Set(existingNames.values());
|
|
501
|
+
if (!usedNames.has(candidate)) {
|
|
502
|
+
return candidate;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
// If it still collides, try adding suffix with the last different parts
|
|
506
|
+
// Compare with other conflicting routes to find differences
|
|
507
|
+
const otherPatterns = conflictingRoutes
|
|
508
|
+
.filter(r => r.pattern !== route.pattern)
|
|
509
|
+
.map(r => r.pattern.split('/').filter(Boolean));
|
|
510
|
+
// Try using more parts from the end of the path
|
|
511
|
+
for (let i = allParts.length; i > 0; i--) {
|
|
512
|
+
const suffix = allParts.slice(-i).join('');
|
|
513
|
+
if (suffix) {
|
|
514
|
+
const candidate = `${baseName}${suffix}`;
|
|
515
|
+
const usedNames = new Set(existingNames.values());
|
|
516
|
+
if (!usedNames.has(candidate)) {
|
|
517
|
+
return candidate;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
// Last resort: add sequential numeric suffix
|
|
522
|
+
let counter = 1;
|
|
523
|
+
let uniqueName = `${baseName}${counter}`;
|
|
524
|
+
const usedNames = new Set(existingNames.values());
|
|
525
|
+
while (usedNames.has(uniqueName)) {
|
|
526
|
+
counter++;
|
|
527
|
+
uniqueName = `${baseName}${counter}`;
|
|
528
|
+
}
|
|
529
|
+
return uniqueName;
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* Generates the URL path by replacing params with template literals
|
|
533
|
+
* Example: "users/:id" -> "/users/${params.id}"
|
|
534
|
+
* Example: "posts/index" -> "/posts" (index is removed)
|
|
535
|
+
* Example: "" -> "/"
|
|
536
|
+
*/
|
|
537
|
+
function generateUrlPath(pattern, params) {
|
|
538
|
+
if (pattern === '') {
|
|
539
|
+
return '/';
|
|
540
|
+
}
|
|
541
|
+
// Remove "index" from the end of pattern if it exists
|
|
542
|
+
let cleanPattern = pattern.replace(/\/index$/, '').replace(/^index\//, '');
|
|
543
|
+
if (cleanPattern === 'index') {
|
|
544
|
+
cleanPattern = '';
|
|
545
|
+
}
|
|
546
|
+
if (cleanPattern === '') {
|
|
547
|
+
return '/';
|
|
548
|
+
}
|
|
549
|
+
let urlPath = '/' + cleanPattern;
|
|
550
|
+
// Substitui :param por ${params.param}
|
|
551
|
+
// Substitui *param por ${params.param} (catch-all)
|
|
552
|
+
params.forEach(param => {
|
|
553
|
+
// Para catch-all (*param), substitui o *param
|
|
554
|
+
const catchAllPattern = `*${param}`;
|
|
555
|
+
if (urlPath.includes(catchAllPattern)) {
|
|
556
|
+
urlPath = urlPath.replace(catchAllPattern, `\${params.${param}}`);
|
|
557
|
+
}
|
|
558
|
+
else {
|
|
559
|
+
// Para parâmetro normal (:param)
|
|
560
|
+
urlPath = urlPath.replace(`:${param}`, `\${params.${param}}`);
|
|
561
|
+
}
|
|
562
|
+
});
|
|
563
|
+
return urlPath;
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Writes the generated services file
|
|
567
|
+
*/
|
|
568
|
+
export async function generateServicesFile(outputPath, routes, apiBasePath, isTypeScript = true) {
|
|
569
|
+
const content = generateServicesContent(routes, apiBasePath, isTypeScript);
|
|
570
|
+
// Create directory if it doesn't exist
|
|
571
|
+
const dir = path.dirname(outputPath);
|
|
572
|
+
if (!fs.existsSync(dir)) {
|
|
573
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
574
|
+
}
|
|
575
|
+
fs.writeFileSync(outputPath, content, 'utf-8');
|
|
576
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema for type generation
|
|
3
|
+
* Core logic - no dependencies
|
|
4
|
+
*/
|
|
5
|
+
import type { Route } from '../routing/route-types.js';
|
|
6
|
+
/**
|
|
7
|
+
* Schema of a route for type generation
|
|
8
|
+
*/
|
|
9
|
+
export interface RouteSchema {
|
|
10
|
+
pattern: string;
|
|
11
|
+
method: string;
|
|
12
|
+
params: string[];
|
|
13
|
+
file: string;
|
|
14
|
+
bodyType?: string;
|
|
15
|
+
queryType?: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Converts routes to schema
|
|
19
|
+
*/
|
|
20
|
+
export declare function routesToSchema(routes: Route[]): RouteSchema[];
|
|
21
|
+
//# sourceMappingURL=schema.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../../src/core/types/schema.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,2BAA2B,CAAC;AAEvD;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,WAAW,EAAE,CAS7D"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema for type generation
|
|
3
|
+
* Core logic - no dependencies
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Converts routes to schema
|
|
7
|
+
*/
|
|
8
|
+
export function routesToSchema(routes) {
|
|
9
|
+
return routes.map(route => ({
|
|
10
|
+
pattern: route.pattern,
|
|
11
|
+
method: route.method,
|
|
12
|
+
params: route.params,
|
|
13
|
+
file: route.file,
|
|
14
|
+
bodyType: route.bodyType,
|
|
15
|
+
queryType: route.queryType,
|
|
16
|
+
}));
|
|
17
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validation types
|
|
3
|
+
* Core logic - runtime agnostic
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Validation rule for a field
|
|
7
|
+
*/
|
|
8
|
+
export interface ValidationRule {
|
|
9
|
+
type: 'string' | 'number' | 'boolean' | 'object' | 'array';
|
|
10
|
+
required?: boolean;
|
|
11
|
+
min?: number;
|
|
12
|
+
max?: number;
|
|
13
|
+
pattern?: string | RegExp;
|
|
14
|
+
custom?: (value: any) => boolean | string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Validation schema for body or query parameters
|
|
18
|
+
*/
|
|
19
|
+
export type ValidationSchema = Record<string, ValidationRule>;
|
|
20
|
+
/**
|
|
21
|
+
* Validation result
|
|
22
|
+
*/
|
|
23
|
+
export interface ValidationResult {
|
|
24
|
+
valid: boolean;
|
|
25
|
+
errors?: Record<string, string[]>;
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/core/validation/types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,QAAQ,GAAG,QAAQ,GAAG,SAAS,GAAG,QAAQ,GAAG,OAAO,CAAC;IAC3D,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC1B,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,KAAK,OAAO,GAAG,MAAM,CAAC;CAC3C;AAED;;GAEG;AACH,MAAM,MAAM,gBAAgB,GAAG,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;AAE9D;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;CACnC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validation logic
|
|
3
|
+
* Core logic - runtime agnostic
|
|
4
|
+
*/
|
|
5
|
+
import type { ValidationSchema, ValidationResult } from './types.js';
|
|
6
|
+
/**
|
|
7
|
+
* Validates an object against a validation schema
|
|
8
|
+
*/
|
|
9
|
+
export declare function validate(data: any, schema: ValidationSchema): ValidationResult;
|
|
10
|
+
/**
|
|
11
|
+
* Validates and throws ValidationError if invalid
|
|
12
|
+
*/
|
|
13
|
+
export declare function validateOrThrow(data: any, schema: ValidationSchema): void;
|
|
14
|
+
/**
|
|
15
|
+
* Validates body parameters
|
|
16
|
+
*/
|
|
17
|
+
export declare function validateBody(body: any, schema: ValidationSchema): any;
|
|
18
|
+
/**
|
|
19
|
+
* Validates query parameters
|
|
20
|
+
*/
|
|
21
|
+
export declare function validateQuery(query: any, schema: ValidationSchema): any;
|
|
22
|
+
//# sourceMappingURL=validator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validator.d.ts","sourceRoot":"","sources":["../../../src/core/validation/validator.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,KAAK,EAAE,gBAAgB,EAAE,gBAAgB,EAAkB,MAAM,YAAY,CAAC;AAiGrF;;GAEG;AACH,wBAAgB,QAAQ,CACtB,IAAI,EAAE,GAAG,EACT,MAAM,EAAE,gBAAgB,GACvB,gBAAgB,CAmBlB;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,GAAG,EAAE,MAAM,EAAE,gBAAgB,GAAG,IAAI,CAKzE;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,GAAG,EAAE,MAAM,EAAE,gBAAgB,GAAG,GAAG,CAGrE;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,gBAAgB,GAAG,GAAG,CAGvE"}
|