@geekmidas/cli 0.10.0 → 0.12.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 +525 -0
- package/dist/bundler-DRXCw_YR.mjs +70 -0
- package/dist/bundler-DRXCw_YR.mjs.map +1 -0
- package/dist/bundler-WsEvH_b2.cjs +71 -0
- package/dist/bundler-WsEvH_b2.cjs.map +1 -0
- package/dist/{config-C9aXOHBe.cjs → config-AmInkU7k.cjs} +8 -8
- package/dist/config-AmInkU7k.cjs.map +1 -0
- package/dist/{config-BrkUalUh.mjs → config-DYULeEv8.mjs} +3 -3
- package/dist/config-DYULeEv8.mjs.map +1 -0
- package/dist/config.cjs +1 -1
- package/dist/config.d.cts +1 -1
- package/dist/config.d.mts +1 -1
- package/dist/config.mjs +1 -1
- package/dist/encryption-C8H-38Yy.mjs +42 -0
- package/dist/encryption-C8H-38Yy.mjs.map +1 -0
- package/dist/encryption-Dyf_r1h-.cjs +44 -0
- package/dist/encryption-Dyf_r1h-.cjs.map +1 -0
- package/dist/index.cjs +2116 -179
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +2134 -192
- package/dist/index.mjs.map +1 -1
- package/dist/{openapi-CZLI4QTr.mjs → openapi-BfFlOBCG.mjs} +801 -38
- package/dist/openapi-BfFlOBCG.mjs.map +1 -0
- package/dist/{openapi-BeHLKcwP.cjs → openapi-Bt_1FDpT.cjs} +794 -31
- package/dist/openapi-Bt_1FDpT.cjs.map +1 -0
- package/dist/{openapi-react-query-o5iMi8tz.cjs → openapi-react-query-B-sNWHFU.cjs} +5 -5
- package/dist/openapi-react-query-B-sNWHFU.cjs.map +1 -0
- package/dist/{openapi-react-query-CcciaVu5.mjs → openapi-react-query-B6XTeGqS.mjs} +5 -5
- package/dist/openapi-react-query-B6XTeGqS.mjs.map +1 -0
- package/dist/openapi-react-query.cjs +1 -1
- package/dist/openapi-react-query.d.cts.map +1 -1
- package/dist/openapi-react-query.d.mts.map +1 -1
- package/dist/openapi-react-query.mjs +1 -1
- package/dist/openapi.cjs +2 -2
- package/dist/openapi.d.cts +1 -1
- package/dist/openapi.d.cts.map +1 -1
- package/dist/openapi.d.mts +1 -1
- package/dist/openapi.d.mts.map +1 -1
- package/dist/openapi.mjs +2 -2
- package/dist/storage-BUYQJgz7.cjs +4 -0
- package/dist/storage-BXoJvmv2.cjs +149 -0
- package/dist/storage-BXoJvmv2.cjs.map +1 -0
- package/dist/storage-C9PU_30f.mjs +101 -0
- package/dist/storage-C9PU_30f.mjs.map +1 -0
- package/dist/storage-DLJAYxzJ.mjs +3 -0
- package/dist/{types-b-vwGpqc.d.cts → types-BR0M2v_c.d.mts} +100 -1
- package/dist/types-BR0M2v_c.d.mts.map +1 -0
- package/dist/{types-DXgiA1sF.d.mts → types-BhkZc-vm.d.cts} +100 -1
- package/dist/types-BhkZc-vm.d.cts.map +1 -0
- package/examples/cron-example.ts +27 -27
- package/examples/env.ts +27 -27
- package/examples/function-example.ts +31 -31
- package/examples/gkm.config.json +20 -20
- package/examples/gkm.config.ts +8 -8
- package/examples/gkm.minimal.config.json +5 -5
- package/examples/gkm.production.config.json +25 -25
- package/examples/logger.ts +2 -2
- package/package.json +6 -6
- package/src/__tests__/EndpointGenerator.hooks.spec.ts +191 -191
- package/src/__tests__/config.spec.ts +55 -55
- package/src/__tests__/loadEnvFiles.spec.ts +93 -93
- package/src/__tests__/normalizeHooksConfig.spec.ts +58 -58
- package/src/__tests__/openapi-react-query.spec.ts +497 -497
- package/src/__tests__/openapi.spec.ts +428 -428
- package/src/__tests__/test-helpers.ts +76 -76
- package/src/auth/__tests__/credentials.spec.ts +204 -0
- package/src/auth/__tests__/index.spec.ts +168 -0
- package/src/auth/credentials.ts +187 -0
- package/src/auth/index.ts +226 -0
- package/src/build/__tests__/index-new.spec.ts +474 -474
- package/src/build/__tests__/manifests.spec.ts +333 -333
- package/src/build/bundler.ts +141 -0
- package/src/build/endpoint-analyzer.ts +236 -0
- package/src/build/handler-templates.ts +1253 -0
- package/src/build/index.ts +250 -179
- package/src/build/manifests.ts +52 -52
- package/src/build/providerResolver.ts +145 -145
- package/src/build/types.ts +64 -43
- package/src/config.ts +39 -39
- package/src/deploy/__tests__/docker.spec.ts +111 -0
- package/src/deploy/__tests__/dokploy.spec.ts +245 -0
- package/src/deploy/__tests__/init.spec.ts +662 -0
- package/src/deploy/docker.ts +128 -0
- package/src/deploy/dokploy.ts +204 -0
- package/src/deploy/index.ts +136 -0
- package/src/deploy/init.ts +484 -0
- package/src/deploy/types.ts +48 -0
- package/src/dev/__tests__/index.spec.ts +266 -266
- package/src/dev/index.ts +647 -601
- package/src/docker/__tests__/compose.spec.ts +531 -0
- package/src/docker/__tests__/templates.spec.ts +280 -0
- package/src/docker/compose.ts +273 -0
- package/src/docker/index.ts +230 -0
- package/src/docker/templates.ts +446 -0
- package/src/generators/CronGenerator.ts +72 -72
- package/src/generators/EndpointGenerator.ts +699 -398
- package/src/generators/FunctionGenerator.ts +84 -84
- package/src/generators/Generator.ts +72 -72
- package/src/generators/OpenApiTsGenerator.ts +577 -577
- package/src/generators/SubscriberGenerator.ts +124 -124
- package/src/generators/__tests__/CronGenerator.spec.ts +433 -433
- package/src/generators/__tests__/EndpointGenerator.spec.ts +532 -382
- package/src/generators/__tests__/FunctionGenerator.spec.ts +244 -244
- package/src/generators/__tests__/SubscriberGenerator.spec.ts +397 -382
- package/src/generators/index.ts +4 -4
- package/src/index.ts +623 -201
- package/src/init/__tests__/generators.spec.ts +334 -334
- package/src/init/__tests__/init.spec.ts +332 -332
- package/src/init/__tests__/utils.spec.ts +89 -89
- package/src/init/generators/config.ts +175 -175
- package/src/init/generators/docker.ts +41 -41
- package/src/init/generators/env.ts +72 -72
- package/src/init/generators/index.ts +1 -1
- package/src/init/generators/models.ts +64 -64
- package/src/init/generators/monorepo.ts +161 -161
- package/src/init/generators/package.ts +71 -71
- package/src/init/generators/source.ts +6 -6
- package/src/init/index.ts +203 -208
- package/src/init/templates/api.ts +115 -115
- package/src/init/templates/index.ts +75 -75
- package/src/init/templates/minimal.ts +98 -98
- package/src/init/templates/serverless.ts +89 -89
- package/src/init/templates/worker.ts +98 -98
- package/src/init/utils.ts +54 -56
- package/src/openapi-react-query.ts +194 -194
- package/src/openapi.ts +63 -63
- package/src/secrets/__tests__/encryption.spec.ts +226 -0
- package/src/secrets/__tests__/generator.spec.ts +319 -0
- package/src/secrets/__tests__/index.spec.ts +91 -0
- package/src/secrets/__tests__/storage.spec.ts +403 -0
- package/src/secrets/encryption.ts +91 -0
- package/src/secrets/generator.ts +164 -0
- package/src/secrets/index.ts +383 -0
- package/src/secrets/storage.ts +134 -0
- package/src/secrets/types.ts +53 -0
- package/src/types.ts +295 -176
- package/tsdown.config.ts +11 -8
- package/dist/config-BrkUalUh.mjs.map +0 -1
- package/dist/config-C9aXOHBe.cjs.map +0 -1
- package/dist/openapi-BeHLKcwP.cjs.map +0 -1
- package/dist/openapi-CZLI4QTr.mjs.map +0 -1
- package/dist/openapi-react-query-CcciaVu5.mjs.map +0 -1
- package/dist/openapi-react-query-o5iMi8tz.cjs.map +0 -1
- package/dist/types-DXgiA1sF.d.mts.map +0 -1
- package/dist/types-b-vwGpqc.d.cts.map +0 -1
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { loadConfig } from "./config-
|
|
2
|
-
import { relative } from "path";
|
|
1
|
+
import { loadConfig } from "./config-DYULeEv8.mjs";
|
|
2
|
+
import { dirname, join, relative } from "node:path";
|
|
3
3
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
4
|
-
import { dirname, join as join$1, relative as relative$1 } from "node:path";
|
|
5
4
|
import fg from "fast-glob";
|
|
6
5
|
import kebabCase from "lodash.kebabcase";
|
|
7
6
|
import { Endpoint } from "@geekmidas/constructs/endpoints";
|
|
@@ -41,6 +40,586 @@ var ConstructGenerator = class {
|
|
|
41
40
|
}
|
|
42
41
|
};
|
|
43
42
|
|
|
43
|
+
//#endregion
|
|
44
|
+
//#region src/build/endpoint-analyzer.ts
|
|
45
|
+
/**
|
|
46
|
+
* Analyze an endpoint to extract its features
|
|
47
|
+
*/
|
|
48
|
+
function analyzeEndpointFeatures(endpoint) {
|
|
49
|
+
return {
|
|
50
|
+
hasAuth: !!endpoint.authorizer,
|
|
51
|
+
hasServices: endpoint.services.length > 0,
|
|
52
|
+
hasDatabase: !!endpoint.databaseService,
|
|
53
|
+
hasBodyValidation: !!endpoint.input?.body,
|
|
54
|
+
hasQueryValidation: !!endpoint.input?.query,
|
|
55
|
+
hasParamValidation: !!endpoint.input?.params,
|
|
56
|
+
hasAudits: (endpoint.audits?.length ?? 0) > 0,
|
|
57
|
+
hasEvents: (endpoint.events?.length ?? 0) > 0,
|
|
58
|
+
hasRateLimit: !!endpoint.rateLimit,
|
|
59
|
+
hasRls: !!endpoint.rlsConfig && !endpoint.rlsBypass,
|
|
60
|
+
hasOutputValidation: !!endpoint.outputSchema
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Determine the optimization tier for an endpoint based on its features
|
|
65
|
+
*/
|
|
66
|
+
function determineEndpointTier(features) {
|
|
67
|
+
const { hasAuth, hasServices, hasDatabase, hasAudits, hasEvents, hasRateLimit, hasRls } = features;
|
|
68
|
+
if (!hasAuth && !hasServices && !hasDatabase && !hasAudits && !hasEvents && !hasRateLimit && !hasRls) return "minimal";
|
|
69
|
+
if (hasAudits || hasRls || hasRateLimit) return "full";
|
|
70
|
+
return "standard";
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Perform complete analysis of an endpoint
|
|
74
|
+
*/
|
|
75
|
+
function analyzeEndpoint(endpoint, exportName) {
|
|
76
|
+
const features = analyzeEndpointFeatures(endpoint);
|
|
77
|
+
const tier = determineEndpointTier(features);
|
|
78
|
+
return {
|
|
79
|
+
route: endpoint.route,
|
|
80
|
+
method: endpoint.method,
|
|
81
|
+
exportName,
|
|
82
|
+
features,
|
|
83
|
+
tier,
|
|
84
|
+
serviceNames: endpoint.services.map((s) => s.serviceName),
|
|
85
|
+
databaseServiceName: endpoint.databaseService?.serviceName
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Generate a summary of endpoint analysis for logging
|
|
90
|
+
*/
|
|
91
|
+
function summarizeAnalysis(analyses) {
|
|
92
|
+
const byTier = {
|
|
93
|
+
minimal: 0,
|
|
94
|
+
standard: 0,
|
|
95
|
+
full: 0
|
|
96
|
+
};
|
|
97
|
+
const byFeature = {
|
|
98
|
+
hasAuth: 0,
|
|
99
|
+
hasServices: 0,
|
|
100
|
+
hasDatabase: 0,
|
|
101
|
+
hasBodyValidation: 0,
|
|
102
|
+
hasQueryValidation: 0,
|
|
103
|
+
hasParamValidation: 0,
|
|
104
|
+
hasAudits: 0,
|
|
105
|
+
hasEvents: 0,
|
|
106
|
+
hasRateLimit: 0,
|
|
107
|
+
hasRls: 0,
|
|
108
|
+
hasOutputValidation: 0
|
|
109
|
+
};
|
|
110
|
+
for (const analysis of analyses) {
|
|
111
|
+
byTier[analysis.tier]++;
|
|
112
|
+
for (const [feature, enabled] of Object.entries(analysis.features)) if (enabled) byFeature[feature]++;
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
total: analyses.length,
|
|
116
|
+
byTier,
|
|
117
|
+
byFeature
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
//#endregion
|
|
122
|
+
//#region src/build/handler-templates.ts
|
|
123
|
+
/**
|
|
124
|
+
* Generate validator references for an endpoint
|
|
125
|
+
*/
|
|
126
|
+
function generateValidators(exportName, features) {
|
|
127
|
+
const validators = [];
|
|
128
|
+
if (features.hasBodyValidation) validators.push(`validateBody(${exportName})`);
|
|
129
|
+
if (features.hasQueryValidation) validators.push(`validateQuery(${exportName})`);
|
|
130
|
+
if (features.hasParamValidation) validators.push(`validateParams(${exportName})`);
|
|
131
|
+
return validators.length > 0 ? `\n ${validators.join(",\n ")},` : "";
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Generate a minimal handler (near-raw-Hono performance)
|
|
135
|
+
*
|
|
136
|
+
* Used for: Health checks, public endpoints with no services
|
|
137
|
+
*/
|
|
138
|
+
function generateMinimalHandler(analysis) {
|
|
139
|
+
const { exportName, features } = analysis;
|
|
140
|
+
const method = analysis.method.toLowerCase();
|
|
141
|
+
const validators = generateValidators(exportName, features);
|
|
142
|
+
const hasValidators = validators.length > 0;
|
|
143
|
+
if (!hasValidators && !features.hasOutputValidation) return `
|
|
144
|
+
// Minimal handler: ${analysis.route} (${analysis.method})
|
|
145
|
+
app.${method}('${analysis.route}', async (c) => {
|
|
146
|
+
const result = await ${exportName}.handler(
|
|
147
|
+
{
|
|
148
|
+
services: {},
|
|
149
|
+
logger,
|
|
150
|
+
body: undefined,
|
|
151
|
+
query: undefined,
|
|
152
|
+
params: undefined,
|
|
153
|
+
session: undefined,
|
|
154
|
+
header: Endpoint.createHeaders(c.req.header()),
|
|
155
|
+
cookie: Endpoint.createCookies(c.req.header().cookie),
|
|
156
|
+
auditor: undefined,
|
|
157
|
+
db: undefined,
|
|
158
|
+
} as any,
|
|
159
|
+
{ getMetadata: () => ({}) } as any,
|
|
160
|
+
);
|
|
161
|
+
return c.json(result, ${exportName}.status as any);
|
|
162
|
+
});`;
|
|
163
|
+
return `
|
|
164
|
+
// Minimal handler with validation: ${analysis.route} (${analysis.method})
|
|
165
|
+
app.${method}('${analysis.route}',${validators}
|
|
166
|
+
async (c) => {
|
|
167
|
+
const result = await ${exportName}.handler(
|
|
168
|
+
{
|
|
169
|
+
services: {},
|
|
170
|
+
logger,
|
|
171
|
+
body: ${features.hasBodyValidation ? "(c.req.valid as any)('json')" : "undefined"},
|
|
172
|
+
query: ${features.hasQueryValidation ? "(c.req.valid as any)('query')" : "undefined"},
|
|
173
|
+
params: ${features.hasParamValidation ? "(c.req.valid as any)('param')" : "undefined"},
|
|
174
|
+
session: undefined,
|
|
175
|
+
header: Endpoint.createHeaders(c.req.header()),
|
|
176
|
+
cookie: Endpoint.createCookies(c.req.header().cookie),
|
|
177
|
+
auditor: undefined,
|
|
178
|
+
db: undefined,
|
|
179
|
+
} as any,
|
|
180
|
+
{ getMetadata: () => ({}) } as any,
|
|
181
|
+
);
|
|
182
|
+
${features.hasOutputValidation ? `const output = await ${exportName}.parseOutput(result);
|
|
183
|
+
return c.json(output, ${exportName}.status as any);` : `return c.json(result, ${exportName}.status as any);`}
|
|
184
|
+
}
|
|
185
|
+
);`;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Generate a standard handler (auth and/or services)
|
|
189
|
+
*
|
|
190
|
+
* Used for: Authenticated endpoints, endpoints with services
|
|
191
|
+
*/
|
|
192
|
+
function generateStandardHandler(analysis) {
|
|
193
|
+
const { exportName, features } = analysis;
|
|
194
|
+
const method = analysis.method.toLowerCase();
|
|
195
|
+
const validators = generateValidators(exportName, features);
|
|
196
|
+
let serviceResolution = "";
|
|
197
|
+
if (features.hasServices || features.hasDatabase) serviceResolution = `
|
|
198
|
+
const services = await serviceDiscovery.register(${exportName}.services);
|
|
199
|
+
${features.hasDatabase ? `const db = ${exportName}.databaseService
|
|
200
|
+
? (await serviceDiscovery.register([${exportName}.databaseService]) as any)[${exportName}.databaseService.serviceName]
|
|
201
|
+
: undefined;` : "const db = undefined;"}`;
|
|
202
|
+
else serviceResolution = `
|
|
203
|
+
const services = {};
|
|
204
|
+
const db = undefined;`;
|
|
205
|
+
let authCode = "";
|
|
206
|
+
if (features.hasAuth) authCode = `
|
|
207
|
+
// Authentication
|
|
208
|
+
const session = await ${exportName}.getSession({
|
|
209
|
+
services,
|
|
210
|
+
logger,
|
|
211
|
+
header,
|
|
212
|
+
cookie,
|
|
213
|
+
...(db !== undefined && { db }),
|
|
214
|
+
} as any);
|
|
215
|
+
|
|
216
|
+
const isAuthorized = await ${exportName}.authorize({
|
|
217
|
+
header,
|
|
218
|
+
cookie,
|
|
219
|
+
services,
|
|
220
|
+
logger,
|
|
221
|
+
session,
|
|
222
|
+
} as any);
|
|
223
|
+
|
|
224
|
+
if (!isAuthorized) {
|
|
225
|
+
return c.json({ error: 'Unauthorized' }, 401);
|
|
226
|
+
}`;
|
|
227
|
+
let eventCode = "";
|
|
228
|
+
if (features.hasEvents) eventCode = `
|
|
229
|
+
// Publish events on success
|
|
230
|
+
if (Endpoint.isSuccessStatus(${exportName}.status)) {
|
|
231
|
+
await (publishConstructEvents as any)(
|
|
232
|
+
${exportName},
|
|
233
|
+
result,
|
|
234
|
+
serviceDiscovery,
|
|
235
|
+
logger,
|
|
236
|
+
);
|
|
237
|
+
}`;
|
|
238
|
+
return `
|
|
239
|
+
// Standard handler: ${analysis.route} (${analysis.method})
|
|
240
|
+
app.${method}('${analysis.route}',${validators}
|
|
241
|
+
async (c) => {
|
|
242
|
+
const headerValues = c.req.header();
|
|
243
|
+
const header = Endpoint.createHeaders(headerValues);
|
|
244
|
+
const cookie = Endpoint.createCookies(headerValues.cookie);
|
|
245
|
+
${serviceResolution}
|
|
246
|
+
${authCode}
|
|
247
|
+
|
|
248
|
+
const responseBuilder = new ResponseBuilder();
|
|
249
|
+
const result = await ${exportName}.handler(
|
|
250
|
+
{
|
|
251
|
+
services,
|
|
252
|
+
logger,
|
|
253
|
+
body: ${features.hasBodyValidation ? "(c.req.valid as any)('json')" : "undefined"},
|
|
254
|
+
query: ${features.hasQueryValidation ? "(c.req.valid as any)('query')" : "undefined"},
|
|
255
|
+
params: ${features.hasParamValidation ? "(c.req.valid as any)('param')" : "undefined"},
|
|
256
|
+
session: ${features.hasAuth ? "session" : "undefined"},
|
|
257
|
+
header,
|
|
258
|
+
cookie,
|
|
259
|
+
auditor: undefined,
|
|
260
|
+
db,
|
|
261
|
+
} as any,
|
|
262
|
+
responseBuilder,
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
let data = result;
|
|
266
|
+
let metadata = responseBuilder.getMetadata();
|
|
267
|
+
|
|
268
|
+
if (Endpoint.hasMetadata(result)) {
|
|
269
|
+
data = result.data;
|
|
270
|
+
metadata = result.metadata;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
${features.hasOutputValidation ? `const output = ${exportName}.outputSchema
|
|
274
|
+
? await ${exportName}.parseOutput(data)
|
|
275
|
+
: data;` : "const output = data;"}
|
|
276
|
+
${eventCode}
|
|
277
|
+
|
|
278
|
+
const status = (metadata.status ?? ${exportName}.status) as any;
|
|
279
|
+
return c.json(output, status);
|
|
280
|
+
}
|
|
281
|
+
);`;
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Generate validators.ts - Shared validator middleware factories
|
|
285
|
+
*/
|
|
286
|
+
function generateValidatorsFile(analyses) {
|
|
287
|
+
const needsBody = analyses.some((a) => a.features.hasBodyValidation);
|
|
288
|
+
const needsQuery = analyses.some((a) => a.features.hasQueryValidation);
|
|
289
|
+
const needsParams = analyses.some((a) => a.features.hasParamValidation);
|
|
290
|
+
if (!needsBody && !needsQuery && !needsParams) return `// No validators needed for this build\nexport {};\n`;
|
|
291
|
+
const exports = [];
|
|
292
|
+
const factories = [];
|
|
293
|
+
if (needsBody) {
|
|
294
|
+
exports.push("validateBody");
|
|
295
|
+
factories.push(`
|
|
296
|
+
export const validateBody = (endpoint: any) =>
|
|
297
|
+
validator('json', async (value, c) => {
|
|
298
|
+
if (!endpoint.input?.body) return undefined;
|
|
299
|
+
const parsed = await Endpoint.validate(endpoint.input.body, value);
|
|
300
|
+
if (parsed.issues) return c.json(parsed.issues, 422);
|
|
301
|
+
return parsed.value;
|
|
302
|
+
});`);
|
|
303
|
+
}
|
|
304
|
+
if (needsQuery) {
|
|
305
|
+
exports.push("validateQuery");
|
|
306
|
+
factories.push(`
|
|
307
|
+
export const validateQuery = (endpoint: any) =>
|
|
308
|
+
validator('query', async (_, c) => {
|
|
309
|
+
if (!endpoint.input?.query) return undefined;
|
|
310
|
+
const rawQuery = Object.fromEntries(new URL(c.req.url).searchParams);
|
|
311
|
+
const parsed = await Endpoint.validate(endpoint.input.query, rawQuery);
|
|
312
|
+
if (parsed.issues) return c.json(parsed.issues, 422);
|
|
313
|
+
return parsed.value;
|
|
314
|
+
});`);
|
|
315
|
+
}
|
|
316
|
+
if (needsParams) {
|
|
317
|
+
exports.push("validateParams");
|
|
318
|
+
factories.push(`
|
|
319
|
+
export const validateParams = (endpoint: any) =>
|
|
320
|
+
validator('param', async (params, c) => {
|
|
321
|
+
if (!endpoint.input?.params) return undefined;
|
|
322
|
+
const parsed = await Endpoint.validate(endpoint.input.params, params);
|
|
323
|
+
if (parsed.issues) return c.json(parsed.issues, 422);
|
|
324
|
+
return parsed.value;
|
|
325
|
+
});`);
|
|
326
|
+
}
|
|
327
|
+
return `/**
|
|
328
|
+
* Generated validator middleware factories
|
|
329
|
+
* Shared across all endpoint tiers that need validation
|
|
330
|
+
*/
|
|
331
|
+
import { validator } from 'hono/validator';
|
|
332
|
+
import { Endpoint } from '@geekmidas/constructs/endpoints';
|
|
333
|
+
${factories.join("\n")}
|
|
334
|
+
`;
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Generate a standalone minimal endpoint file
|
|
338
|
+
*/
|
|
339
|
+
function generateMinimalEndpointFile(analysis, endpointImport) {
|
|
340
|
+
const { exportName, features } = analysis;
|
|
341
|
+
const needsValidators = features.hasBodyValidation || features.hasQueryValidation || features.hasParamValidation;
|
|
342
|
+
const validatorImport = needsValidators ? generateValidatorImportsForEndpoint(analysis) : "";
|
|
343
|
+
const handler = generateMinimalHandler(analysis);
|
|
344
|
+
return `/**
|
|
345
|
+
* Minimal endpoint: ${analysis.route} (${analysis.method})
|
|
346
|
+
* Near-raw-Hono performance
|
|
347
|
+
*/
|
|
348
|
+
import type { Hono } from 'hono';
|
|
349
|
+
import type { Logger } from '@geekmidas/logger';
|
|
350
|
+
import { Endpoint } from '@geekmidas/constructs/endpoints';
|
|
351
|
+
${validatorImport}
|
|
352
|
+
import { ${exportName} } from '${endpointImport.importPath}';
|
|
353
|
+
|
|
354
|
+
export function setup${capitalize(exportName)}(
|
|
355
|
+
app: Hono,
|
|
356
|
+
logger: Logger,
|
|
357
|
+
): void {
|
|
358
|
+
${handler}
|
|
359
|
+
}
|
|
360
|
+
`;
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Generate a standalone standard endpoint file
|
|
364
|
+
*/
|
|
365
|
+
function generateStandardEndpointFile(analysis, endpointImport) {
|
|
366
|
+
const { exportName, features } = analysis;
|
|
367
|
+
const needsValidators = features.hasBodyValidation || features.hasQueryValidation || features.hasParamValidation;
|
|
368
|
+
const validatorImport = needsValidators ? generateValidatorImportsForEndpoint(analysis) : "";
|
|
369
|
+
const eventsImport = features.hasEvents ? `import { publishConstructEvents } from '@geekmidas/constructs/endpoints';` : "";
|
|
370
|
+
const handler = generateStandardHandler(analysis);
|
|
371
|
+
return `/**
|
|
372
|
+
* Standard endpoint: ${analysis.route} (${analysis.method})
|
|
373
|
+
* Auth and/or services enabled
|
|
374
|
+
*/
|
|
375
|
+
import type { Hono } from 'hono';
|
|
376
|
+
import type { Logger } from '@geekmidas/logger';
|
|
377
|
+
import type { ServiceDiscovery } from '@geekmidas/services';
|
|
378
|
+
import { Endpoint, ResponseBuilder } from '@geekmidas/constructs/endpoints';
|
|
379
|
+
${eventsImport}
|
|
380
|
+
${validatorImport}
|
|
381
|
+
import { ${exportName} } from '${endpointImport.importPath}';
|
|
382
|
+
|
|
383
|
+
export function setup${capitalize(exportName)}(
|
|
384
|
+
app: Hono,
|
|
385
|
+
serviceDiscovery: ServiceDiscovery<any, any>,
|
|
386
|
+
logger: Logger,
|
|
387
|
+
): void {
|
|
388
|
+
${handler}
|
|
389
|
+
}
|
|
390
|
+
`;
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Generate a standalone full endpoint file
|
|
394
|
+
*/
|
|
395
|
+
function generateFullEndpointFile(analysis, endpointImport) {
|
|
396
|
+
const { exportName } = analysis;
|
|
397
|
+
return `/**
|
|
398
|
+
* Full endpoint: ${analysis.route} (${analysis.method})
|
|
399
|
+
* Complex features: audits, RLS, rate limiting
|
|
400
|
+
*/
|
|
401
|
+
import type { Hono } from 'hono';
|
|
402
|
+
import type { ServiceDiscovery } from '@geekmidas/services';
|
|
403
|
+
import { HonoEndpoint } from '@geekmidas/constructs/hono';
|
|
404
|
+
import { ${exportName} } from '${endpointImport.importPath}';
|
|
405
|
+
|
|
406
|
+
export function setup${capitalize(exportName)}(
|
|
407
|
+
app: Hono,
|
|
408
|
+
serviceDiscovery: ServiceDiscovery<any, any>,
|
|
409
|
+
openApiOptions: any,
|
|
410
|
+
): void {
|
|
411
|
+
HonoEndpoint.addRoutes([${exportName}] as any, serviceDiscovery, app, openApiOptions);
|
|
412
|
+
}
|
|
413
|
+
`;
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Generate tier index file that imports and calls all endpoint setup functions
|
|
417
|
+
*/
|
|
418
|
+
function generateTierIndexFile(tier, analyses) {
|
|
419
|
+
const tierEndpoints = analyses.filter((a) => a.tier === tier);
|
|
420
|
+
if (tierEndpoints.length === 0) {
|
|
421
|
+
if (tier === "minimal") return `// No minimal-tier endpoints in this build
|
|
422
|
+
import type { Hono } from 'hono';
|
|
423
|
+
import type { Logger } from '@geekmidas/logger';
|
|
424
|
+
|
|
425
|
+
export function setupMinimalEndpoints(
|
|
426
|
+
_app: Hono,
|
|
427
|
+
_logger: Logger,
|
|
428
|
+
): void {
|
|
429
|
+
// No minimal endpoints
|
|
430
|
+
}
|
|
431
|
+
`;
|
|
432
|
+
if (tier === "standard") return `// No standard-tier endpoints in this build
|
|
433
|
+
import type { Hono } from 'hono';
|
|
434
|
+
import type { Logger } from '@geekmidas/logger';
|
|
435
|
+
import type { ServiceDiscovery } from '@geekmidas/services';
|
|
436
|
+
|
|
437
|
+
export function setupStandardEndpoints(
|
|
438
|
+
_app: Hono,
|
|
439
|
+
_serviceDiscovery: ServiceDiscovery<any, any>,
|
|
440
|
+
_logger: Logger,
|
|
441
|
+
): void {
|
|
442
|
+
// No standard endpoints
|
|
443
|
+
}
|
|
444
|
+
`;
|
|
445
|
+
return `// No full-tier endpoints in this build
|
|
446
|
+
import type { Hono } from 'hono';
|
|
447
|
+
import type { ServiceDiscovery } from '@geekmidas/services';
|
|
448
|
+
|
|
449
|
+
export function setupFullEndpoints(
|
|
450
|
+
_app: Hono,
|
|
451
|
+
_serviceDiscovery: ServiceDiscovery<any, any>,
|
|
452
|
+
_enableOpenApi: boolean,
|
|
453
|
+
): void {
|
|
454
|
+
// No full endpoints
|
|
455
|
+
}
|
|
456
|
+
`;
|
|
457
|
+
}
|
|
458
|
+
const imports = tierEndpoints.map((a) => `import { setup${capitalize(a.exportName)} } from './${a.exportName}.js';`).join("\n");
|
|
459
|
+
if (tier === "minimal") {
|
|
460
|
+
const calls$1 = tierEndpoints.map((a) => ` setup${capitalize(a.exportName)}(app, logger);`).join("\n");
|
|
461
|
+
return `/**
|
|
462
|
+
* Minimal-tier endpoint index (${tierEndpoints.length} endpoints)
|
|
463
|
+
* Near-raw-Hono performance
|
|
464
|
+
*/
|
|
465
|
+
import type { Hono } from 'hono';
|
|
466
|
+
import type { Logger } from '@geekmidas/logger';
|
|
467
|
+
${imports}
|
|
468
|
+
|
|
469
|
+
export function setupMinimalEndpoints(
|
|
470
|
+
app: Hono,
|
|
471
|
+
logger: Logger,
|
|
472
|
+
): void {
|
|
473
|
+
${calls$1}
|
|
474
|
+
}
|
|
475
|
+
`;
|
|
476
|
+
}
|
|
477
|
+
if (tier === "standard") {
|
|
478
|
+
const calls$1 = tierEndpoints.map((a) => ` setup${capitalize(a.exportName)}(app, serviceDiscovery, logger);`).join("\n");
|
|
479
|
+
return `/**
|
|
480
|
+
* Standard-tier endpoint index (${tierEndpoints.length} endpoints)
|
|
481
|
+
* Auth and/or services enabled
|
|
482
|
+
*/
|
|
483
|
+
import type { Hono } from 'hono';
|
|
484
|
+
import type { Logger } from '@geekmidas/logger';
|
|
485
|
+
import type { ServiceDiscovery } from '@geekmidas/services';
|
|
486
|
+
${imports}
|
|
487
|
+
|
|
488
|
+
export function setupStandardEndpoints(
|
|
489
|
+
app: Hono,
|
|
490
|
+
serviceDiscovery: ServiceDiscovery<any, any>,
|
|
491
|
+
logger: Logger,
|
|
492
|
+
): void {
|
|
493
|
+
${calls$1}
|
|
494
|
+
}
|
|
495
|
+
`;
|
|
496
|
+
}
|
|
497
|
+
const calls = tierEndpoints.map((a) => ` setup${capitalize(a.exportName)}(app, serviceDiscovery, openApiOptions);`).join("\n");
|
|
498
|
+
return `/**
|
|
499
|
+
* Full-tier endpoint index (${tierEndpoints.length} endpoints)
|
|
500
|
+
* Complex features: audits, RLS, rate limiting
|
|
501
|
+
*/
|
|
502
|
+
import type { Hono } from 'hono';
|
|
503
|
+
import type { ServiceDiscovery } from '@geekmidas/services';
|
|
504
|
+
${imports}
|
|
505
|
+
|
|
506
|
+
export function setupFullEndpoints(
|
|
507
|
+
app: Hono,
|
|
508
|
+
serviceDiscovery: ServiceDiscovery<any, any>,
|
|
509
|
+
enableOpenApi: boolean,
|
|
510
|
+
): void {
|
|
511
|
+
const openApiOptions: any = enableOpenApi ? {
|
|
512
|
+
docsPath: '/__docs',
|
|
513
|
+
openApiOptions: {
|
|
514
|
+
title: 'API Documentation',
|
|
515
|
+
version: '1.0.0',
|
|
516
|
+
description: 'Generated API documentation'
|
|
517
|
+
}
|
|
518
|
+
} : { docsPath: false };
|
|
519
|
+
|
|
520
|
+
${calls}
|
|
521
|
+
}
|
|
522
|
+
`;
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* Generate validator imports for a single endpoint
|
|
526
|
+
*/
|
|
527
|
+
function generateValidatorImportsForEndpoint(analysis) {
|
|
528
|
+
const imports = [];
|
|
529
|
+
if (analysis.features.hasBodyValidation) imports.push("validateBody");
|
|
530
|
+
if (analysis.features.hasQueryValidation) imports.push("validateQuery");
|
|
531
|
+
if (analysis.features.hasParamValidation) imports.push("validateParams");
|
|
532
|
+
if (imports.length === 0) return "";
|
|
533
|
+
return `import { ${imports.join(", ")} } from '../validators.js';`;
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* Capitalize first letter
|
|
537
|
+
*/
|
|
538
|
+
function capitalize(str) {
|
|
539
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
540
|
+
}
|
|
541
|
+
/**
|
|
542
|
+
* Generate all endpoint files with nested folder structure (per-endpoint files)
|
|
543
|
+
*/
|
|
544
|
+
function generateEndpointFilesNested(analyses, endpointImports) {
|
|
545
|
+
const files = {
|
|
546
|
+
"validators.ts": generateValidatorsFile(analyses),
|
|
547
|
+
"minimal/index.ts": generateTierIndexFile("minimal", analyses),
|
|
548
|
+
"standard/index.ts": generateTierIndexFile("standard", analyses),
|
|
549
|
+
"full/index.ts": generateTierIndexFile("full", analyses),
|
|
550
|
+
"index.ts": generateNestedIndexFile(analyses)
|
|
551
|
+
};
|
|
552
|
+
for (const analysis of analyses) {
|
|
553
|
+
const endpointImport = endpointImports.find((i) => i.exportName === analysis.exportName);
|
|
554
|
+
if (!endpointImport) continue;
|
|
555
|
+
const fileName = `${analysis.tier}/${analysis.exportName}.ts`;
|
|
556
|
+
switch (analysis.tier) {
|
|
557
|
+
case "minimal":
|
|
558
|
+
files[fileName] = generateMinimalEndpointFile(analysis, endpointImport);
|
|
559
|
+
break;
|
|
560
|
+
case "standard":
|
|
561
|
+
files[fileName] = generateStandardEndpointFile(analysis, endpointImport);
|
|
562
|
+
break;
|
|
563
|
+
case "full":
|
|
564
|
+
files[fileName] = generateFullEndpointFile(analysis, endpointImport);
|
|
565
|
+
break;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
return files;
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* Generate index.ts for nested structure
|
|
572
|
+
*/
|
|
573
|
+
function generateNestedIndexFile(analyses) {
|
|
574
|
+
const minimalCount = analyses.filter((a) => a.tier === "minimal").length;
|
|
575
|
+
const standardCount = analyses.filter((a) => a.tier === "standard").length;
|
|
576
|
+
const fullCount = analyses.filter((a) => a.tier === "full").length;
|
|
577
|
+
return `/**
|
|
578
|
+
* Generated optimized endpoints
|
|
579
|
+
*
|
|
580
|
+
* Build-time optimization tiers:
|
|
581
|
+
* - minimal: ${minimalCount} endpoints (near-raw-Hono)
|
|
582
|
+
* - standard: ${standardCount} endpoints (auth/services)
|
|
583
|
+
* - full: ${fullCount} endpoints (audits/rls/rate-limit)
|
|
584
|
+
*/
|
|
585
|
+
import type { EnvironmentParser } from '@geekmidas/envkit';
|
|
586
|
+
import type { Logger } from '@geekmidas/logger';
|
|
587
|
+
import type { Hono } from 'hono';
|
|
588
|
+
import { ServiceDiscovery } from '@geekmidas/services';
|
|
589
|
+
import { setupMinimalEndpoints } from './minimal/index.js';
|
|
590
|
+
import { setupStandardEndpoints } from './standard/index.js';
|
|
591
|
+
import { setupFullEndpoints } from './full/index.js';
|
|
592
|
+
|
|
593
|
+
export async function setupEndpoints(
|
|
594
|
+
app: Hono,
|
|
595
|
+
envParser: EnvironmentParser<any>,
|
|
596
|
+
logger: Logger,
|
|
597
|
+
enableOpenApi: boolean = false,
|
|
598
|
+
): Promise<void> {
|
|
599
|
+
const serviceDiscovery = ServiceDiscovery.getInstance(envParser);
|
|
600
|
+
|
|
601
|
+
// Minimal handlers (${minimalCount} endpoints) - near-raw-Hono performance
|
|
602
|
+
setupMinimalEndpoints(app, logger);
|
|
603
|
+
|
|
604
|
+
// Standard handlers (${standardCount} endpoints) - auth/services
|
|
605
|
+
setupStandardEndpoints(app, serviceDiscovery, logger);
|
|
606
|
+
|
|
607
|
+
// Full handlers (${fullCount} endpoints) - audits/rls/rate-limit
|
|
608
|
+
setupFullEndpoints(app, serviceDiscovery, enableOpenApi);
|
|
609
|
+
|
|
610
|
+
// Add Swagger UI if OpenAPI is enabled
|
|
611
|
+
if (enableOpenApi) {
|
|
612
|
+
try {
|
|
613
|
+
const { swaggerUI } = await import('@hono/swagger-ui');
|
|
614
|
+
app.get('/__docs/ui', swaggerUI({ url: '/__docs' }));
|
|
615
|
+
} catch {
|
|
616
|
+
// @hono/swagger-ui not installed, skip Swagger UI
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
`;
|
|
621
|
+
}
|
|
622
|
+
|
|
44
623
|
//#endregion
|
|
45
624
|
//#region src/generators/EndpointGenerator.ts
|
|
46
625
|
var EndpointGenerator = class extends ConstructGenerator {
|
|
@@ -59,19 +638,19 @@ var EndpointGenerator = class extends ConstructGenerator {
|
|
|
59
638
|
routes.push({
|
|
60
639
|
path: "*",
|
|
61
640
|
method: "ALL",
|
|
62
|
-
handler: relative
|
|
641
|
+
handler: relative(process.cwd(), appFile),
|
|
63
642
|
authorizer: "none"
|
|
64
643
|
});
|
|
65
644
|
logger.log(`Generated server with ${constructs.length} endpoints${enableOpenApi ? " (OpenAPI enabled)" : ""}`);
|
|
66
645
|
} else if (provider === "aws-lambda") {
|
|
67
|
-
const routesDir = join
|
|
646
|
+
const routesDir = join(outputDir, "routes");
|
|
68
647
|
await mkdir(routesDir, { recursive: true });
|
|
69
648
|
for (const { key, construct, path } of constructs) {
|
|
70
649
|
const handlerFile = await this.generateHandlerFile(routesDir, path.relative, key, "aws-apigatewayv2", construct, context);
|
|
71
650
|
const routeInfo = {
|
|
72
651
|
path: construct._path,
|
|
73
652
|
method: construct.method,
|
|
74
|
-
handler: relative
|
|
653
|
+
handler: relative(process.cwd(), handlerFile).replace(/\.ts$/, ".handler"),
|
|
75
654
|
timeout: construct.timeout,
|
|
76
655
|
memorySize: construct.memorySize,
|
|
77
656
|
environment: await construct.getEnvironment(),
|
|
@@ -85,7 +664,7 @@ var EndpointGenerator = class extends ConstructGenerator {
|
|
|
85
664
|
const routeInfo = {
|
|
86
665
|
path: construct._path,
|
|
87
666
|
method: construct.method,
|
|
88
|
-
handler: relative
|
|
667
|
+
handler: relative(process.cwd(), handlerFile).replace(/\.ts$/, ".handler"),
|
|
89
668
|
timeout: construct.timeout,
|
|
90
669
|
memorySize: construct.memorySize,
|
|
91
670
|
environment: await construct.getEnvironment(),
|
|
@@ -98,10 +677,10 @@ var EndpointGenerator = class extends ConstructGenerator {
|
|
|
98
677
|
}
|
|
99
678
|
async generateHandlerFile(outputDir, sourceFile, exportName, provider, _endpoint, context) {
|
|
100
679
|
const handlerFileName = `${exportName}.ts`;
|
|
101
|
-
const handlerPath = join
|
|
102
|
-
const relativePath = relative
|
|
680
|
+
const handlerPath = join(outputDir, handlerFileName);
|
|
681
|
+
const relativePath = relative(dirname(handlerPath), sourceFile);
|
|
103
682
|
const importPath = relativePath.replace(/\.ts$/, ".js");
|
|
104
|
-
const relativeEnvParserPath = relative
|
|
683
|
+
const relativeEnvParserPath = relative(dirname(handlerPath), context.envParserPath);
|
|
105
684
|
let content;
|
|
106
685
|
switch (provider) {
|
|
107
686
|
case "aws-apigatewayv1":
|
|
@@ -118,25 +697,26 @@ var EndpointGenerator = class extends ConstructGenerator {
|
|
|
118
697
|
await writeFile(handlerPath, content);
|
|
119
698
|
return handlerPath;
|
|
120
699
|
}
|
|
121
|
-
async generateEndpointsFile(outputDir, endpoints,
|
|
700
|
+
async generateEndpointsFile(outputDir, endpoints, context) {
|
|
122
701
|
const endpointsFileName = "endpoints.ts";
|
|
123
|
-
const endpointsPath = join
|
|
702
|
+
const endpointsPath = join(outputDir, endpointsFileName);
|
|
124
703
|
const importsByFile = /* @__PURE__ */ new Map();
|
|
125
704
|
for (const { path, key } of endpoints) {
|
|
126
|
-
const relativePath = relative
|
|
705
|
+
const relativePath = relative(dirname(endpointsPath), path.relative);
|
|
127
706
|
const importPath = relativePath.replace(/\.ts$/, ".js");
|
|
128
707
|
if (!importsByFile.has(importPath)) importsByFile.set(importPath, []);
|
|
129
|
-
importsByFile.get(importPath)
|
|
708
|
+
importsByFile.get(importPath)?.push(key);
|
|
130
709
|
}
|
|
131
|
-
const
|
|
710
|
+
const endpointImports = Array.from(importsByFile.entries()).map(([importPath, exports]) => `import { ${exports.join(", ")} } from '${importPath}';`).join("\n");
|
|
132
711
|
const allExportNames = endpoints.map(({ key }) => key);
|
|
712
|
+
if (context.production?.enabled && context.production.optimizedHandlers) return this.generateOptimizedEndpointsFile(endpointsPath, endpoints, endpointImports, allExportNames);
|
|
133
713
|
const content = `import type { EnvironmentParser } from '@geekmidas/envkit';
|
|
134
714
|
import type { Logger } from '@geekmidas/logger';
|
|
135
715
|
import { HonoEndpoint } from '@geekmidas/constructs/hono';
|
|
136
716
|
import { Endpoint } from '@geekmidas/constructs/endpoints';
|
|
137
717
|
import { ServiceDiscovery } from '@geekmidas/services';
|
|
138
718
|
import type { Hono } from 'hono';
|
|
139
|
-
${
|
|
719
|
+
${endpointImports}
|
|
140
720
|
|
|
141
721
|
const endpoints: Endpoint<any, any, any, any, any, any, any, any, any, any, any, any, any, any>[] = [
|
|
142
722
|
${allExportNames.join(",\n ")}
|
|
@@ -148,10 +728,7 @@ export async function setupEndpoints(
|
|
|
148
728
|
logger: Logger,
|
|
149
729
|
enableOpenApi: boolean = true,
|
|
150
730
|
): Promise<void> {
|
|
151
|
-
const serviceDiscovery = ServiceDiscovery.getInstance(
|
|
152
|
-
logger,
|
|
153
|
-
envParser
|
|
154
|
-
);
|
|
731
|
+
const serviceDiscovery = ServiceDiscovery.getInstance(envParser);
|
|
155
732
|
|
|
156
733
|
// Configure OpenAPI options based on enableOpenApi flag
|
|
157
734
|
const openApiOptions: any = enableOpenApi ? {
|
|
@@ -179,11 +756,49 @@ export async function setupEndpoints(
|
|
|
179
756
|
await writeFile(endpointsPath, content);
|
|
180
757
|
return endpointsPath;
|
|
181
758
|
}
|
|
759
|
+
/**
|
|
760
|
+
* Generate optimized endpoints files with nested folder structure (per-endpoint files)
|
|
761
|
+
*/
|
|
762
|
+
async generateOptimizedEndpointsFile(endpointsPath, endpoints, _endpointImports, _allExportNames) {
|
|
763
|
+
const logger = console;
|
|
764
|
+
const outputDir = dirname(endpointsPath);
|
|
765
|
+
const endpointsDir = join(outputDir, "endpoints");
|
|
766
|
+
await mkdir(join(endpointsDir, "minimal"), { recursive: true });
|
|
767
|
+
await mkdir(join(endpointsDir, "standard"), { recursive: true });
|
|
768
|
+
await mkdir(join(endpointsDir, "full"), { recursive: true });
|
|
769
|
+
const analyses = endpoints.map(({ key, construct }) => analyzeEndpoint(construct, key));
|
|
770
|
+
const endpointImports = endpoints.map(({ key, path }) => {
|
|
771
|
+
const tierDir = join(endpointsDir, "standard");
|
|
772
|
+
const relativePath = relative(tierDir, path.relative);
|
|
773
|
+
const importPath = relativePath.replace(/\.ts$/, ".js");
|
|
774
|
+
return {
|
|
775
|
+
exportName: key,
|
|
776
|
+
importPath
|
|
777
|
+
};
|
|
778
|
+
});
|
|
779
|
+
const summary = summarizeAnalysis(analyses);
|
|
780
|
+
logger.log(`\n📊 Endpoint Analysis:`);
|
|
781
|
+
logger.log(` Total: ${summary.total} endpoints`);
|
|
782
|
+
logger.log(` - Minimal (near-raw-Hono): ${summary.byTier.minimal} endpoints`);
|
|
783
|
+
logger.log(` - Standard (auth/services): ${summary.byTier.standard} endpoints`);
|
|
784
|
+
logger.log(` - Full (audits/rls/rate-limit): ${summary.byTier.full} endpoints`);
|
|
785
|
+
const files = generateEndpointFilesNested(analyses, endpointImports);
|
|
786
|
+
for (const [filename, content] of Object.entries(files)) {
|
|
787
|
+
const filePath = join(endpointsDir, filename);
|
|
788
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
789
|
+
await writeFile(filePath, content);
|
|
790
|
+
}
|
|
791
|
+
const endpointFiles = Object.keys(files).filter((f) => !f.endsWith("index.ts") && !f.endsWith("validators.ts")).length;
|
|
792
|
+
const indexFiles = Object.keys(files).filter((f) => f.endsWith("index.ts")).length;
|
|
793
|
+
logger.log(` Generated ${endpointFiles} endpoint files + ${indexFiles} index files + validators.ts`);
|
|
794
|
+
return join(endpointsDir, "index.ts");
|
|
795
|
+
}
|
|
182
796
|
async generateAppFile(outputDir, context) {
|
|
797
|
+
if (context.production?.enabled) return this.generateProductionAppFile(outputDir, context);
|
|
183
798
|
const appFileName = "app.ts";
|
|
184
|
-
const appPath = join
|
|
185
|
-
const relativeLoggerPath = relative
|
|
186
|
-
const relativeEnvParserPath = relative
|
|
799
|
+
const appPath = join(outputDir, appFileName);
|
|
800
|
+
const relativeLoggerPath = relative(dirname(appPath), context.loggerPath);
|
|
801
|
+
const relativeEnvParserPath = relative(dirname(appPath), context.envParserPath);
|
|
187
802
|
const telescopeEnabled = context.telescope?.enabled;
|
|
188
803
|
const telescopeWebSocketEnabled = context.telescope?.websocket;
|
|
189
804
|
const usesExternalTelescope = !!context.telescope?.telescopePath;
|
|
@@ -191,15 +806,15 @@ export async function setupEndpoints(
|
|
|
191
806
|
const usesExternalStudio = !!context.studio?.studioPath;
|
|
192
807
|
let telescopeImports = "";
|
|
193
808
|
if (telescopeEnabled) if (usesExternalTelescope) {
|
|
194
|
-
const relativeTelescopePath = relative
|
|
195
|
-
telescopeImports = `import ${context.telescope
|
|
809
|
+
const relativeTelescopePath = relative(dirname(appPath), context.telescope?.telescopePath);
|
|
810
|
+
telescopeImports = `import ${context.telescope?.telescopeImportPattern} from '${relativeTelescopePath}';
|
|
196
811
|
import { createMiddleware, createUI } from '@geekmidas/telescope/hono';`;
|
|
197
812
|
} else telescopeImports = `import { Telescope, InMemoryStorage } from '@geekmidas/telescope';
|
|
198
813
|
import { createMiddleware, createUI } from '@geekmidas/telescope/hono';`;
|
|
199
814
|
let studioImports = "";
|
|
200
815
|
if (studioEnabled) if (usesExternalStudio) {
|
|
201
|
-
const relativeStudioPath = relative
|
|
202
|
-
studioImports = `import ${context.studio
|
|
816
|
+
const relativeStudioPath = relative(dirname(appPath), context.studio?.studioPath);
|
|
817
|
+
studioImports = `import ${context.studio?.studioImportPattern} from '${relativeStudioPath}';
|
|
203
818
|
import { createStudioApp } from '@geekmidas/studio/server/hono';`;
|
|
204
819
|
} else studioImports = `// Studio requires a configured instance - use studio config path
|
|
205
820
|
// import { createStudioApp } from '@geekmidas/studio/server/hono';`;
|
|
@@ -207,7 +822,7 @@ import { createStudioApp } from '@geekmidas/studio/server/hono';`;
|
|
|
207
822
|
let beforeSetupCall = "";
|
|
208
823
|
let afterSetupCall = "";
|
|
209
824
|
if (context.hooks?.serverHooksPath) {
|
|
210
|
-
const relativeHooksPath = relative
|
|
825
|
+
const relativeHooksPath = relative(dirname(appPath), context.hooks.serverHooksPath);
|
|
211
826
|
hooksImports = `import * as serverHooks from '${relativeHooksPath}';`;
|
|
212
827
|
beforeSetupCall = `
|
|
213
828
|
// Call beforeSetup hook if defined
|
|
@@ -228,7 +843,7 @@ import { createStudioApp } from '@geekmidas/studio/server/hono';`;
|
|
|
228
843
|
const { createNodeWebSocket } = await import('@hono/node-ws');
|
|
229
844
|
const { injectWebSocket, upgradeWebSocket } = createNodeWebSocket({ app: honoApp });
|
|
230
845
|
// Add WebSocket route directly to main app (sub-app routes don't support WS upgrade)
|
|
231
|
-
honoApp.get('${context.telescope
|
|
846
|
+
honoApp.get('${context.telescope?.path}/ws', upgradeWebSocket(() => ({
|
|
232
847
|
onOpen: (_event: Event, ws: any) => {
|
|
233
848
|
telescope.addWsClient(ws);
|
|
234
849
|
},
|
|
@@ -261,16 +876,16 @@ ${telescopeWebSocketSetupCode}
|
|
|
261
876
|
|
|
262
877
|
// Mount telescope UI
|
|
263
878
|
const telescopeUI = createUI(telescope);
|
|
264
|
-
honoApp.route('${context.telescope
|
|
879
|
+
honoApp.route('${context.telescope?.path}', telescopeUI);
|
|
265
880
|
`;
|
|
266
881
|
else telescopeSetup = `
|
|
267
882
|
// Setup Telescope for debugging/monitoring
|
|
268
|
-
const telescopeStorage = new InMemoryStorage({ maxEntries: ${context.telescope
|
|
883
|
+
const telescopeStorage = new InMemoryStorage({ maxEntries: ${context.telescope?.maxEntries} });
|
|
269
884
|
const telescope = new Telescope({
|
|
270
885
|
enabled: true,
|
|
271
|
-
path: '${context.telescope
|
|
272
|
-
ignorePatterns: ${JSON.stringify(context.telescope
|
|
273
|
-
recordBody: ${context.telescope
|
|
886
|
+
path: '${context.telescope?.path}',
|
|
887
|
+
ignorePatterns: ${JSON.stringify(context.telescope?.ignore)},
|
|
888
|
+
recordBody: ${context.telescope?.recordBody},
|
|
274
889
|
storage: telescopeStorage,
|
|
275
890
|
});
|
|
276
891
|
${telescopeWebSocketSetupCode}
|
|
@@ -279,13 +894,13 @@ ${telescopeWebSocketSetupCode}
|
|
|
279
894
|
|
|
280
895
|
// Mount telescope UI
|
|
281
896
|
const telescopeUI = createUI(telescope);
|
|
282
|
-
honoApp.route('${context.telescope
|
|
897
|
+
honoApp.route('${context.telescope?.path}', telescopeUI);
|
|
283
898
|
`;
|
|
284
899
|
let studioSetup = "";
|
|
285
900
|
if (studioEnabled && usesExternalStudio) studioSetup = `
|
|
286
901
|
// Mount Studio data browser UI
|
|
287
902
|
const studioApp = createStudioApp(studio);
|
|
288
|
-
honoApp.route('${context.studio
|
|
903
|
+
honoApp.route('${context.studio?.path}', studioApp);
|
|
289
904
|
`;
|
|
290
905
|
const content = `/**
|
|
291
906
|
* Generated server application
|
|
@@ -414,6 +1029,154 @@ export const handler = adapter.handler;
|
|
|
414
1029
|
export const handler = ${exportName};
|
|
415
1030
|
`;
|
|
416
1031
|
}
|
|
1032
|
+
/**
|
|
1033
|
+
* Generate a production-optimized app.ts file
|
|
1034
|
+
* No dev tools (Telescope, Studio, WebSocket), includes health checks and graceful shutdown
|
|
1035
|
+
*/
|
|
1036
|
+
async generateProductionAppFile(outputDir, context) {
|
|
1037
|
+
const appFileName = "app.ts";
|
|
1038
|
+
const appPath = join(outputDir, appFileName);
|
|
1039
|
+
const relativeLoggerPath = relative(dirname(appPath), context.loggerPath);
|
|
1040
|
+
const relativeEnvParserPath = relative(dirname(appPath), context.envParserPath);
|
|
1041
|
+
const production = context.production;
|
|
1042
|
+
const healthCheckPath = production.healthCheck;
|
|
1043
|
+
const enableGracefulShutdown = production.gracefulShutdown;
|
|
1044
|
+
const enableOpenApi = production.openapi;
|
|
1045
|
+
const includeSubscribers = production.subscribers === "include";
|
|
1046
|
+
let hooksImports = "";
|
|
1047
|
+
let beforeSetupCall = "";
|
|
1048
|
+
let afterSetupCall = "";
|
|
1049
|
+
if (context.hooks?.serverHooksPath) {
|
|
1050
|
+
const relativeHooksPath = relative(dirname(appPath), context.hooks.serverHooksPath);
|
|
1051
|
+
hooksImports = `import * as serverHooks from '${relativeHooksPath}';`;
|
|
1052
|
+
beforeSetupCall = `
|
|
1053
|
+
// Call beforeSetup hook if defined
|
|
1054
|
+
if (typeof serverHooks.beforeSetup === 'function') {
|
|
1055
|
+
await serverHooks.beforeSetup(honoApp, { envParser, logger });
|
|
1056
|
+
}
|
|
1057
|
+
`;
|
|
1058
|
+
afterSetupCall = `
|
|
1059
|
+
// Call afterSetup hook if defined
|
|
1060
|
+
if (typeof serverHooks.afterSetup === 'function') {
|
|
1061
|
+
await serverHooks.afterSetup(honoApp, { envParser, logger });
|
|
1062
|
+
}
|
|
1063
|
+
`;
|
|
1064
|
+
}
|
|
1065
|
+
const subscriberSetup = includeSubscribers ? `
|
|
1066
|
+
// Start subscribers in background
|
|
1067
|
+
await setupSubscribers(envParser, logger).catch((error) => {
|
|
1068
|
+
logger.error({ error }, 'Failed to start subscribers');
|
|
1069
|
+
});
|
|
1070
|
+
` : "";
|
|
1071
|
+
const subscriberImport = includeSubscribers ? `import { setupSubscribers } from './subscribers.js';` : "";
|
|
1072
|
+
const gracefulShutdownCode = enableGracefulShutdown ? `
|
|
1073
|
+
// Graceful shutdown handling
|
|
1074
|
+
let isShuttingDown = false;
|
|
1075
|
+
|
|
1076
|
+
const shutdown = async () => {
|
|
1077
|
+
if (isShuttingDown) return;
|
|
1078
|
+
isShuttingDown = true;
|
|
1079
|
+
logger.info('Graceful shutdown initiated');
|
|
1080
|
+
// Allow in-flight requests to complete (30s timeout)
|
|
1081
|
+
setTimeout(() => process.exit(0), 30000);
|
|
1082
|
+
};
|
|
1083
|
+
|
|
1084
|
+
process.on('SIGTERM', shutdown);
|
|
1085
|
+
process.on('SIGINT', shutdown);
|
|
1086
|
+
` : "";
|
|
1087
|
+
const endpointsImportPath = production.optimizedHandlers ? "./endpoints/index.js" : "./endpoints.js";
|
|
1088
|
+
const content = `/**
|
|
1089
|
+
* Generated production server application
|
|
1090
|
+
*
|
|
1091
|
+
* This is a production-optimized build without dev tools.
|
|
1092
|
+
* - No Telescope debugging dashboard
|
|
1093
|
+
* - No Studio database browser
|
|
1094
|
+
* - No WebSocket updates
|
|
1095
|
+
* - Includes health checks and graceful shutdown
|
|
1096
|
+
*/
|
|
1097
|
+
import { Hono } from 'hono';
|
|
1098
|
+
import type { Hono as HonoType } from 'hono';
|
|
1099
|
+
import { setupEndpoints } from '${endpointsImportPath}';
|
|
1100
|
+
${subscriberImport}
|
|
1101
|
+
import ${context.envParserImportPattern} from '${relativeEnvParserPath}';
|
|
1102
|
+
import ${context.loggerImportPattern} from '${relativeLoggerPath}';
|
|
1103
|
+
${hooksImports}
|
|
1104
|
+
|
|
1105
|
+
export interface ServerApp {
|
|
1106
|
+
app: HonoType;
|
|
1107
|
+
start: (options?: {
|
|
1108
|
+
port?: number;
|
|
1109
|
+
serve: (app: HonoType, port: number) => void | Promise<void>;
|
|
1110
|
+
}) => Promise<void>;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
/**
|
|
1114
|
+
* Create and configure the production Hono application
|
|
1115
|
+
*/
|
|
1116
|
+
export async function createApp(app?: HonoType): Promise<ServerApp> {
|
|
1117
|
+
const honoApp = app || new Hono();
|
|
1118
|
+
|
|
1119
|
+
// Health check endpoint (always first)
|
|
1120
|
+
honoApp.get('${healthCheckPath}', (c) => c.json({ status: 'ok', timestamp: Date.now() }));
|
|
1121
|
+
honoApp.get('/ready', (c) => c.json({ ready: true }));
|
|
1122
|
+
${beforeSetupCall}
|
|
1123
|
+
// Setup HTTP endpoints (OpenAPI: ${enableOpenApi})
|
|
1124
|
+
await setupEndpoints(honoApp, envParser, logger, ${enableOpenApi});
|
|
1125
|
+
${afterSetupCall}
|
|
1126
|
+
return {
|
|
1127
|
+
app: honoApp,
|
|
1128
|
+
async start(options) {
|
|
1129
|
+
if (!options?.serve) {
|
|
1130
|
+
throw new Error(
|
|
1131
|
+
'serve function is required. Pass a serve function for your runtime:\\n' +
|
|
1132
|
+
' - Bun: (app, port) => Bun.serve({ port, fetch: app.fetch })\\n' +
|
|
1133
|
+
' - Node: (app, port) => serve({ fetch: app.fetch, port })'
|
|
1134
|
+
);
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
const port = options.port ?? Number(process.env.PORT) ?? 3000;
|
|
1138
|
+
${gracefulShutdownCode}${subscriberSetup}
|
|
1139
|
+
logger.info({ port }, 'Starting production server');
|
|
1140
|
+
|
|
1141
|
+
// Start HTTP server using provided serve function
|
|
1142
|
+
await options.serve(honoApp, port);
|
|
1143
|
+
|
|
1144
|
+
logger.info({ port }, 'Production server started');
|
|
1145
|
+
}
|
|
1146
|
+
};
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
// Default export for convenience
|
|
1150
|
+
export default createApp;
|
|
1151
|
+
`;
|
|
1152
|
+
await writeFile(appPath, content);
|
|
1153
|
+
await this.generateProductionServerEntry(outputDir);
|
|
1154
|
+
return appPath;
|
|
1155
|
+
}
|
|
1156
|
+
/**
|
|
1157
|
+
* Generate production server.ts entry point
|
|
1158
|
+
*/
|
|
1159
|
+
async generateProductionServerEntry(outputDir) {
|
|
1160
|
+
const serverPath = join(outputDir, "server.ts");
|
|
1161
|
+
const content = `#!/usr/bin/env node
|
|
1162
|
+
/**
|
|
1163
|
+
* Production server entry point
|
|
1164
|
+
* Generated by 'gkm build --provider server --production'
|
|
1165
|
+
*/
|
|
1166
|
+
import { serve } from '@hono/node-server';
|
|
1167
|
+
import { createApp } from './app.js';
|
|
1168
|
+
|
|
1169
|
+
const port = Number(process.env.PORT) || 3000;
|
|
1170
|
+
|
|
1171
|
+
const { app, start } = await createApp();
|
|
1172
|
+
|
|
1173
|
+
await start({
|
|
1174
|
+
port,
|
|
1175
|
+
serve: (app, port) => serve({ fetch: app.fetch, port }),
|
|
1176
|
+
});
|
|
1177
|
+
`;
|
|
1178
|
+
await writeFile(serverPath, content);
|
|
1179
|
+
}
|
|
417
1180
|
};
|
|
418
1181
|
|
|
419
1182
|
//#endregion
|
|
@@ -946,7 +1709,7 @@ async function generateOpenApi(config, options = {}) {
|
|
|
946
1709
|
return null;
|
|
947
1710
|
}
|
|
948
1711
|
const endpoints = loadedEndpoints.map(({ construct }) => construct);
|
|
949
|
-
const outputPath = join
|
|
1712
|
+
const outputPath = join(process.cwd(), OPENAPI_OUTPUT_PATH);
|
|
950
1713
|
await mkdir(dirname(outputPath), { recursive: true });
|
|
951
1714
|
const tsGenerator = new OpenApiTsGenerator();
|
|
952
1715
|
const tsContent = await tsGenerator.generate(endpoints, {
|
|
@@ -975,4 +1738,4 @@ async function openapiCommand(options = {}) {
|
|
|
975
1738
|
|
|
976
1739
|
//#endregion
|
|
977
1740
|
export { ConstructGenerator, EndpointGenerator, OPENAPI_OUTPUT_PATH, generateOpenApi, openapiCommand, resolveOpenApiConfig };
|
|
978
|
-
//# sourceMappingURL=openapi-
|
|
1741
|
+
//# sourceMappingURL=openapi-BfFlOBCG.mjs.map
|