@geekmidas/cli 0.6.1 → 0.7.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 +119 -0
- package/dist/config-Bq72aj8e.mjs.map +1 -1
- package/dist/config-CFls09Ey.cjs.map +1 -1
- package/dist/config.d.cts +2 -2
- package/dist/config.d.mts +2 -2
- package/dist/index.cjs +2561 -34
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +2556 -29
- package/dist/index.mjs.map +1 -1
- package/dist/openapi-Mwy2_R4W.mjs +957 -0
- package/dist/openapi-Mwy2_R4W.mjs.map +1 -0
- package/dist/{openapi-react-query-_-B3s8v_.mjs → openapi-react-query-CcciaVu5.mjs} +1 -1
- package/dist/{openapi-react-query-_-B3s8v_.mjs.map → openapi-react-query-CcciaVu5.mjs.map} +1 -1
- package/dist/{openapi-react-query-Cp-w8_05.cjs → openapi-react-query-o5iMi8tz.cjs} +1 -1
- package/dist/{openapi-react-query-Cp-w8_05.cjs.map → openapi-react-query-o5iMi8tz.cjs.map} +1 -1
- package/dist/openapi-react-query.cjs +1 -1
- package/dist/openapi-react-query.mjs +1 -1
- package/dist/openapi-tAIbJJU_.cjs +993 -0
- package/dist/openapi-tAIbJJU_.cjs.map +1 -0
- package/dist/openapi.cjs +1 -4
- package/dist/openapi.d.cts +1 -1
- package/dist/openapi.d.mts +1 -1
- package/dist/openapi.mjs +1 -4
- package/dist/{types-Bi7VzDUZ.d.mts → types-B3TXoj7v.d.mts} +21 -53
- package/dist/{types-KmjzMgu8.d.cts → types-C0hwnSjm.d.cts} +21 -53
- package/package.json +6 -6
- package/src/build/types.ts +13 -0
- package/src/config.ts +1 -0
- package/src/dev/index.ts +71 -8
- package/src/generators/EndpointGenerator.ts +46 -5
- package/src/generators/__tests__/EndpointGenerator.spec.ts +1 -1
- package/src/init/generators/config.ts +6 -1
- package/src/init/generators/package.ts +5 -1
- package/src/init/index.ts +9 -1
- package/src/init/templates/api.ts +31 -0
- package/src/init/templates/index.ts +1 -0
- package/src/init/templates/minimal.ts +83 -0
- package/src/types.ts +19 -0
- package/tsdown.config.ts +6 -0
- package/dist/CronGenerator-CCRYptuT.mjs +0 -55
- package/dist/CronGenerator-CCRYptuT.mjs.map +0 -1
- package/dist/CronGenerator-D4TWXQbh.cjs +0 -61
- package/dist/CronGenerator-D4TWXQbh.cjs.map +0 -1
- package/dist/CronGenerator-DWS3CCZt.d.cts +0 -14
- package/dist/CronGenerator-DZjdkEjI.d.mts +0 -14
- package/dist/EndpointGenerator-DGivkPLT.mjs +0 -335
- package/dist/EndpointGenerator-DGivkPLT.mjs.map +0 -1
- package/dist/EndpointGenerator-Dh7kMtuL.d.mts +0 -19
- package/dist/EndpointGenerator-npWEDoK2.cjs +0 -341
- package/dist/EndpointGenerator-npWEDoK2.cjs.map +0 -1
- package/dist/EndpointGenerator-zBsie_7s.d.cts +0 -19
- package/dist/FunctionGenerator-BmDHo27U.d.mts +0 -14
- package/dist/FunctionGenerator-CVk0h8tO.mjs +0 -54
- package/dist/FunctionGenerator-CVk0h8tO.mjs.map +0 -1
- package/dist/FunctionGenerator-DXjXBxUd.d.cts +0 -14
- package/dist/FunctionGenerator-DYTnyr4c.cjs +0 -60
- package/dist/FunctionGenerator-DYTnyr4c.cjs.map +0 -1
- package/dist/Generator-BGY-2dgI.d.cts +0 -27
- package/dist/Generator-CDt4pB3W.mjs +0 -41
- package/dist/Generator-CDt4pB3W.mjs.map +0 -1
- package/dist/Generator-CLVplqm2.cjs +0 -47
- package/dist/Generator-CLVplqm2.cjs.map +0 -1
- package/dist/Generator-yi9DH5TN.d.mts +0 -27
- package/dist/OpenApiTsGenerator-BVS4pOH7.mjs +0 -495
- package/dist/OpenApiTsGenerator-BVS4pOH7.mjs.map +0 -1
- package/dist/OpenApiTsGenerator-gPIIyppX.cjs +0 -501
- package/dist/OpenApiTsGenerator-gPIIyppX.cjs.map +0 -1
- package/dist/SubscriberGenerator-Bb-z3Kvx.d.cts +0 -15
- package/dist/SubscriberGenerator-CwsXqCpS.d.mts +0 -15
- package/dist/SubscriberGenerator-DABaJXML.mjs +0 -200
- package/dist/SubscriberGenerator-DABaJXML.mjs.map +0 -1
- package/dist/SubscriberGenerator-D_zpNGFr.cjs +0 -206
- package/dist/SubscriberGenerator-D_zpNGFr.cjs.map +0 -1
- package/dist/api-Bp5TIl1R.mjs +0 -167
- package/dist/api-Bp5TIl1R.mjs.map +0 -1
- package/dist/api-D4W9-tdZ.cjs +0 -173
- package/dist/api-D4W9-tdZ.cjs.map +0 -1
- package/dist/build/index.cjs +0 -15
- package/dist/build/index.d.cts +0 -7
- package/dist/build/index.d.mts +0 -7
- package/dist/build/index.mjs +0 -15
- package/dist/build/manifests.cjs +0 -4
- package/dist/build/manifests.d.cts +0 -13
- package/dist/build/manifests.d.mts +0 -13
- package/dist/build/manifests.mjs +0 -3
- package/dist/build/providerResolver.cjs +0 -5
- package/dist/build/providerResolver.d.cts +0 -23
- package/dist/build/providerResolver.d.mts +0 -23
- package/dist/build/providerResolver.mjs +0 -3
- package/dist/build/types.cjs +0 -0
- package/dist/build/types.d.cts +0 -3
- package/dist/build/types.d.mts +0 -3
- package/dist/build/types.mjs +0 -0
- package/dist/build-Cu6Mi0Lf.mjs +0 -87
- package/dist/build-Cu6Mi0Lf.mjs.map +0 -1
- package/dist/build-wmt8ZcmA.cjs +0 -93
- package/dist/build-wmt8ZcmA.cjs.map +0 -1
- package/dist/config-BP1IZynR.cjs +0 -168
- package/dist/config-BP1IZynR.cjs.map +0 -1
- package/dist/config-CIzRhm_D.d.mts +0 -11
- package/dist/config-CvehIYsb.d.cts +0 -11
- package/dist/config-UCK12Lrr.mjs +0 -162
- package/dist/config-UCK12Lrr.mjs.map +0 -1
- package/dist/dev/index.cjs +0 -17
- package/dist/dev/index.d.cts +0 -36
- package/dist/dev/index.d.mts +0 -36
- package/dist/dev/index.mjs +0 -13
- package/dist/dev-BBPWSllq.mjs +0 -348
- package/dist/dev-BBPWSllq.mjs.map +0 -1
- package/dist/dev-C2lCgE53.cjs +0 -378
- package/dist/dev-C2lCgE53.cjs.map +0 -1
- package/dist/docker-2-ipZDOJ.cjs +0 -119
- package/dist/docker-2-ipZDOJ.cjs.map +0 -1
- package/dist/docker-31GNwU3F.mjs +0 -113
- package/dist/docker-31GNwU3F.mjs.map +0 -1
- package/dist/env-CQ3hXAAW.d.mts +0 -11
- package/dist/env-CS0jvg7k.cjs +0 -144
- package/dist/env-CS0jvg7k.cjs.map +0 -1
- package/dist/env-D4YFgMqo.d.cts +0 -11
- package/dist/env-DEeVOvVu.mjs +0 -138
- package/dist/env-DEeVOvVu.mjs.map +0 -1
- package/dist/generators/CronGenerator.cjs +0 -4
- package/dist/generators/CronGenerator.d.cts +0 -5
- package/dist/generators/CronGenerator.d.mts +0 -5
- package/dist/generators/CronGenerator.mjs +0 -4
- package/dist/generators/EndpointGenerator.cjs +0 -4
- package/dist/generators/EndpointGenerator.d.cts +0 -5
- package/dist/generators/EndpointGenerator.d.mts +0 -5
- package/dist/generators/EndpointGenerator.mjs +0 -4
- package/dist/generators/FunctionGenerator.cjs +0 -4
- package/dist/generators/FunctionGenerator.d.cts +0 -5
- package/dist/generators/FunctionGenerator.d.mts +0 -5
- package/dist/generators/FunctionGenerator.mjs +0 -4
- package/dist/generators/Generator.cjs +0 -3
- package/dist/generators/Generator.d.cts +0 -4
- package/dist/generators/Generator.d.mts +0 -4
- package/dist/generators/Generator.mjs +0 -3
- package/dist/generators/OpenApiTsGenerator.cjs +0 -3
- package/dist/generators/OpenApiTsGenerator.d.cts +0 -44
- package/dist/generators/OpenApiTsGenerator.d.mts +0 -44
- package/dist/generators/OpenApiTsGenerator.mjs +0 -3
- package/dist/generators/SubscriberGenerator.cjs +0 -4
- package/dist/generators/SubscriberGenerator.d.cts +0 -5
- package/dist/generators/SubscriberGenerator.d.mts +0 -5
- package/dist/generators/SubscriberGenerator.mjs +0 -4
- package/dist/generators/index.cjs +0 -12
- package/dist/generators/index.d.cts +0 -8
- package/dist/generators/index.d.mts +0 -8
- package/dist/generators/index.mjs +0 -8
- package/dist/generators-3IemvCLk.cjs +0 -0
- package/dist/generators-FNpdfN6J.mjs +0 -0
- package/dist/index-DG6xNQMH.d.cts +0 -81
- package/dist/index-DZgrOOOW.d.mts +0 -81
- package/dist/init/generators/config.cjs +0 -3
- package/dist/init/generators/config.d.cts +0 -3
- package/dist/init/generators/config.d.mts +0 -3
- package/dist/init/generators/config.mjs +0 -3
- package/dist/init/generators/docker.cjs +0 -3
- package/dist/init/generators/docker.d.cts +0 -11
- package/dist/init/generators/docker.d.mts +0 -11
- package/dist/init/generators/docker.mjs +0 -3
- package/dist/init/generators/env.cjs +0 -3
- package/dist/init/generators/env.d.cts +0 -3
- package/dist/init/generators/env.d.mts +0 -3
- package/dist/init/generators/env.mjs +0 -3
- package/dist/init/generators/index.cjs +0 -14
- package/dist/init/generators/index.d.cts +0 -6
- package/dist/init/generators/index.d.mts +0 -6
- package/dist/init/generators/index.mjs +0 -11
- package/dist/init/generators/models.cjs +0 -3
- package/dist/init/generators/models.d.cts +0 -11
- package/dist/init/generators/models.d.mts +0 -11
- package/dist/init/generators/models.mjs +0 -3
- package/dist/init/generators/monorepo.cjs +0 -3
- package/dist/init/generators/monorepo.d.cts +0 -11
- package/dist/init/generators/monorepo.d.mts +0 -11
- package/dist/init/generators/monorepo.mjs +0 -3
- package/dist/init/generators/package.cjs +0 -8
- package/dist/init/generators/package.d.cts +0 -3
- package/dist/init/generators/package.d.mts +0 -3
- package/dist/init/generators/package.mjs +0 -8
- package/dist/init/generators/source.cjs +0 -3
- package/dist/init/generators/source.d.cts +0 -3
- package/dist/init/generators/source.d.mts +0 -3
- package/dist/init/generators/source.mjs +0 -3
- package/dist/init/index.cjs +0 -16
- package/dist/init/index.d.cts +0 -17
- package/dist/init/index.d.mts +0 -17
- package/dist/init/index.mjs +0 -16
- package/dist/init/templates/api.cjs +0 -3
- package/dist/init/templates/api.d.cts +0 -7
- package/dist/init/templates/api.d.mts +0 -7
- package/dist/init/templates/api.mjs +0 -3
- package/dist/init/templates/index.cjs +0 -12
- package/dist/init/templates/index.d.cts +0 -2
- package/dist/init/templates/index.d.mts +0 -2
- package/dist/init/templates/index.mjs +0 -7
- package/dist/init/templates/minimal.cjs +0 -3
- package/dist/init/templates/minimal.d.cts +0 -7
- package/dist/init/templates/minimal.d.mts +0 -7
- package/dist/init/templates/minimal.mjs +0 -3
- package/dist/init/templates/serverless.cjs +0 -3
- package/dist/init/templates/serverless.d.cts +0 -7
- package/dist/init/templates/serverless.d.mts +0 -7
- package/dist/init/templates/serverless.mjs +0 -3
- package/dist/init/templates/worker.cjs +0 -3
- package/dist/init/templates/worker.d.cts +0 -7
- package/dist/init/templates/worker.d.mts +0 -7
- package/dist/init/templates/worker.mjs +0 -3
- package/dist/init/utils.cjs +0 -7
- package/dist/init/utils.d.cts +0 -25
- package/dist/init/utils.d.mts +0 -25
- package/dist/init/utils.mjs +0 -3
- package/dist/init-BMA7xi8r.mjs +0 -161
- package/dist/init-BMA7xi8r.mjs.map +0 -1
- package/dist/init-D-7WEk-b.cjs +0 -167
- package/dist/init-D-7WEk-b.cjs.map +0 -1
- package/dist/manifests-BNKG6AXf.mjs +0 -68
- package/dist/manifests-BNKG6AXf.mjs.map +0 -1
- package/dist/manifests-D13Ej8AE.cjs +0 -80
- package/dist/manifests-D13Ej8AE.cjs.map +0 -1
- package/dist/minimal-BkyASH_C.mjs +0 -93
- package/dist/minimal-BkyASH_C.mjs.map +0 -1
- package/dist/minimal-CSFggzdH.cjs +0 -99
- package/dist/minimal-CSFggzdH.cjs.map +0 -1
- package/dist/models-BWlDfviw.mjs +0 -115
- package/dist/models-BWlDfviw.mjs.map +0 -1
- package/dist/models-BapGSoHC.cjs +0 -121
- package/dist/models-BapGSoHC.cjs.map +0 -1
- package/dist/monorepo-BBOWhkcd.mjs +0 -184
- package/dist/monorepo-BBOWhkcd.mjs.map +0 -1
- package/dist/monorepo-CFtxHeDh.cjs +0 -190
- package/dist/monorepo-CFtxHeDh.cjs.map +0 -1
- package/dist/openapi-DA9RkPJl.mjs +0 -74
- package/dist/openapi-DA9RkPJl.mjs.map +0 -1
- package/dist/openapi-DZH6RQHk.cjs +0 -98
- package/dist/openapi-DZH6RQHk.cjs.map +0 -1
- package/dist/package-6h-7QfJZ.d.cts +0 -11
- package/dist/package-BCe_KvGv.d.mts +0 -11
- package/dist/package-C3If80n1.mjs +0 -57
- package/dist/package-C3If80n1.mjs.map +0 -1
- package/dist/package-Dk8IMBOB.cjs +0 -62
- package/dist/package-Dk8IMBOB.cjs.map +0 -1
- package/dist/providerResolver-DEVKngbC.mjs +0 -96
- package/dist/providerResolver-DEVKngbC.mjs.map +0 -1
- package/dist/providerResolver-DOTbN9jo.cjs +0 -114
- package/dist/providerResolver-DOTbN9jo.cjs.map +0 -1
- package/dist/serverless-AGOS-l3G.cjs +0 -119
- package/dist/serverless-AGOS-l3G.cjs.map +0 -1
- package/dist/serverless-D5HjJByU.mjs +0 -113
- package/dist/serverless-D5HjJByU.mjs.map +0 -1
- package/dist/source-C1cyfHcF.cjs +0 -17
- package/dist/source-C1cyfHcF.cjs.map +0 -1
- package/dist/source-C3LiNUV9.d.mts +0 -11
- package/dist/source-CkQHBpwu.mjs +0 -11
- package/dist/source-CkQHBpwu.mjs.map +0 -1
- package/dist/source-Dtcjbokc.d.cts +0 -11
- package/dist/templates-C0EMmhwb.mjs +0 -88
- package/dist/templates-C0EMmhwb.mjs.map +0 -1
- package/dist/templates-CbgQ9dw0.cjs +0 -123
- package/dist/templates-CbgQ9dw0.cjs.map +0 -1
- package/dist/types-D2xYkOal.d.mts +0 -51
- package/dist/types-DA-r8HWZ.d.cts +0 -51
- package/dist/types.cjs +0 -0
- package/dist/types.d.cts +0 -2
- package/dist/types.d.mts +0 -2
- package/dist/types.mjs +0 -0
- package/dist/utils-CKEzCxc1.mjs +0 -69
- package/dist/utils-CKEzCxc1.mjs.map +0 -1
- package/dist/utils-DSdN2MTt.cjs +0 -99
- package/dist/utils-DSdN2MTt.cjs.map +0 -1
- package/dist/worker-CGhlqNH-.cjs +0 -156
- package/dist/worker-CGhlqNH-.cjs.map +0 -1
- package/dist/worker-CiP420As.mjs +0 -150
- package/dist/worker-CiP420As.mjs.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -1,38 +1,26 @@
|
|
|
1
1
|
#!/usr/bin/env -S npx tsx
|
|
2
2
|
const require_chunk = require('./chunk-CUT6urMc.cjs');
|
|
3
|
-
require('./config-CFls09Ey.cjs');
|
|
4
|
-
require('./
|
|
5
|
-
require('./
|
|
6
|
-
require(
|
|
7
|
-
require('./EndpointGenerator-npWEDoK2.cjs');
|
|
8
|
-
require('./FunctionGenerator-DYTnyr4c.cjs');
|
|
9
|
-
require('./SubscriberGenerator-D_zpNGFr.cjs');
|
|
10
|
-
require('./generators-3IemvCLk.cjs');
|
|
11
|
-
require('./OpenApiTsGenerator-gPIIyppX.cjs');
|
|
12
|
-
const require_openapi = require('./openapi-DZH6RQHk.cjs');
|
|
13
|
-
const require_dev = require('./dev-C2lCgE53.cjs');
|
|
14
|
-
require('./manifests-D13Ej8AE.cjs');
|
|
15
|
-
const require_build = require('./build-wmt8ZcmA.cjs');
|
|
16
|
-
require('./config-BP1IZynR.cjs');
|
|
17
|
-
require('./docker-2-ipZDOJ.cjs');
|
|
18
|
-
require('./env-CS0jvg7k.cjs');
|
|
19
|
-
require('./models-BapGSoHC.cjs');
|
|
20
|
-
require('./monorepo-CFtxHeDh.cjs');
|
|
21
|
-
require('./api-D4W9-tdZ.cjs');
|
|
22
|
-
require('./minimal-CSFggzdH.cjs');
|
|
23
|
-
require('./serverless-AGOS-l3G.cjs');
|
|
24
|
-
require('./worker-CGhlqNH-.cjs');
|
|
25
|
-
require('./templates-CbgQ9dw0.cjs');
|
|
26
|
-
require('./package-Dk8IMBOB.cjs');
|
|
27
|
-
require('./source-C1cyfHcF.cjs');
|
|
28
|
-
require('./utils-DSdN2MTt.cjs');
|
|
29
|
-
const require_init = require('./init-D-7WEk-b.cjs');
|
|
30
|
-
const require_openapi_react_query = require('./openapi-react-query-Cp-w8_05.cjs');
|
|
3
|
+
const require_config = require('./config-CFls09Ey.cjs');
|
|
4
|
+
const require_openapi = require('./openapi-tAIbJJU_.cjs');
|
|
5
|
+
const require_openapi_react_query = require('./openapi-react-query-o5iMi8tz.cjs');
|
|
6
|
+
const path = require_chunk.__toESM(require("path"));
|
|
31
7
|
const commander = require_chunk.__toESM(require("commander"));
|
|
8
|
+
const node_fs_promises = require_chunk.__toESM(require("node:fs/promises"));
|
|
9
|
+
const node_path = require_chunk.__toESM(require("node:path"));
|
|
10
|
+
const node_child_process = require_chunk.__toESM(require("node:child_process"));
|
|
11
|
+
const node_fs = require_chunk.__toESM(require("node:fs"));
|
|
12
|
+
const node_net = require_chunk.__toESM(require("node:net"));
|
|
13
|
+
const chokidar = require_chunk.__toESM(require("chokidar"));
|
|
14
|
+
const dotenv = require_chunk.__toESM(require("dotenv"));
|
|
15
|
+
const fast_glob = require_chunk.__toESM(require("fast-glob"));
|
|
16
|
+
const __geekmidas_constructs_crons = require_chunk.__toESM(require("@geekmidas/constructs/crons"));
|
|
17
|
+
const __geekmidas_constructs_functions = require_chunk.__toESM(require("@geekmidas/constructs/functions"));
|
|
18
|
+
const __geekmidas_constructs_subscribers = require_chunk.__toESM(require("@geekmidas/constructs/subscribers"));
|
|
19
|
+
const prompts = require_chunk.__toESM(require("prompts"));
|
|
32
20
|
|
|
33
21
|
//#region package.json
|
|
34
22
|
var name = "@geekmidas/cli";
|
|
35
|
-
var version = "0.
|
|
23
|
+
var version = "0.7.0";
|
|
36
24
|
var description = "CLI tools for building Lambda handlers, server applications, and generating OpenAPI specs";
|
|
37
25
|
var private$1 = false;
|
|
38
26
|
var type = "module";
|
|
@@ -112,6 +100,2545 @@ var package_default = {
|
|
|
112
100
|
peerDependenciesMeta
|
|
113
101
|
};
|
|
114
102
|
|
|
103
|
+
//#endregion
|
|
104
|
+
//#region src/build/providerResolver.ts
|
|
105
|
+
/**
|
|
106
|
+
* Resolves provider configuration from the new simplified system
|
|
107
|
+
* to the internal legacy format for backward compatibility
|
|
108
|
+
*/
|
|
109
|
+
function resolveProviders(config, options) {
|
|
110
|
+
const providers = [];
|
|
111
|
+
let enableOpenApi = options.enableOpenApi || false;
|
|
112
|
+
if (options.providers) return {
|
|
113
|
+
providers: options.providers,
|
|
114
|
+
enableOpenApi
|
|
115
|
+
};
|
|
116
|
+
if (options.provider) {
|
|
117
|
+
const resolvedProviders = resolveMainProvider(options.provider, config.providers);
|
|
118
|
+
providers.push(...resolvedProviders.providers);
|
|
119
|
+
enableOpenApi = resolvedProviders.enableOpenApi || enableOpenApi;
|
|
120
|
+
} else if (config.providers) {
|
|
121
|
+
const resolvedProviders = resolveAllConfiguredProviders(config.providers);
|
|
122
|
+
providers.push(...resolvedProviders.providers);
|
|
123
|
+
enableOpenApi = resolvedProviders.enableOpenApi || enableOpenApi;
|
|
124
|
+
} else providers.push("aws-apigatewayv2", "aws-lambda");
|
|
125
|
+
return {
|
|
126
|
+
providers: [...new Set(providers)],
|
|
127
|
+
enableOpenApi
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
function resolveMainProvider(mainProvider, providersConfig) {
|
|
131
|
+
const providers = [];
|
|
132
|
+
let enableOpenApi = false;
|
|
133
|
+
if (mainProvider === "aws") {
|
|
134
|
+
const awsConfig = providersConfig?.aws;
|
|
135
|
+
if (awsConfig?.apiGateway) {
|
|
136
|
+
if (isEnabled(awsConfig.apiGateway.v1)) providers.push("aws-apigatewayv1");
|
|
137
|
+
if (isEnabled(awsConfig.apiGateway.v2)) providers.push("aws-apigatewayv2");
|
|
138
|
+
} else providers.push("aws-apigatewayv2");
|
|
139
|
+
if (awsConfig?.lambda) {
|
|
140
|
+
if (isEnabled(awsConfig.lambda.functions) || isEnabled(awsConfig.lambda.crons)) providers.push("aws-lambda");
|
|
141
|
+
} else providers.push("aws-lambda");
|
|
142
|
+
} else if (mainProvider === "server") {
|
|
143
|
+
providers.push("server");
|
|
144
|
+
const serverConfig = providersConfig?.server;
|
|
145
|
+
if (typeof serverConfig === "object" && serverConfig?.enableOpenApi) enableOpenApi = true;
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
providers,
|
|
149
|
+
enableOpenApi
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
function resolveAllConfiguredProviders(providersConfig) {
|
|
153
|
+
const providers = [];
|
|
154
|
+
let enableOpenApi = false;
|
|
155
|
+
if (providersConfig.aws) {
|
|
156
|
+
const awsProviders = resolveMainProvider("aws", providersConfig);
|
|
157
|
+
providers.push(...awsProviders.providers);
|
|
158
|
+
}
|
|
159
|
+
if (providersConfig.server && isEnabled(providersConfig.server)) {
|
|
160
|
+
providers.push("server");
|
|
161
|
+
if (typeof providersConfig.server === "object" && providersConfig.server.enableOpenApi) enableOpenApi = true;
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
providers,
|
|
165
|
+
enableOpenApi
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
function isEnabled(config) {
|
|
169
|
+
if (config === void 0) return false;
|
|
170
|
+
if (typeof config === "boolean") return config;
|
|
171
|
+
return config.enabled !== false;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
//#endregion
|
|
175
|
+
//#region src/generators/CronGenerator.ts
|
|
176
|
+
var CronGenerator = class extends require_openapi.ConstructGenerator {
|
|
177
|
+
async build(context, constructs, outputDir, options) {
|
|
178
|
+
const provider = options?.provider || "aws-lambda";
|
|
179
|
+
const logger$3 = console;
|
|
180
|
+
const cronInfos = [];
|
|
181
|
+
if (constructs.length === 0 || provider !== "aws-lambda") return cronInfos;
|
|
182
|
+
const cronsDir = (0, node_path.join)(outputDir, "crons");
|
|
183
|
+
await (0, node_fs_promises.mkdir)(cronsDir, { recursive: true });
|
|
184
|
+
for (const { key, construct, path: path$1 } of constructs) {
|
|
185
|
+
const handlerFile = await this.generateCronHandler(cronsDir, path$1.relative, key, context);
|
|
186
|
+
cronInfos.push({
|
|
187
|
+
name: key,
|
|
188
|
+
handler: (0, node_path.relative)(process.cwd(), handlerFile).replace(/\.ts$/, ".handler"),
|
|
189
|
+
schedule: construct.schedule || "rate(1 hour)",
|
|
190
|
+
timeout: construct.timeout,
|
|
191
|
+
memorySize: construct.memorySize,
|
|
192
|
+
environment: await construct.getEnvironment()
|
|
193
|
+
});
|
|
194
|
+
logger$3.log(`Generated cron handler: ${key}`);
|
|
195
|
+
}
|
|
196
|
+
return cronInfos;
|
|
197
|
+
}
|
|
198
|
+
isConstruct(value) {
|
|
199
|
+
return __geekmidas_constructs_crons.Cron.isCron(value);
|
|
200
|
+
}
|
|
201
|
+
async generateCronHandler(outputDir, sourceFile, exportName, context) {
|
|
202
|
+
const handlerFileName = `${exportName}.ts`;
|
|
203
|
+
const handlerPath = (0, node_path.join)(outputDir, handlerFileName);
|
|
204
|
+
const relativePath = (0, node_path.relative)((0, node_path.dirname)(handlerPath), sourceFile);
|
|
205
|
+
const importPath = relativePath.replace(/\.ts$/, ".js");
|
|
206
|
+
const relativeEnvParserPath = (0, node_path.relative)((0, node_path.dirname)(handlerPath), context.envParserPath);
|
|
207
|
+
const relativeLoggerPath = (0, node_path.relative)((0, node_path.dirname)(handlerPath), context.loggerPath);
|
|
208
|
+
const content = `import { AWSScheduledFunction } from '@geekmidas/constructs/crons';
|
|
209
|
+
import { ${exportName} } from '${importPath}';
|
|
210
|
+
import ${context.envParserImportPattern} from '${relativeEnvParserPath}';
|
|
211
|
+
import ${context.loggerImportPattern} from '${relativeLoggerPath}';
|
|
212
|
+
|
|
213
|
+
const adapter = new AWSScheduledFunction(envParser, ${exportName});
|
|
214
|
+
|
|
215
|
+
export const handler = adapter.handler;
|
|
216
|
+
`;
|
|
217
|
+
await (0, node_fs_promises.writeFile)(handlerPath, content);
|
|
218
|
+
return handlerPath;
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
//#endregion
|
|
223
|
+
//#region src/generators/FunctionGenerator.ts
|
|
224
|
+
var FunctionGenerator = class extends require_openapi.ConstructGenerator {
|
|
225
|
+
isConstruct(value) {
|
|
226
|
+
return __geekmidas_constructs_functions.Function.isFunction(value);
|
|
227
|
+
}
|
|
228
|
+
async build(context, constructs, outputDir, options) {
|
|
229
|
+
const provider = options?.provider || "aws-lambda";
|
|
230
|
+
const logger$3 = console;
|
|
231
|
+
const functionInfos = [];
|
|
232
|
+
if (constructs.length === 0 || provider !== "aws-lambda") return functionInfos;
|
|
233
|
+
const functionsDir = (0, node_path.join)(outputDir, "functions");
|
|
234
|
+
await (0, node_fs_promises.mkdir)(functionsDir, { recursive: true });
|
|
235
|
+
for (const { key, construct, path: path$1 } of constructs) {
|
|
236
|
+
const handlerFile = await this.generateFunctionHandler(functionsDir, path$1.relative, key, context);
|
|
237
|
+
functionInfos.push({
|
|
238
|
+
name: key,
|
|
239
|
+
handler: (0, node_path.relative)(process.cwd(), handlerFile).replace(/\.ts$/, ".handler"),
|
|
240
|
+
timeout: construct.timeout,
|
|
241
|
+
memorySize: construct.memorySize,
|
|
242
|
+
environment: await construct.getEnvironment()
|
|
243
|
+
});
|
|
244
|
+
logger$3.log(`Generated function handler: ${key}`);
|
|
245
|
+
}
|
|
246
|
+
return functionInfos;
|
|
247
|
+
}
|
|
248
|
+
async generateFunctionHandler(outputDir, sourceFile, exportName, context) {
|
|
249
|
+
const handlerFileName = `${exportName}.ts`;
|
|
250
|
+
const handlerPath = (0, node_path.join)(outputDir, handlerFileName);
|
|
251
|
+
const relativePath = (0, node_path.relative)((0, node_path.dirname)(handlerPath), sourceFile);
|
|
252
|
+
const importPath = relativePath.replace(/\.ts$/, ".js");
|
|
253
|
+
const relativeEnvParserPath = (0, node_path.relative)((0, node_path.dirname)(handlerPath), context.envParserPath);
|
|
254
|
+
const relativeLoggerPath = (0, node_path.relative)((0, node_path.dirname)(handlerPath), context.loggerPath);
|
|
255
|
+
const content = `import { AWSLambdaFunction } from '@geekmidas/constructs/functions';
|
|
256
|
+
import { ${exportName} } from '${importPath}';
|
|
257
|
+
import ${context.envParserImportPattern} from '${relativeEnvParserPath}';
|
|
258
|
+
import ${context.loggerImportPattern} from '${relativeLoggerPath}';
|
|
259
|
+
|
|
260
|
+
const adapter = new AWSLambdaFunction(envParser, ${exportName});
|
|
261
|
+
|
|
262
|
+
export const handler = adapter.handler;
|
|
263
|
+
`;
|
|
264
|
+
await (0, node_fs_promises.writeFile)(handlerPath, content);
|
|
265
|
+
return handlerPath;
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
//#endregion
|
|
270
|
+
//#region src/generators/SubscriberGenerator.ts
|
|
271
|
+
var SubscriberGenerator = class extends require_openapi.ConstructGenerator {
|
|
272
|
+
isConstruct(value) {
|
|
273
|
+
return __geekmidas_constructs_subscribers.Subscriber.isSubscriber(value);
|
|
274
|
+
}
|
|
275
|
+
async build(context, constructs, outputDir, options) {
|
|
276
|
+
const provider = options?.provider || "aws-lambda";
|
|
277
|
+
const logger$3 = console;
|
|
278
|
+
const subscriberInfos = [];
|
|
279
|
+
if (provider === "server") {
|
|
280
|
+
await this.generateServerSubscribersFile(outputDir, constructs);
|
|
281
|
+
logger$3.log(`Generated server subscribers file with ${constructs.length} subscribers (polling mode)`);
|
|
282
|
+
return subscriberInfos;
|
|
283
|
+
}
|
|
284
|
+
if (constructs.length === 0) return subscriberInfos;
|
|
285
|
+
if (provider !== "aws-lambda") return subscriberInfos;
|
|
286
|
+
const subscribersDir = (0, node_path.join)(outputDir, "subscribers");
|
|
287
|
+
await (0, node_fs_promises.mkdir)(subscribersDir, { recursive: true });
|
|
288
|
+
for (const { key, construct, path: path$1 } of constructs) {
|
|
289
|
+
const handlerFile = await this.generateSubscriberHandler(subscribersDir, path$1.relative, key, construct, context);
|
|
290
|
+
subscriberInfos.push({
|
|
291
|
+
name: key,
|
|
292
|
+
handler: (0, node_path.relative)(process.cwd(), handlerFile).replace(/\.ts$/, ".handler"),
|
|
293
|
+
subscribedEvents: construct.subscribedEvents || [],
|
|
294
|
+
timeout: construct.timeout,
|
|
295
|
+
memorySize: construct.memorySize,
|
|
296
|
+
environment: await construct.getEnvironment()
|
|
297
|
+
});
|
|
298
|
+
logger$3.log(`Generated subscriber handler: ${key}`);
|
|
299
|
+
}
|
|
300
|
+
return subscriberInfos;
|
|
301
|
+
}
|
|
302
|
+
async generateSubscriberHandler(outputDir, sourceFile, exportName, _subscriber, context) {
|
|
303
|
+
const handlerFileName = `${exportName}.ts`;
|
|
304
|
+
const handlerPath = (0, node_path.join)(outputDir, handlerFileName);
|
|
305
|
+
const relativePath = (0, node_path.relative)((0, node_path.dirname)(handlerPath), sourceFile);
|
|
306
|
+
const importPath = relativePath.replace(/\.ts$/, ".js");
|
|
307
|
+
const relativeEnvParserPath = (0, node_path.relative)((0, node_path.dirname)(handlerPath), context.envParserPath);
|
|
308
|
+
const content = `import { AWSLambdaSubscriber } from '@geekmidas/constructs/aws';
|
|
309
|
+
import { ${exportName} } from '${importPath}';
|
|
310
|
+
import ${context.envParserImportPattern} from '${relativeEnvParserPath}';
|
|
311
|
+
|
|
312
|
+
const adapter = new AWSLambdaSubscriber(envParser, ${exportName});
|
|
313
|
+
|
|
314
|
+
export const handler = adapter.handler;
|
|
315
|
+
`;
|
|
316
|
+
await (0, node_fs_promises.writeFile)(handlerPath, content);
|
|
317
|
+
return handlerPath;
|
|
318
|
+
}
|
|
319
|
+
async generateServerSubscribersFile(outputDir, subscribers) {
|
|
320
|
+
await (0, node_fs_promises.mkdir)(outputDir, { recursive: true });
|
|
321
|
+
const subscribersFileName = "subscribers.ts";
|
|
322
|
+
const subscribersPath = (0, node_path.join)(outputDir, subscribersFileName);
|
|
323
|
+
const importsByFile = /* @__PURE__ */ new Map();
|
|
324
|
+
for (const { path: path$1, key } of subscribers) {
|
|
325
|
+
const relativePath = (0, node_path.relative)((0, node_path.dirname)(subscribersPath), path$1.relative);
|
|
326
|
+
const importPath = relativePath.replace(/\.ts$/, ".js");
|
|
327
|
+
if (!importsByFile.has(importPath)) importsByFile.set(importPath, []);
|
|
328
|
+
importsByFile.get(importPath).push(key);
|
|
329
|
+
}
|
|
330
|
+
const imports = Array.from(importsByFile.entries()).map(([importPath, exports$2]) => `import { ${exports$2.join(", ")} } from '${importPath}';`).join("\n");
|
|
331
|
+
const allExportNames = subscribers.map(({ key }) => key);
|
|
332
|
+
const content = `/**
|
|
333
|
+
* Generated subscribers setup
|
|
334
|
+
*
|
|
335
|
+
* ⚠️ WARNING: This is for LOCAL DEVELOPMENT ONLY
|
|
336
|
+
* This uses event polling which is not suitable for production.
|
|
337
|
+
*
|
|
338
|
+
* For production, use AWS Lambda with SQS/SNS event source mappings.
|
|
339
|
+
* Lambda automatically:
|
|
340
|
+
* - Scales based on queue depth
|
|
341
|
+
* - Handles batch processing and retries
|
|
342
|
+
* - Manages dead letter queues
|
|
343
|
+
* - Provides better cost optimization
|
|
344
|
+
*
|
|
345
|
+
* This polling implementation is useful for:
|
|
346
|
+
* - Local development and testing
|
|
347
|
+
* - Understanding event flow without Lambda deployment
|
|
348
|
+
*
|
|
349
|
+
* Supported connection strings:
|
|
350
|
+
* - sqs://region/account-id/queue-name (SQS queue)
|
|
351
|
+
* - sns://region/account-id/topic-name (SNS topic)
|
|
352
|
+
* - rabbitmq://host:port/queue-name (RabbitMQ)
|
|
353
|
+
* - basic://in-memory (In-memory for testing)
|
|
354
|
+
*/
|
|
355
|
+
import type { EnvironmentParser } from '@geekmidas/envkit';
|
|
356
|
+
import type { Logger } from '@geekmidas/logger';
|
|
357
|
+
import { EventConnectionFactory, Subscriber } from '@geekmidas/events';
|
|
358
|
+
import type { EventConnection, EventSubscriber } from '@geekmidas/events';
|
|
359
|
+
import { ServiceDiscovery } from '@geekmidas/services';
|
|
360
|
+
${imports}
|
|
361
|
+
|
|
362
|
+
const subscribers = [
|
|
363
|
+
${allExportNames.join(",\n ")}
|
|
364
|
+
];
|
|
365
|
+
|
|
366
|
+
const activeSubscribers: EventSubscriber<any>[] = [];
|
|
367
|
+
|
|
368
|
+
export async function setupSubscribers(
|
|
369
|
+
envParser: EnvironmentParser<any>,
|
|
370
|
+
logger: Logger,
|
|
371
|
+
): Promise<void> {
|
|
372
|
+
logger.info('Setting up subscribers in polling mode (local development)');
|
|
373
|
+
|
|
374
|
+
const config = envParser.create((get) => ({
|
|
375
|
+
connectionString: get('EVENT_SUBSCRIBER_CONNECTION_STRING').string().optional(),
|
|
376
|
+
})).parse();
|
|
377
|
+
|
|
378
|
+
if (!config.connectionString) {
|
|
379
|
+
logger.warn('EVENT_SUBSCRIBER_CONNECTION_STRING not configured, skipping subscriber setup');
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const serviceDiscovery = ServiceDiscovery.getInstance(logger, envParser);
|
|
384
|
+
|
|
385
|
+
// Create connection once, outside the loop (more efficient)
|
|
386
|
+
// EventConnectionFactory automatically determines the right connection type
|
|
387
|
+
let connection: EventConnection;
|
|
388
|
+
try {
|
|
389
|
+
connection = await EventConnectionFactory.fromConnectionString(config.connectionString);
|
|
390
|
+
|
|
391
|
+
const connectionType = new URL(config.connectionString).protocol.replace(':', '');
|
|
392
|
+
logger.info({ connectionType }, 'Created shared event connection');
|
|
393
|
+
} catch (error) {
|
|
394
|
+
logger.error({ error }, 'Failed to create event connection');
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
for (const subscriber of subscribers) {
|
|
399
|
+
try {
|
|
400
|
+
// Create subscriber from shared connection
|
|
401
|
+
const eventSubscriber = await Subscriber.fromConnection(connection);
|
|
402
|
+
|
|
403
|
+
// Register services
|
|
404
|
+
const services = subscriber.services.length > 0
|
|
405
|
+
? await serviceDiscovery.register(subscriber.services)
|
|
406
|
+
: {};
|
|
407
|
+
|
|
408
|
+
// Subscribe to events
|
|
409
|
+
const subscribedEvents = subscriber.subscribedEvents || [];
|
|
410
|
+
|
|
411
|
+
if (subscribedEvents.length === 0) {
|
|
412
|
+
logger.warn({ subscriber: subscriber.constructor.name }, 'Subscriber has no subscribed events, skipping');
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
await eventSubscriber.subscribe(subscribedEvents, async (event) => {
|
|
417
|
+
try {
|
|
418
|
+
// Process single event (batch of 1)
|
|
419
|
+
await subscriber.handler({
|
|
420
|
+
events: [event],
|
|
421
|
+
services: services as any,
|
|
422
|
+
logger: subscriber.logger,
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
logger.debug({ eventType: event.type }, 'Successfully processed event');
|
|
426
|
+
} catch (error) {
|
|
427
|
+
logger.error({ error, event }, 'Failed to process event');
|
|
428
|
+
// Event will become visible again for retry
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
activeSubscribers.push(eventSubscriber);
|
|
433
|
+
|
|
434
|
+
logger.info(
|
|
435
|
+
{
|
|
436
|
+
events: subscribedEvents,
|
|
437
|
+
},
|
|
438
|
+
'Subscriber started polling'
|
|
439
|
+
);
|
|
440
|
+
} catch (error) {
|
|
441
|
+
logger.error({ error, subscriber: subscriber.constructor.name }, 'Failed to setup subscriber');
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Setup graceful shutdown
|
|
446
|
+
const shutdown = () => {
|
|
447
|
+
logger.info('Stopping all subscribers');
|
|
448
|
+
for (const eventSubscriber of activeSubscribers) {
|
|
449
|
+
connection.stop();
|
|
450
|
+
}
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
process.on('SIGTERM', shutdown);
|
|
454
|
+
process.on('SIGINT', shutdown);
|
|
455
|
+
}
|
|
456
|
+
`;
|
|
457
|
+
await (0, node_fs_promises.writeFile)(subscribersPath, content);
|
|
458
|
+
return subscribersPath;
|
|
459
|
+
}
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
//#endregion
|
|
463
|
+
//#region src/dev/index.ts
|
|
464
|
+
const logger$2 = console;
|
|
465
|
+
/**
|
|
466
|
+
* Load environment files
|
|
467
|
+
* @internal Exported for testing
|
|
468
|
+
*/
|
|
469
|
+
function loadEnvFiles(envConfig, cwd = process.cwd()) {
|
|
470
|
+
const loaded = [];
|
|
471
|
+
const missing = [];
|
|
472
|
+
const envFiles = envConfig ? Array.isArray(envConfig) ? envConfig : [envConfig] : [".env"];
|
|
473
|
+
for (const envFile of envFiles) {
|
|
474
|
+
const envPath = (0, node_path.resolve)(cwd, envFile);
|
|
475
|
+
if ((0, node_fs.existsSync)(envPath)) {
|
|
476
|
+
(0, dotenv.config)({
|
|
477
|
+
path: envPath,
|
|
478
|
+
override: true,
|
|
479
|
+
quiet: true
|
|
480
|
+
});
|
|
481
|
+
loaded.push(envFile);
|
|
482
|
+
} else if (envConfig) missing.push(envFile);
|
|
483
|
+
}
|
|
484
|
+
return {
|
|
485
|
+
loaded,
|
|
486
|
+
missing
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Check if a port is available
|
|
491
|
+
* @internal Exported for testing
|
|
492
|
+
*/
|
|
493
|
+
async function isPortAvailable(port) {
|
|
494
|
+
return new Promise((resolve$1) => {
|
|
495
|
+
const server = (0, node_net.createServer)();
|
|
496
|
+
server.once("error", (err) => {
|
|
497
|
+
if (err.code === "EADDRINUSE") resolve$1(false);
|
|
498
|
+
else resolve$1(false);
|
|
499
|
+
});
|
|
500
|
+
server.once("listening", () => {
|
|
501
|
+
server.close();
|
|
502
|
+
resolve$1(true);
|
|
503
|
+
});
|
|
504
|
+
server.listen(port);
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Find an available port starting from the preferred port
|
|
509
|
+
* @internal Exported for testing
|
|
510
|
+
*/
|
|
511
|
+
async function findAvailablePort(preferredPort, maxAttempts = 10) {
|
|
512
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
513
|
+
const port = preferredPort + i;
|
|
514
|
+
if (await isPortAvailable(port)) return port;
|
|
515
|
+
logger$2.log(`⚠️ Port ${port} is in use, trying ${port + 1}...`);
|
|
516
|
+
}
|
|
517
|
+
throw new Error(`Could not find an available port after trying ${maxAttempts} ports starting from ${preferredPort}`);
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* Normalize telescope configuration
|
|
521
|
+
* @internal Exported for testing
|
|
522
|
+
*/
|
|
523
|
+
function normalizeTelescopeConfig(config) {
|
|
524
|
+
if (config === false) return void 0;
|
|
525
|
+
if (typeof config === "string") {
|
|
526
|
+
const { path: telescopePath, importPattern: telescopeImportPattern } = require_config.parseModuleConfig(config, "telescope");
|
|
527
|
+
return {
|
|
528
|
+
enabled: true,
|
|
529
|
+
telescopePath,
|
|
530
|
+
telescopeImportPattern,
|
|
531
|
+
path: "/__telescope",
|
|
532
|
+
ignore: [],
|
|
533
|
+
recordBody: true,
|
|
534
|
+
maxEntries: 1e3,
|
|
535
|
+
websocket: true
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
const isEnabled$1 = config === true || config === void 0 || config.enabled !== false;
|
|
539
|
+
if (!isEnabled$1) return void 0;
|
|
540
|
+
const telescopeConfig = typeof config === "object" ? config : {};
|
|
541
|
+
return {
|
|
542
|
+
enabled: true,
|
|
543
|
+
path: telescopeConfig.path ?? "/__telescope",
|
|
544
|
+
ignore: telescopeConfig.ignore ?? [],
|
|
545
|
+
recordBody: telescopeConfig.recordBody ?? true,
|
|
546
|
+
maxEntries: telescopeConfig.maxEntries ?? 1e3,
|
|
547
|
+
websocket: telescopeConfig.websocket ?? true
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
551
|
+
* Normalize studio configuration
|
|
552
|
+
* @internal Exported for testing
|
|
553
|
+
*/
|
|
554
|
+
function normalizeStudioConfig(config) {
|
|
555
|
+
if (config === false) return void 0;
|
|
556
|
+
if (typeof config === "string") {
|
|
557
|
+
const { path: studioPath, importPattern: studioImportPattern } = require_config.parseModuleConfig(config, "studio");
|
|
558
|
+
return {
|
|
559
|
+
enabled: true,
|
|
560
|
+
studioPath,
|
|
561
|
+
studioImportPattern,
|
|
562
|
+
path: "/__studio",
|
|
563
|
+
schema: "public"
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
const isEnabled$1 = config === true || config === void 0 || config.enabled !== false;
|
|
567
|
+
if (!isEnabled$1) return void 0;
|
|
568
|
+
const studioConfig = typeof config === "object" ? config : {};
|
|
569
|
+
return {
|
|
570
|
+
enabled: true,
|
|
571
|
+
path: studioConfig.path ?? "/__studio",
|
|
572
|
+
schema: studioConfig.schema ?? "public"
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
async function devCommand(options) {
|
|
576
|
+
const defaultEnv = loadEnvFiles(".env");
|
|
577
|
+
if (defaultEnv.loaded.length > 0) logger$2.log(`📦 Loaded env: ${defaultEnv.loaded.join(", ")}`);
|
|
578
|
+
const config = await require_config.loadConfig();
|
|
579
|
+
if (config.env) {
|
|
580
|
+
const { loaded, missing } = loadEnvFiles(config.env);
|
|
581
|
+
if (loaded.length > 0) logger$2.log(`📦 Loaded env: ${loaded.join(", ")}`);
|
|
582
|
+
if (missing.length > 0) logger$2.warn(`⚠️ Missing env files: ${missing.join(", ")}`);
|
|
583
|
+
}
|
|
584
|
+
const resolved = resolveProviders(config, { provider: "server" });
|
|
585
|
+
logger$2.log("🚀 Starting development server...");
|
|
586
|
+
logger$2.log(`Loading routes from: ${config.routes}`);
|
|
587
|
+
if (config.functions) logger$2.log(`Loading functions from: ${config.functions}`);
|
|
588
|
+
if (config.crons) logger$2.log(`Loading crons from: ${config.crons}`);
|
|
589
|
+
if (config.subscribers) logger$2.log(`Loading subscribers from: ${config.subscribers}`);
|
|
590
|
+
logger$2.log(`Using envParser: ${config.envParser}`);
|
|
591
|
+
const { path: envParserPath, importPattern: envParserImportPattern } = require_config.parseModuleConfig(config.envParser, "envParser");
|
|
592
|
+
const { path: loggerPath, importPattern: loggerImportPattern } = require_config.parseModuleConfig(config.logger, "logger");
|
|
593
|
+
const telescope = normalizeTelescopeConfig(config.telescope);
|
|
594
|
+
if (telescope) logger$2.log(`🔭 Telescope enabled at ${telescope.path}`);
|
|
595
|
+
const studio = normalizeStudioConfig(config.studio);
|
|
596
|
+
if (studio) logger$2.log(`🗄️ Studio enabled at ${studio.path}`);
|
|
597
|
+
const openApiConfig = require_openapi.resolveOpenApiConfig(config);
|
|
598
|
+
const enableOpenApi = openApiConfig.enabled || resolved.enableOpenApi;
|
|
599
|
+
if (enableOpenApi) logger$2.log(`📄 OpenAPI output: ${openApiConfig.output}`);
|
|
600
|
+
const buildContext = {
|
|
601
|
+
envParserPath,
|
|
602
|
+
envParserImportPattern,
|
|
603
|
+
loggerPath,
|
|
604
|
+
loggerImportPattern,
|
|
605
|
+
telescope,
|
|
606
|
+
studio
|
|
607
|
+
};
|
|
608
|
+
await buildServer(config, buildContext, resolved.providers[0], enableOpenApi);
|
|
609
|
+
if (enableOpenApi) await require_openapi.generateOpenApi(config);
|
|
610
|
+
const runtime = config.runtime ?? "node";
|
|
611
|
+
const devServer = new DevServer(resolved.providers[0], options.port || 3e3, enableOpenApi, telescope, studio, runtime);
|
|
612
|
+
await devServer.start();
|
|
613
|
+
const envParserFile = config.envParser.split("#")[0];
|
|
614
|
+
const loggerFile = config.logger.split("#")[0];
|
|
615
|
+
const watchPatterns = [
|
|
616
|
+
config.routes,
|
|
617
|
+
...config.functions ? [config.functions] : [],
|
|
618
|
+
...config.crons ? [config.crons] : [],
|
|
619
|
+
...config.subscribers ? [config.subscribers] : [],
|
|
620
|
+
envParserFile.endsWith(".ts") ? envParserFile : `${envParserFile}.ts`,
|
|
621
|
+
loggerFile.endsWith(".ts") ? loggerFile : `${loggerFile}.ts`
|
|
622
|
+
].flat();
|
|
623
|
+
const normalizedPatterns = watchPatterns.map((p) => p.startsWith("./") ? p.slice(2) : p);
|
|
624
|
+
logger$2.log(`👀 Watching for changes in: ${normalizedPatterns.join(", ")}`);
|
|
625
|
+
const resolvedFiles = await (0, fast_glob.default)(normalizedPatterns, {
|
|
626
|
+
cwd: process.cwd(),
|
|
627
|
+
absolute: false,
|
|
628
|
+
onlyFiles: true
|
|
629
|
+
});
|
|
630
|
+
const dirsToWatch = [...new Set(resolvedFiles.map((f) => f.split("/").slice(0, -1).join("/")))];
|
|
631
|
+
logger$2.log(`📁 Found ${resolvedFiles.length} files in ${dirsToWatch.length} directories`);
|
|
632
|
+
const watcher = chokidar.default.watch([...resolvedFiles, ...dirsToWatch], {
|
|
633
|
+
ignored: /(^|[\/\\])\../,
|
|
634
|
+
persistent: true,
|
|
635
|
+
ignoreInitial: true,
|
|
636
|
+
cwd: process.cwd()
|
|
637
|
+
});
|
|
638
|
+
watcher.on("ready", () => {
|
|
639
|
+
logger$2.log("🔍 File watcher ready");
|
|
640
|
+
});
|
|
641
|
+
watcher.on("error", (error) => {
|
|
642
|
+
logger$2.error("❌ Watcher error:", error);
|
|
643
|
+
});
|
|
644
|
+
let rebuildTimeout = null;
|
|
645
|
+
watcher.on("change", async (path$1) => {
|
|
646
|
+
logger$2.log(`📝 File changed: ${path$1}`);
|
|
647
|
+
if (rebuildTimeout) clearTimeout(rebuildTimeout);
|
|
648
|
+
rebuildTimeout = setTimeout(async () => {
|
|
649
|
+
try {
|
|
650
|
+
logger$2.log("🔄 Rebuilding...");
|
|
651
|
+
await buildServer(config, buildContext, resolved.providers[0], enableOpenApi);
|
|
652
|
+
if (enableOpenApi) await require_openapi.generateOpenApi(config, { silent: true });
|
|
653
|
+
logger$2.log("✅ Rebuild complete, restarting server...");
|
|
654
|
+
await devServer.restart();
|
|
655
|
+
} catch (error) {
|
|
656
|
+
logger$2.error("❌ Rebuild failed:", error.message);
|
|
657
|
+
}
|
|
658
|
+
}, 300);
|
|
659
|
+
});
|
|
660
|
+
const shutdown = async () => {
|
|
661
|
+
logger$2.log("\n🛑 Shutting down...");
|
|
662
|
+
await watcher.close();
|
|
663
|
+
await devServer.stop();
|
|
664
|
+
process.exit(0);
|
|
665
|
+
};
|
|
666
|
+
process.on("SIGINT", shutdown);
|
|
667
|
+
process.on("SIGTERM", shutdown);
|
|
668
|
+
}
|
|
669
|
+
async function buildServer(config, context, provider, enableOpenApi) {
|
|
670
|
+
const endpointGenerator = new require_openapi.EndpointGenerator();
|
|
671
|
+
const functionGenerator = new FunctionGenerator();
|
|
672
|
+
const cronGenerator = new CronGenerator();
|
|
673
|
+
const subscriberGenerator = new SubscriberGenerator();
|
|
674
|
+
const [allEndpoints, allFunctions, allCrons, allSubscribers] = await Promise.all([
|
|
675
|
+
endpointGenerator.load(config.routes),
|
|
676
|
+
config.functions ? functionGenerator.load(config.functions) : [],
|
|
677
|
+
config.crons ? cronGenerator.load(config.crons) : [],
|
|
678
|
+
config.subscribers ? subscriberGenerator.load(config.subscribers) : []
|
|
679
|
+
]);
|
|
680
|
+
const outputDir = (0, node_path.join)(process.cwd(), ".gkm", provider);
|
|
681
|
+
await (0, node_fs_promises.mkdir)(outputDir, { recursive: true });
|
|
682
|
+
await Promise.all([
|
|
683
|
+
endpointGenerator.build(context, allEndpoints, outputDir, {
|
|
684
|
+
provider,
|
|
685
|
+
enableOpenApi
|
|
686
|
+
}),
|
|
687
|
+
functionGenerator.build(context, allFunctions, outputDir, { provider }),
|
|
688
|
+
cronGenerator.build(context, allCrons, outputDir, { provider }),
|
|
689
|
+
subscriberGenerator.build(context, allSubscribers, outputDir, { provider })
|
|
690
|
+
]);
|
|
691
|
+
}
|
|
692
|
+
var DevServer = class {
|
|
693
|
+
serverProcess = null;
|
|
694
|
+
isRunning = false;
|
|
695
|
+
actualPort;
|
|
696
|
+
constructor(provider, requestedPort, enableOpenApi, telescope, studio, runtime = "node") {
|
|
697
|
+
this.provider = provider;
|
|
698
|
+
this.requestedPort = requestedPort;
|
|
699
|
+
this.enableOpenApi = enableOpenApi;
|
|
700
|
+
this.telescope = telescope;
|
|
701
|
+
this.studio = studio;
|
|
702
|
+
this.runtime = runtime;
|
|
703
|
+
this.actualPort = requestedPort;
|
|
704
|
+
}
|
|
705
|
+
async start() {
|
|
706
|
+
if (this.isRunning) await this.stop();
|
|
707
|
+
this.actualPort = await findAvailablePort(this.requestedPort);
|
|
708
|
+
if (this.actualPort !== this.requestedPort) logger$2.log(`ℹ️ Port ${this.requestedPort} was in use, using port ${this.actualPort} instead`);
|
|
709
|
+
const serverEntryPath = (0, node_path.join)(process.cwd(), ".gkm", this.provider, "server.ts");
|
|
710
|
+
await this.createServerEntry();
|
|
711
|
+
logger$2.log(`\n✨ Starting server on port ${this.actualPort}...`);
|
|
712
|
+
this.serverProcess = (0, node_child_process.spawn)("npx", [
|
|
713
|
+
"tsx",
|
|
714
|
+
serverEntryPath,
|
|
715
|
+
"--port",
|
|
716
|
+
this.actualPort.toString()
|
|
717
|
+
], {
|
|
718
|
+
stdio: "inherit",
|
|
719
|
+
env: {
|
|
720
|
+
...process.env,
|
|
721
|
+
NODE_ENV: "development"
|
|
722
|
+
},
|
|
723
|
+
detached: true
|
|
724
|
+
});
|
|
725
|
+
this.isRunning = true;
|
|
726
|
+
this.serverProcess.on("error", (error) => {
|
|
727
|
+
logger$2.error("❌ Server error:", error);
|
|
728
|
+
});
|
|
729
|
+
this.serverProcess.on("exit", (code, signal) => {
|
|
730
|
+
if (code !== null && code !== 0 && signal !== "SIGTERM") logger$2.error(`❌ Server exited with code ${code}`);
|
|
731
|
+
this.isRunning = false;
|
|
732
|
+
});
|
|
733
|
+
await new Promise((resolve$1) => setTimeout(resolve$1, 1e3));
|
|
734
|
+
if (this.isRunning) {
|
|
735
|
+
logger$2.log(`\n🎉 Server running at http://localhost:${this.actualPort}`);
|
|
736
|
+
if (this.enableOpenApi) logger$2.log(`📚 API Docs available at http://localhost:${this.actualPort}/__docs`);
|
|
737
|
+
if (this.telescope) logger$2.log(`🔭 Telescope available at http://localhost:${this.actualPort}${this.telescope.path}`);
|
|
738
|
+
if (this.studio) logger$2.log(`🗄️ Studio available at http://localhost:${this.actualPort}${this.studio.path}`);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
async stop() {
|
|
742
|
+
if (this.serverProcess && this.isRunning) {
|
|
743
|
+
const pid = this.serverProcess.pid;
|
|
744
|
+
if (pid) try {
|
|
745
|
+
process.kill(-pid, "SIGTERM");
|
|
746
|
+
} catch {}
|
|
747
|
+
await new Promise((resolve$1) => {
|
|
748
|
+
const timeout = setTimeout(() => {
|
|
749
|
+
if (pid) try {
|
|
750
|
+
process.kill(-pid, "SIGKILL");
|
|
751
|
+
} catch {}
|
|
752
|
+
resolve$1();
|
|
753
|
+
}, 3e3);
|
|
754
|
+
this.serverProcess?.on("exit", () => {
|
|
755
|
+
clearTimeout(timeout);
|
|
756
|
+
resolve$1();
|
|
757
|
+
});
|
|
758
|
+
});
|
|
759
|
+
this.serverProcess = null;
|
|
760
|
+
this.isRunning = false;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
async restart() {
|
|
764
|
+
const portToReuse = this.actualPort;
|
|
765
|
+
await this.stop();
|
|
766
|
+
let attempts = 0;
|
|
767
|
+
while (attempts < 30) {
|
|
768
|
+
if (await isPortAvailable(portToReuse)) break;
|
|
769
|
+
await new Promise((resolve$1) => setTimeout(resolve$1, 100));
|
|
770
|
+
attempts++;
|
|
771
|
+
}
|
|
772
|
+
this.requestedPort = portToReuse;
|
|
773
|
+
await this.start();
|
|
774
|
+
}
|
|
775
|
+
async createServerEntry() {
|
|
776
|
+
const { writeFile: writeFile$5 } = await import("node:fs/promises");
|
|
777
|
+
const { relative: relative$5, dirname: dirname$4 } = await import("node:path");
|
|
778
|
+
const serverPath = (0, node_path.join)(process.cwd(), ".gkm", this.provider, "server.ts");
|
|
779
|
+
const relativeAppPath = relative$5(dirname$4(serverPath), (0, node_path.join)(dirname$4(serverPath), "app.js"));
|
|
780
|
+
const serveCode = this.runtime === "bun" ? `Bun.serve({
|
|
781
|
+
port,
|
|
782
|
+
fetch: app.fetch,
|
|
783
|
+
});` : `const { serve } = await import('@hono/node-server');
|
|
784
|
+
const server = serve({
|
|
785
|
+
fetch: app.fetch,
|
|
786
|
+
port,
|
|
787
|
+
});
|
|
788
|
+
// Inject WebSocket support if available
|
|
789
|
+
const injectWs = (app as any).__injectWebSocket;
|
|
790
|
+
if (injectWs) {
|
|
791
|
+
injectWs(server);
|
|
792
|
+
console.log('🔌 Telescope real-time updates enabled');
|
|
793
|
+
}`;
|
|
794
|
+
const content = `#!/usr/bin/env node
|
|
795
|
+
/**
|
|
796
|
+
* Development server entry point
|
|
797
|
+
* This file is auto-generated by 'gkm dev'
|
|
798
|
+
*/
|
|
799
|
+
import { createApp } from './${relativeAppPath.startsWith(".") ? relativeAppPath : "./" + relativeAppPath}';
|
|
800
|
+
|
|
801
|
+
const port = process.argv.includes('--port')
|
|
802
|
+
? Number.parseInt(process.argv[process.argv.indexOf('--port') + 1])
|
|
803
|
+
: 3000;
|
|
804
|
+
|
|
805
|
+
// createApp is async to support optional WebSocket setup
|
|
806
|
+
const { app, start } = await createApp(undefined, ${this.enableOpenApi});
|
|
807
|
+
|
|
808
|
+
// Start the server
|
|
809
|
+
start({
|
|
810
|
+
port,
|
|
811
|
+
serve: async (app, port) => {
|
|
812
|
+
${serveCode}
|
|
813
|
+
},
|
|
814
|
+
}).catch((error) => {
|
|
815
|
+
console.error('Failed to start server:', error);
|
|
816
|
+
process.exit(1);
|
|
817
|
+
});
|
|
818
|
+
`;
|
|
819
|
+
await writeFile$5(serverPath, content);
|
|
820
|
+
}
|
|
821
|
+
};
|
|
822
|
+
|
|
823
|
+
//#endregion
|
|
824
|
+
//#region src/build/manifests.ts
|
|
825
|
+
const logger$1 = console;
|
|
826
|
+
async function generateAwsManifest(outputDir, routes, functions, crons, subscribers) {
|
|
827
|
+
const manifestDir = (0, path.join)(outputDir, "manifest");
|
|
828
|
+
await (0, node_fs_promises.mkdir)(manifestDir, { recursive: true });
|
|
829
|
+
const awsRoutes = routes.filter((r) => r.method !== "ALL");
|
|
830
|
+
const content = `export const manifest = {
|
|
831
|
+
routes: ${JSON.stringify(awsRoutes, null, 2)},
|
|
832
|
+
functions: ${JSON.stringify(functions, null, 2)},
|
|
833
|
+
crons: ${JSON.stringify(crons, null, 2)},
|
|
834
|
+
subscribers: ${JSON.stringify(subscribers, null, 2)},
|
|
835
|
+
} as const;
|
|
836
|
+
|
|
837
|
+
// Derived types
|
|
838
|
+
export type Route = (typeof manifest.routes)[number];
|
|
839
|
+
export type Function = (typeof manifest.functions)[number];
|
|
840
|
+
export type Cron = (typeof manifest.crons)[number];
|
|
841
|
+
export type Subscriber = (typeof manifest.subscribers)[number];
|
|
842
|
+
|
|
843
|
+
// Useful union types
|
|
844
|
+
export type Authorizer = Route['authorizer'];
|
|
845
|
+
export type HttpMethod = Route['method'];
|
|
846
|
+
export type RoutePath = Route['path'];
|
|
847
|
+
`;
|
|
848
|
+
const manifestPath = (0, path.join)(manifestDir, "aws.ts");
|
|
849
|
+
await (0, node_fs_promises.writeFile)(manifestPath, content);
|
|
850
|
+
logger$1.log(`Generated AWS manifest with ${awsRoutes.length} routes, ${functions.length} functions, ${crons.length} crons, ${subscribers.length} subscribers`);
|
|
851
|
+
logger$1.log(`Manifest: ${(0, path.relative)(process.cwd(), manifestPath)}`);
|
|
852
|
+
}
|
|
853
|
+
async function generateServerManifest(outputDir, appInfo, routes, subscribers) {
|
|
854
|
+
const manifestDir = (0, path.join)(outputDir, "manifest");
|
|
855
|
+
await (0, node_fs_promises.mkdir)(manifestDir, { recursive: true });
|
|
856
|
+
const serverRoutes = routes.filter((r) => r.method !== "ALL").map((r) => ({
|
|
857
|
+
path: r.path,
|
|
858
|
+
method: r.method,
|
|
859
|
+
authorizer: r.authorizer
|
|
860
|
+
}));
|
|
861
|
+
const serverSubscribers = subscribers.map((s) => ({
|
|
862
|
+
name: s.name,
|
|
863
|
+
subscribedEvents: s.subscribedEvents
|
|
864
|
+
}));
|
|
865
|
+
const content = `export const manifest = {
|
|
866
|
+
app: ${JSON.stringify(appInfo, null, 2)},
|
|
867
|
+
routes: ${JSON.stringify(serverRoutes, null, 2)},
|
|
868
|
+
subscribers: ${JSON.stringify(serverSubscribers, null, 2)},
|
|
869
|
+
} as const;
|
|
870
|
+
|
|
871
|
+
// Derived types
|
|
872
|
+
export type Route = (typeof manifest.routes)[number];
|
|
873
|
+
export type Subscriber = (typeof manifest.subscribers)[number];
|
|
874
|
+
|
|
875
|
+
// Useful union types
|
|
876
|
+
export type Authorizer = Route['authorizer'];
|
|
877
|
+
export type HttpMethod = Route['method'];
|
|
878
|
+
export type RoutePath = Route['path'];
|
|
879
|
+
`;
|
|
880
|
+
const manifestPath = (0, path.join)(manifestDir, "server.ts");
|
|
881
|
+
await (0, node_fs_promises.writeFile)(manifestPath, content);
|
|
882
|
+
logger$1.log(`Generated server manifest with ${serverRoutes.length} routes, ${serverSubscribers.length} subscribers`);
|
|
883
|
+
logger$1.log(`Manifest: ${(0, path.relative)(process.cwd(), manifestPath)}`);
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
//#endregion
|
|
887
|
+
//#region src/build/index.ts
|
|
888
|
+
const logger = console;
|
|
889
|
+
async function buildCommand(options) {
|
|
890
|
+
const config = await require_config.loadConfig();
|
|
891
|
+
const resolved = resolveProviders(config, options);
|
|
892
|
+
logger.log(`Building with providers: ${resolved.providers.join(", ")}`);
|
|
893
|
+
logger.log(`Loading routes from: ${config.routes}`);
|
|
894
|
+
if (config.functions) logger.log(`Loading functions from: ${config.functions}`);
|
|
895
|
+
if (config.crons) logger.log(`Loading crons from: ${config.crons}`);
|
|
896
|
+
if (config.subscribers) logger.log(`Loading subscribers from: ${config.subscribers}`);
|
|
897
|
+
logger.log(`Using envParser: ${config.envParser}`);
|
|
898
|
+
const { path: envParserPath, importPattern: envParserImportPattern } = require_config.parseModuleConfig(config.envParser, "envParser");
|
|
899
|
+
const { path: loggerPath, importPattern: loggerImportPattern } = require_config.parseModuleConfig(config.logger, "logger");
|
|
900
|
+
const telescope = normalizeTelescopeConfig(config.telescope);
|
|
901
|
+
if (telescope) logger.log(`🔭 Telescope enabled at ${telescope.path}`);
|
|
902
|
+
const buildContext = {
|
|
903
|
+
envParserPath,
|
|
904
|
+
envParserImportPattern,
|
|
905
|
+
loggerPath,
|
|
906
|
+
loggerImportPattern,
|
|
907
|
+
telescope
|
|
908
|
+
};
|
|
909
|
+
const endpointGenerator = new require_openapi.EndpointGenerator();
|
|
910
|
+
const functionGenerator = new FunctionGenerator();
|
|
911
|
+
const cronGenerator = new CronGenerator();
|
|
912
|
+
const subscriberGenerator = new SubscriberGenerator();
|
|
913
|
+
const [allEndpoints, allFunctions, allCrons, allSubscribers] = await Promise.all([
|
|
914
|
+
endpointGenerator.load(config.routes),
|
|
915
|
+
config.functions ? functionGenerator.load(config.functions) : [],
|
|
916
|
+
config.crons ? cronGenerator.load(config.crons) : [],
|
|
917
|
+
config.subscribers ? subscriberGenerator.load(config.subscribers) : []
|
|
918
|
+
]);
|
|
919
|
+
logger.log(`Found ${allEndpoints.length} endpoints`);
|
|
920
|
+
logger.log(`Found ${allFunctions.length} functions`);
|
|
921
|
+
logger.log(`Found ${allCrons.length} crons`);
|
|
922
|
+
logger.log(`Found ${allSubscribers.length} subscribers`);
|
|
923
|
+
if (allEndpoints.length === 0 && allFunctions.length === 0 && allCrons.length === 0 && allSubscribers.length === 0) {
|
|
924
|
+
logger.log("No endpoints, functions, crons, or subscribers found to process");
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
927
|
+
const rootOutputDir = (0, node_path.join)(process.cwd(), ".gkm");
|
|
928
|
+
await (0, node_fs_promises.mkdir)(rootOutputDir, { recursive: true });
|
|
929
|
+
for (const provider of resolved.providers) await buildForProvider(provider, buildContext, rootOutputDir, endpointGenerator, functionGenerator, cronGenerator, subscriberGenerator, allEndpoints, allFunctions, allCrons, allSubscribers, resolved.enableOpenApi);
|
|
930
|
+
}
|
|
931
|
+
async function buildForProvider(provider, context, rootOutputDir, endpointGenerator, functionGenerator, cronGenerator, subscriberGenerator, endpoints, functions, crons, subscribers, enableOpenApi) {
|
|
932
|
+
const outputDir = (0, node_path.join)(process.cwd(), ".gkm", provider);
|
|
933
|
+
await (0, node_fs_promises.mkdir)(outputDir, { recursive: true });
|
|
934
|
+
logger.log(`\nGenerating handlers for provider: ${provider}`);
|
|
935
|
+
const [routes, functionInfos, cronInfos, subscriberInfos] = await Promise.all([
|
|
936
|
+
endpointGenerator.build(context, endpoints, outputDir, {
|
|
937
|
+
provider,
|
|
938
|
+
enableOpenApi
|
|
939
|
+
}),
|
|
940
|
+
functionGenerator.build(context, functions, outputDir, { provider }),
|
|
941
|
+
cronGenerator.build(context, crons, outputDir, { provider }),
|
|
942
|
+
subscriberGenerator.build(context, subscribers, outputDir, { provider })
|
|
943
|
+
]);
|
|
944
|
+
logger.log(`Generated ${routes.length} routes, ${functionInfos.length} functions, ${cronInfos.length} crons, ${subscriberInfos.length} subscribers for ${provider}`);
|
|
945
|
+
if (provider === "server") {
|
|
946
|
+
const routeMetadata = await Promise.all(endpoints.map(async ({ construct }) => ({
|
|
947
|
+
path: construct._path,
|
|
948
|
+
method: construct.method,
|
|
949
|
+
handler: "",
|
|
950
|
+
authorizer: construct.authorizer?.name ?? "none"
|
|
951
|
+
})));
|
|
952
|
+
const appInfo = {
|
|
953
|
+
handler: (0, node_path.relative)(process.cwd(), (0, node_path.join)(outputDir, "app.ts")),
|
|
954
|
+
endpoints: (0, node_path.relative)(process.cwd(), (0, node_path.join)(outputDir, "endpoints.ts"))
|
|
955
|
+
};
|
|
956
|
+
await generateServerManifest(rootOutputDir, appInfo, routeMetadata, subscriberInfos);
|
|
957
|
+
} else await generateAwsManifest(rootOutputDir, routes, functionInfos, cronInfos, subscriberInfos);
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
//#endregion
|
|
961
|
+
//#region src/init/generators/config.ts
|
|
962
|
+
/**
|
|
963
|
+
* Generate configuration files (gkm.config.ts, tsconfig.json, biome.json, turbo.json)
|
|
964
|
+
*/
|
|
965
|
+
function generateConfigFiles(options, template) {
|
|
966
|
+
const { telescope, studio, routesStructure } = options;
|
|
967
|
+
const isServerless = template.name === "serverless";
|
|
968
|
+
const hasWorker = template.name === "worker";
|
|
969
|
+
const getRoutesGlob = () => {
|
|
970
|
+
switch (routesStructure) {
|
|
971
|
+
case "centralized-endpoints": return "./src/endpoints/**/*.ts";
|
|
972
|
+
case "centralized-routes": return "./src/routes/**/*.ts";
|
|
973
|
+
case "domain-based": return "./src/**/routes/*.ts";
|
|
974
|
+
}
|
|
975
|
+
};
|
|
976
|
+
let gkmConfig = `import { defineConfig } from '@geekmidas/cli/config';
|
|
977
|
+
|
|
978
|
+
export default defineConfig({
|
|
979
|
+
routes: '${getRoutesGlob()}',
|
|
980
|
+
envParser: './src/config/env#envParser',
|
|
981
|
+
logger: './src/config/logger#logger',`;
|
|
982
|
+
if (isServerless || hasWorker) gkmConfig += `
|
|
983
|
+
functions: './src/functions/**/*.ts',`;
|
|
984
|
+
if (hasWorker) gkmConfig += `
|
|
985
|
+
crons: './src/crons/**/*.ts',
|
|
986
|
+
subscribers: './src/subscribers/**/*.ts',`;
|
|
987
|
+
if (telescope) gkmConfig += `
|
|
988
|
+
telescope: {
|
|
989
|
+
enabled: true,
|
|
990
|
+
path: '/__telescope',
|
|
991
|
+
},`;
|
|
992
|
+
if (studio) gkmConfig += `
|
|
993
|
+
studio: './src/config/studio#studio',`;
|
|
994
|
+
gkmConfig += `
|
|
995
|
+
openapi: {
|
|
996
|
+
enabled: true,
|
|
997
|
+
},`;
|
|
998
|
+
gkmConfig += `
|
|
999
|
+
});
|
|
1000
|
+
`;
|
|
1001
|
+
const tsConfig = options.monorepo ? {
|
|
1002
|
+
extends: "../../tsconfig.json",
|
|
1003
|
+
compilerOptions: {
|
|
1004
|
+
outDir: "./dist",
|
|
1005
|
+
rootDir: "./src",
|
|
1006
|
+
baseUrl: ".",
|
|
1007
|
+
paths: { [`@${options.name}/*`]: ["../../packages/*/src"] }
|
|
1008
|
+
},
|
|
1009
|
+
include: ["src/**/*.ts"],
|
|
1010
|
+
exclude: ["node_modules", "dist"]
|
|
1011
|
+
} : {
|
|
1012
|
+
compilerOptions: {
|
|
1013
|
+
target: "ES2022",
|
|
1014
|
+
module: "NodeNext",
|
|
1015
|
+
moduleResolution: "NodeNext",
|
|
1016
|
+
lib: ["ES2022"],
|
|
1017
|
+
strict: true,
|
|
1018
|
+
esModuleInterop: true,
|
|
1019
|
+
skipLibCheck: true,
|
|
1020
|
+
forceConsistentCasingInFileNames: true,
|
|
1021
|
+
resolveJsonModule: true,
|
|
1022
|
+
declaration: true,
|
|
1023
|
+
declarationMap: true,
|
|
1024
|
+
outDir: "./dist",
|
|
1025
|
+
rootDir: "./src"
|
|
1026
|
+
},
|
|
1027
|
+
include: ["src/**/*.ts"],
|
|
1028
|
+
exclude: ["node_modules", "dist"]
|
|
1029
|
+
};
|
|
1030
|
+
if (options.monorepo) return [{
|
|
1031
|
+
path: "gkm.config.ts",
|
|
1032
|
+
content: gkmConfig
|
|
1033
|
+
}, {
|
|
1034
|
+
path: "tsconfig.json",
|
|
1035
|
+
content: JSON.stringify(tsConfig, null, 2) + "\n"
|
|
1036
|
+
}];
|
|
1037
|
+
const biomeConfig = {
|
|
1038
|
+
$schema: "https://biomejs.dev/schemas/1.9.4/schema.json",
|
|
1039
|
+
vcs: {
|
|
1040
|
+
enabled: true,
|
|
1041
|
+
clientKind: "git",
|
|
1042
|
+
useIgnoreFile: true
|
|
1043
|
+
},
|
|
1044
|
+
organizeImports: { enabled: true },
|
|
1045
|
+
formatter: {
|
|
1046
|
+
enabled: true,
|
|
1047
|
+
indentStyle: "space",
|
|
1048
|
+
indentWidth: 2,
|
|
1049
|
+
lineWidth: 80
|
|
1050
|
+
},
|
|
1051
|
+
javascript: { formatter: {
|
|
1052
|
+
quoteStyle: "single",
|
|
1053
|
+
trailingCommas: "all",
|
|
1054
|
+
semicolons: "always",
|
|
1055
|
+
arrowParentheses: "always"
|
|
1056
|
+
} },
|
|
1057
|
+
linter: {
|
|
1058
|
+
enabled: true,
|
|
1059
|
+
rules: {
|
|
1060
|
+
recommended: true,
|
|
1061
|
+
correctness: {
|
|
1062
|
+
noUnusedImports: "error",
|
|
1063
|
+
noUnusedVariables: "error"
|
|
1064
|
+
},
|
|
1065
|
+
style: { noNonNullAssertion: "off" }
|
|
1066
|
+
}
|
|
1067
|
+
},
|
|
1068
|
+
files: { ignore: [
|
|
1069
|
+
"node_modules",
|
|
1070
|
+
"dist",
|
|
1071
|
+
".gkm",
|
|
1072
|
+
"coverage"
|
|
1073
|
+
] }
|
|
1074
|
+
};
|
|
1075
|
+
const turboConfig = {
|
|
1076
|
+
$schema: "https://turbo.build/schema.json",
|
|
1077
|
+
tasks: {
|
|
1078
|
+
build: {
|
|
1079
|
+
dependsOn: ["^build"],
|
|
1080
|
+
outputs: ["dist/**"]
|
|
1081
|
+
},
|
|
1082
|
+
dev: {
|
|
1083
|
+
cache: false,
|
|
1084
|
+
persistent: true
|
|
1085
|
+
},
|
|
1086
|
+
test: {
|
|
1087
|
+
dependsOn: ["^build"],
|
|
1088
|
+
cache: false
|
|
1089
|
+
},
|
|
1090
|
+
"test:once": {
|
|
1091
|
+
dependsOn: ["^build"],
|
|
1092
|
+
outputs: ["coverage/**"]
|
|
1093
|
+
},
|
|
1094
|
+
typecheck: {
|
|
1095
|
+
dependsOn: ["^build"],
|
|
1096
|
+
outputs: []
|
|
1097
|
+
},
|
|
1098
|
+
lint: { outputs: [] },
|
|
1099
|
+
fmt: { outputs: [] }
|
|
1100
|
+
}
|
|
1101
|
+
};
|
|
1102
|
+
return [
|
|
1103
|
+
{
|
|
1104
|
+
path: "gkm.config.ts",
|
|
1105
|
+
content: gkmConfig
|
|
1106
|
+
},
|
|
1107
|
+
{
|
|
1108
|
+
path: "tsconfig.json",
|
|
1109
|
+
content: JSON.stringify(tsConfig, null, 2) + "\n"
|
|
1110
|
+
},
|
|
1111
|
+
{
|
|
1112
|
+
path: "biome.json",
|
|
1113
|
+
content: JSON.stringify(biomeConfig, null, 2) + "\n"
|
|
1114
|
+
},
|
|
1115
|
+
{
|
|
1116
|
+
path: "turbo.json",
|
|
1117
|
+
content: JSON.stringify(turboConfig, null, 2) + "\n"
|
|
1118
|
+
}
|
|
1119
|
+
];
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
//#endregion
|
|
1123
|
+
//#region src/init/generators/docker.ts
|
|
1124
|
+
/**
|
|
1125
|
+
* Generate docker-compose.yml based on template and options
|
|
1126
|
+
*/
|
|
1127
|
+
function generateDockerFiles(options, template) {
|
|
1128
|
+
const { database } = options;
|
|
1129
|
+
const isServerless = template.name === "serverless";
|
|
1130
|
+
const hasWorker = template.name === "worker";
|
|
1131
|
+
const services = [];
|
|
1132
|
+
const volumes = [];
|
|
1133
|
+
if (database) {
|
|
1134
|
+
services.push(` postgres:
|
|
1135
|
+
image: postgres:16-alpine
|
|
1136
|
+
container_name: ${options.name}-postgres
|
|
1137
|
+
restart: unless-stopped
|
|
1138
|
+
environment:
|
|
1139
|
+
POSTGRES_USER: postgres
|
|
1140
|
+
POSTGRES_PASSWORD: postgres
|
|
1141
|
+
POSTGRES_DB: ${options.name.replace(/-/g, "_")}_dev
|
|
1142
|
+
ports:
|
|
1143
|
+
- '5432:5432'
|
|
1144
|
+
volumes:
|
|
1145
|
+
- postgres_data:/var/lib/postgresql/data
|
|
1146
|
+
healthcheck:
|
|
1147
|
+
test: ['CMD-SHELL', 'pg_isready -U postgres']
|
|
1148
|
+
interval: 5s
|
|
1149
|
+
timeout: 5s
|
|
1150
|
+
retries: 5`);
|
|
1151
|
+
volumes.push(" postgres_data:");
|
|
1152
|
+
}
|
|
1153
|
+
if (isServerless) {
|
|
1154
|
+
services.push(` redis:
|
|
1155
|
+
image: redis:7-alpine
|
|
1156
|
+
container_name: ${options.name}-redis
|
|
1157
|
+
restart: unless-stopped
|
|
1158
|
+
ports:
|
|
1159
|
+
- '6379:6379'
|
|
1160
|
+
volumes:
|
|
1161
|
+
- redis_data:/data
|
|
1162
|
+
healthcheck:
|
|
1163
|
+
test: ['CMD', 'redis-cli', 'ping']
|
|
1164
|
+
interval: 5s
|
|
1165
|
+
timeout: 5s
|
|
1166
|
+
retries: 5
|
|
1167
|
+
|
|
1168
|
+
serverless-redis:
|
|
1169
|
+
image: hiett/serverless-redis-http:latest
|
|
1170
|
+
container_name: ${options.name}-serverless-redis
|
|
1171
|
+
restart: unless-stopped
|
|
1172
|
+
ports:
|
|
1173
|
+
- '8079:80'
|
|
1174
|
+
environment:
|
|
1175
|
+
SRH_MODE: env
|
|
1176
|
+
SRH_TOKEN: local_dev_token
|
|
1177
|
+
SRH_CONNECTION_STRING: redis://redis:6379
|
|
1178
|
+
depends_on:
|
|
1179
|
+
redis:
|
|
1180
|
+
condition: service_healthy`);
|
|
1181
|
+
volumes.push(" redis_data:");
|
|
1182
|
+
} else {
|
|
1183
|
+
services.push(` redis:
|
|
1184
|
+
image: redis:7-alpine
|
|
1185
|
+
container_name: ${options.name}-redis
|
|
1186
|
+
restart: unless-stopped
|
|
1187
|
+
ports:
|
|
1188
|
+
- '6379:6379'
|
|
1189
|
+
volumes:
|
|
1190
|
+
- redis_data:/data
|
|
1191
|
+
healthcheck:
|
|
1192
|
+
test: ['CMD', 'redis-cli', 'ping']
|
|
1193
|
+
interval: 5s
|
|
1194
|
+
timeout: 5s
|
|
1195
|
+
retries: 5`);
|
|
1196
|
+
volumes.push(" redis_data:");
|
|
1197
|
+
}
|
|
1198
|
+
if (hasWorker) {
|
|
1199
|
+
services.push(` rabbitmq:
|
|
1200
|
+
image: rabbitmq:3-management-alpine
|
|
1201
|
+
container_name: ${options.name}-rabbitmq
|
|
1202
|
+
restart: unless-stopped
|
|
1203
|
+
ports:
|
|
1204
|
+
- '5672:5672'
|
|
1205
|
+
- '15672:15672'
|
|
1206
|
+
environment:
|
|
1207
|
+
RABBITMQ_DEFAULT_USER: guest
|
|
1208
|
+
RABBITMQ_DEFAULT_PASS: guest
|
|
1209
|
+
volumes:
|
|
1210
|
+
- rabbitmq_data:/var/lib/rabbitmq
|
|
1211
|
+
healthcheck:
|
|
1212
|
+
test: ['CMD', 'rabbitmq-diagnostics', 'check_running']
|
|
1213
|
+
interval: 10s
|
|
1214
|
+
timeout: 5s
|
|
1215
|
+
retries: 5`);
|
|
1216
|
+
volumes.push(" rabbitmq_data:");
|
|
1217
|
+
}
|
|
1218
|
+
let dockerCompose = `version: '3.8'
|
|
1219
|
+
|
|
1220
|
+
services:
|
|
1221
|
+
${services.join("\n\n")}
|
|
1222
|
+
`;
|
|
1223
|
+
if (volumes.length > 0) dockerCompose += `
|
|
1224
|
+
volumes:
|
|
1225
|
+
${volumes.join("\n")}
|
|
1226
|
+
`;
|
|
1227
|
+
return [{
|
|
1228
|
+
path: "docker-compose.yml",
|
|
1229
|
+
content: dockerCompose
|
|
1230
|
+
}];
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
//#endregion
|
|
1234
|
+
//#region src/init/generators/env.ts
|
|
1235
|
+
/**
|
|
1236
|
+
* Generate environment files (.env, .env.example, .env.development, .env.test, .gitignore)
|
|
1237
|
+
*/
|
|
1238
|
+
function generateEnvFiles(options, template) {
|
|
1239
|
+
const { database } = options;
|
|
1240
|
+
const isServerless = template.name === "serverless";
|
|
1241
|
+
const hasWorker = template.name === "worker";
|
|
1242
|
+
let baseEnv = `# Application
|
|
1243
|
+
NODE_ENV=development
|
|
1244
|
+
PORT=3000
|
|
1245
|
+
LOG_LEVEL=info
|
|
1246
|
+
`;
|
|
1247
|
+
if (isServerless) baseEnv = `# AWS
|
|
1248
|
+
STAGE=dev
|
|
1249
|
+
AWS_REGION=us-east-1
|
|
1250
|
+
LOG_LEVEL=info
|
|
1251
|
+
`;
|
|
1252
|
+
if (database) baseEnv += `
|
|
1253
|
+
# Database
|
|
1254
|
+
DATABASE_URL=postgresql://user:password@localhost:5432/mydb
|
|
1255
|
+
`;
|
|
1256
|
+
if (hasWorker) baseEnv += `
|
|
1257
|
+
# Message Queue
|
|
1258
|
+
RABBITMQ_URL=amqp://localhost:5672
|
|
1259
|
+
`;
|
|
1260
|
+
baseEnv += `
|
|
1261
|
+
# Authentication
|
|
1262
|
+
JWT_SECRET=your-secret-key-change-in-production
|
|
1263
|
+
`;
|
|
1264
|
+
let devEnv = `# Development Environment
|
|
1265
|
+
NODE_ENV=development
|
|
1266
|
+
PORT=3000
|
|
1267
|
+
LOG_LEVEL=debug
|
|
1268
|
+
`;
|
|
1269
|
+
if (isServerless) devEnv = `# Development Environment
|
|
1270
|
+
STAGE=dev
|
|
1271
|
+
AWS_REGION=us-east-1
|
|
1272
|
+
LOG_LEVEL=debug
|
|
1273
|
+
`;
|
|
1274
|
+
if (database) devEnv += `
|
|
1275
|
+
# Database
|
|
1276
|
+
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/mydb_dev
|
|
1277
|
+
`;
|
|
1278
|
+
if (hasWorker) devEnv += `
|
|
1279
|
+
# Message Queue
|
|
1280
|
+
RABBITMQ_URL=amqp://localhost:5672
|
|
1281
|
+
`;
|
|
1282
|
+
devEnv += `
|
|
1283
|
+
# Authentication
|
|
1284
|
+
JWT_SECRET=dev-secret-not-for-production
|
|
1285
|
+
`;
|
|
1286
|
+
let testEnv = `# Test Environment
|
|
1287
|
+
NODE_ENV=test
|
|
1288
|
+
PORT=3001
|
|
1289
|
+
LOG_LEVEL=error
|
|
1290
|
+
`;
|
|
1291
|
+
if (isServerless) testEnv = `# Test Environment
|
|
1292
|
+
STAGE=test
|
|
1293
|
+
AWS_REGION=us-east-1
|
|
1294
|
+
LOG_LEVEL=error
|
|
1295
|
+
`;
|
|
1296
|
+
if (database) testEnv += `
|
|
1297
|
+
# Database
|
|
1298
|
+
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/mydb_test
|
|
1299
|
+
`;
|
|
1300
|
+
if (hasWorker) testEnv += `
|
|
1301
|
+
# Message Queue
|
|
1302
|
+
RABBITMQ_URL=amqp://localhost:5672
|
|
1303
|
+
`;
|
|
1304
|
+
testEnv += `
|
|
1305
|
+
# Authentication
|
|
1306
|
+
JWT_SECRET=test-secret-not-for-production
|
|
1307
|
+
`;
|
|
1308
|
+
const files = [
|
|
1309
|
+
{
|
|
1310
|
+
path: ".env.example",
|
|
1311
|
+
content: baseEnv
|
|
1312
|
+
},
|
|
1313
|
+
{
|
|
1314
|
+
path: ".env",
|
|
1315
|
+
content: baseEnv
|
|
1316
|
+
},
|
|
1317
|
+
{
|
|
1318
|
+
path: ".env.development",
|
|
1319
|
+
content: devEnv
|
|
1320
|
+
},
|
|
1321
|
+
{
|
|
1322
|
+
path: ".env.test",
|
|
1323
|
+
content: testEnv
|
|
1324
|
+
}
|
|
1325
|
+
];
|
|
1326
|
+
if (!options.monorepo) {
|
|
1327
|
+
const gitignore = `# Dependencies
|
|
1328
|
+
node_modules/
|
|
1329
|
+
|
|
1330
|
+
# Build output
|
|
1331
|
+
dist/
|
|
1332
|
+
.gkm/
|
|
1333
|
+
|
|
1334
|
+
# Environment
|
|
1335
|
+
.env
|
|
1336
|
+
.env.local
|
|
1337
|
+
.env.*.local
|
|
1338
|
+
|
|
1339
|
+
# IDE
|
|
1340
|
+
.idea/
|
|
1341
|
+
.vscode/
|
|
1342
|
+
*.swp
|
|
1343
|
+
*.swo
|
|
1344
|
+
|
|
1345
|
+
# OS
|
|
1346
|
+
.DS_Store
|
|
1347
|
+
Thumbs.db
|
|
1348
|
+
|
|
1349
|
+
# Logs
|
|
1350
|
+
*.log
|
|
1351
|
+
npm-debug.log*
|
|
1352
|
+
yarn-debug.log*
|
|
1353
|
+
pnpm-debug.log*
|
|
1354
|
+
|
|
1355
|
+
# Test coverage
|
|
1356
|
+
coverage/
|
|
1357
|
+
|
|
1358
|
+
# TypeScript cache
|
|
1359
|
+
*.tsbuildinfo
|
|
1360
|
+
`;
|
|
1361
|
+
files.push({
|
|
1362
|
+
path: ".gitignore",
|
|
1363
|
+
content: gitignore
|
|
1364
|
+
});
|
|
1365
|
+
}
|
|
1366
|
+
return files;
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
//#endregion
|
|
1370
|
+
//#region src/init/generators/models.ts
|
|
1371
|
+
/**
|
|
1372
|
+
* Generate packages/models for shared Zod schemas (monorepo only)
|
|
1373
|
+
*/
|
|
1374
|
+
function generateModelsPackage(options) {
|
|
1375
|
+
if (!options.monorepo) return [];
|
|
1376
|
+
const packageName = `@${options.name}/models`;
|
|
1377
|
+
const packageJson = {
|
|
1378
|
+
name: packageName,
|
|
1379
|
+
version: "0.0.1",
|
|
1380
|
+
private: true,
|
|
1381
|
+
type: "module",
|
|
1382
|
+
exports: {
|
|
1383
|
+
".": {
|
|
1384
|
+
types: "./dist/index.d.ts",
|
|
1385
|
+
import: "./dist/index.js"
|
|
1386
|
+
},
|
|
1387
|
+
"./*": {
|
|
1388
|
+
types: "./dist/*.d.ts",
|
|
1389
|
+
import: "./dist/*.js"
|
|
1390
|
+
}
|
|
1391
|
+
},
|
|
1392
|
+
scripts: {
|
|
1393
|
+
build: "tsc",
|
|
1394
|
+
"build:watch": "tsc --watch",
|
|
1395
|
+
typecheck: "tsc --noEmit"
|
|
1396
|
+
},
|
|
1397
|
+
dependencies: { zod: "~4.1.0" },
|
|
1398
|
+
devDependencies: { typescript: "~5.8.2" }
|
|
1399
|
+
};
|
|
1400
|
+
const tsConfig = {
|
|
1401
|
+
extends: "../../tsconfig.json",
|
|
1402
|
+
compilerOptions: {
|
|
1403
|
+
outDir: "./dist",
|
|
1404
|
+
rootDir: "./src"
|
|
1405
|
+
},
|
|
1406
|
+
include: ["src/**/*.ts"],
|
|
1407
|
+
exclude: ["node_modules", "dist"]
|
|
1408
|
+
};
|
|
1409
|
+
const indexTs = `import { z } from 'zod';
|
|
1410
|
+
|
|
1411
|
+
// ============================================
|
|
1412
|
+
// Common Schemas
|
|
1413
|
+
// ============================================
|
|
1414
|
+
|
|
1415
|
+
export const idSchema = z.string().uuid();
|
|
1416
|
+
|
|
1417
|
+
export const timestampsSchema = z.object({
|
|
1418
|
+
createdAt: z.coerce.date(),
|
|
1419
|
+
updatedAt: z.coerce.date(),
|
|
1420
|
+
});
|
|
1421
|
+
|
|
1422
|
+
export const paginationSchema = z.object({
|
|
1423
|
+
page: z.coerce.number().int().positive().default(1),
|
|
1424
|
+
limit: z.coerce.number().int().positive().max(100).default(20),
|
|
1425
|
+
});
|
|
1426
|
+
|
|
1427
|
+
export const paginatedResponseSchema = <T extends z.ZodTypeAny>(itemSchema: T) =>
|
|
1428
|
+
z.object({
|
|
1429
|
+
items: z.array(itemSchema),
|
|
1430
|
+
total: z.number(),
|
|
1431
|
+
page: z.number(),
|
|
1432
|
+
limit: z.number(),
|
|
1433
|
+
totalPages: z.number(),
|
|
1434
|
+
});
|
|
1435
|
+
|
|
1436
|
+
// ============================================
|
|
1437
|
+
// User Schemas
|
|
1438
|
+
// ============================================
|
|
1439
|
+
|
|
1440
|
+
export const userSchema = z.object({
|
|
1441
|
+
id: idSchema,
|
|
1442
|
+
email: z.string().email(),
|
|
1443
|
+
name: z.string().min(1).max(100),
|
|
1444
|
+
...timestampsSchema.shape,
|
|
1445
|
+
});
|
|
1446
|
+
|
|
1447
|
+
export const createUserSchema = userSchema.omit({
|
|
1448
|
+
id: true,
|
|
1449
|
+
createdAt: true,
|
|
1450
|
+
updatedAt: true,
|
|
1451
|
+
});
|
|
1452
|
+
|
|
1453
|
+
export const updateUserSchema = createUserSchema.partial();
|
|
1454
|
+
|
|
1455
|
+
// ============================================
|
|
1456
|
+
// Type Exports
|
|
1457
|
+
// ============================================
|
|
1458
|
+
|
|
1459
|
+
export type Id = z.infer<typeof idSchema>;
|
|
1460
|
+
export type Timestamps = z.infer<typeof timestampsSchema>;
|
|
1461
|
+
export type Pagination = z.infer<typeof paginationSchema>;
|
|
1462
|
+
export type User = z.infer<typeof userSchema>;
|
|
1463
|
+
export type CreateUser = z.infer<typeof createUserSchema>;
|
|
1464
|
+
export type UpdateUser = z.infer<typeof updateUserSchema>;
|
|
1465
|
+
`;
|
|
1466
|
+
return [
|
|
1467
|
+
{
|
|
1468
|
+
path: "packages/models/package.json",
|
|
1469
|
+
content: JSON.stringify(packageJson, null, 2) + "\n"
|
|
1470
|
+
},
|
|
1471
|
+
{
|
|
1472
|
+
path: "packages/models/tsconfig.json",
|
|
1473
|
+
content: JSON.stringify(tsConfig, null, 2) + "\n"
|
|
1474
|
+
},
|
|
1475
|
+
{
|
|
1476
|
+
path: "packages/models/src/index.ts",
|
|
1477
|
+
content: indexTs
|
|
1478
|
+
}
|
|
1479
|
+
];
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
//#endregion
|
|
1483
|
+
//#region src/init/generators/monorepo.ts
|
|
1484
|
+
/**
|
|
1485
|
+
* Generate monorepo root files (pnpm-workspace.yaml, root package.json, etc.)
|
|
1486
|
+
*/
|
|
1487
|
+
function generateMonorepoFiles(options, _template) {
|
|
1488
|
+
if (!options.monorepo) return [];
|
|
1489
|
+
const rootPackageJson = {
|
|
1490
|
+
name: options.name,
|
|
1491
|
+
version: "0.0.1",
|
|
1492
|
+
private: true,
|
|
1493
|
+
type: "module",
|
|
1494
|
+
scripts: {
|
|
1495
|
+
dev: "turbo dev",
|
|
1496
|
+
build: "turbo build",
|
|
1497
|
+
test: "turbo test",
|
|
1498
|
+
"test:once": "turbo test:once",
|
|
1499
|
+
typecheck: "turbo typecheck",
|
|
1500
|
+
lint: "biome lint .",
|
|
1501
|
+
fmt: "biome format . --write",
|
|
1502
|
+
"fmt:check": "biome format ."
|
|
1503
|
+
},
|
|
1504
|
+
devDependencies: {
|
|
1505
|
+
"@biomejs/biome": "~1.9.4",
|
|
1506
|
+
turbo: "~2.3.0",
|
|
1507
|
+
typescript: "~5.8.2",
|
|
1508
|
+
vitest: "~4.0.0"
|
|
1509
|
+
}
|
|
1510
|
+
};
|
|
1511
|
+
const apiPathParts = options.apiPath.split("/");
|
|
1512
|
+
const appsFolder = apiPathParts[0] || "apps";
|
|
1513
|
+
const pnpmWorkspace = `packages:
|
|
1514
|
+
- '${appsFolder}/*'
|
|
1515
|
+
- 'packages/*'
|
|
1516
|
+
`;
|
|
1517
|
+
const biomeConfig = {
|
|
1518
|
+
$schema: "https://biomejs.dev/schemas/1.9.4/schema.json",
|
|
1519
|
+
vcs: {
|
|
1520
|
+
enabled: true,
|
|
1521
|
+
clientKind: "git",
|
|
1522
|
+
useIgnoreFile: true
|
|
1523
|
+
},
|
|
1524
|
+
organizeImports: { enabled: true },
|
|
1525
|
+
formatter: {
|
|
1526
|
+
enabled: true,
|
|
1527
|
+
indentStyle: "space",
|
|
1528
|
+
indentWidth: 2,
|
|
1529
|
+
lineWidth: 80
|
|
1530
|
+
},
|
|
1531
|
+
javascript: { formatter: {
|
|
1532
|
+
quoteStyle: "single",
|
|
1533
|
+
trailingCommas: "all",
|
|
1534
|
+
semicolons: "always",
|
|
1535
|
+
arrowParentheses: "always"
|
|
1536
|
+
} },
|
|
1537
|
+
linter: {
|
|
1538
|
+
enabled: true,
|
|
1539
|
+
rules: {
|
|
1540
|
+
recommended: true,
|
|
1541
|
+
correctness: {
|
|
1542
|
+
noUnusedImports: "error",
|
|
1543
|
+
noUnusedVariables: "error"
|
|
1544
|
+
},
|
|
1545
|
+
style: { noNonNullAssertion: "off" }
|
|
1546
|
+
}
|
|
1547
|
+
},
|
|
1548
|
+
files: { ignore: [
|
|
1549
|
+
"node_modules",
|
|
1550
|
+
"dist",
|
|
1551
|
+
".gkm",
|
|
1552
|
+
"coverage"
|
|
1553
|
+
] }
|
|
1554
|
+
};
|
|
1555
|
+
const turboConfig = {
|
|
1556
|
+
$schema: "https://turbo.build/schema.json",
|
|
1557
|
+
tasks: {
|
|
1558
|
+
build: {
|
|
1559
|
+
dependsOn: ["^build"],
|
|
1560
|
+
outputs: ["dist/**"]
|
|
1561
|
+
},
|
|
1562
|
+
dev: {
|
|
1563
|
+
cache: false,
|
|
1564
|
+
persistent: true
|
|
1565
|
+
},
|
|
1566
|
+
test: {
|
|
1567
|
+
dependsOn: ["^build"],
|
|
1568
|
+
cache: false
|
|
1569
|
+
},
|
|
1570
|
+
"test:once": {
|
|
1571
|
+
dependsOn: ["^build"],
|
|
1572
|
+
outputs: ["coverage/**"]
|
|
1573
|
+
},
|
|
1574
|
+
typecheck: {
|
|
1575
|
+
dependsOn: ["^build"],
|
|
1576
|
+
outputs: []
|
|
1577
|
+
},
|
|
1578
|
+
lint: { outputs: [] },
|
|
1579
|
+
fmt: { outputs: [] }
|
|
1580
|
+
}
|
|
1581
|
+
};
|
|
1582
|
+
const gitignore = `# Dependencies
|
|
1583
|
+
node_modules/
|
|
1584
|
+
|
|
1585
|
+
# Build output
|
|
1586
|
+
dist/
|
|
1587
|
+
.gkm/
|
|
1588
|
+
|
|
1589
|
+
# Environment
|
|
1590
|
+
.env
|
|
1591
|
+
.env.local
|
|
1592
|
+
.env.*.local
|
|
1593
|
+
|
|
1594
|
+
# IDE
|
|
1595
|
+
.idea/
|
|
1596
|
+
.vscode/
|
|
1597
|
+
*.swp
|
|
1598
|
+
*.swo
|
|
1599
|
+
|
|
1600
|
+
# OS
|
|
1601
|
+
.DS_Store
|
|
1602
|
+
Thumbs.db
|
|
1603
|
+
|
|
1604
|
+
# Logs
|
|
1605
|
+
*.log
|
|
1606
|
+
npm-debug.log*
|
|
1607
|
+
yarn-debug.log*
|
|
1608
|
+
pnpm-debug.log*
|
|
1609
|
+
|
|
1610
|
+
# Test coverage
|
|
1611
|
+
coverage/
|
|
1612
|
+
|
|
1613
|
+
# TypeScript cache
|
|
1614
|
+
*.tsbuildinfo
|
|
1615
|
+
|
|
1616
|
+
# Turbo
|
|
1617
|
+
.turbo/
|
|
1618
|
+
`;
|
|
1619
|
+
const tsConfig = {
|
|
1620
|
+
compilerOptions: {
|
|
1621
|
+
target: "ES2022",
|
|
1622
|
+
module: "NodeNext",
|
|
1623
|
+
moduleResolution: "NodeNext",
|
|
1624
|
+
lib: ["ES2022"],
|
|
1625
|
+
strict: true,
|
|
1626
|
+
esModuleInterop: true,
|
|
1627
|
+
skipLibCheck: true,
|
|
1628
|
+
forceConsistentCasingInFileNames: true,
|
|
1629
|
+
resolveJsonModule: true,
|
|
1630
|
+
declaration: true,
|
|
1631
|
+
declarationMap: true,
|
|
1632
|
+
composite: true
|
|
1633
|
+
},
|
|
1634
|
+
exclude: ["node_modules", "dist"]
|
|
1635
|
+
};
|
|
1636
|
+
return [
|
|
1637
|
+
{
|
|
1638
|
+
path: "package.json",
|
|
1639
|
+
content: JSON.stringify(rootPackageJson, null, 2) + "\n"
|
|
1640
|
+
},
|
|
1641
|
+
{
|
|
1642
|
+
path: "pnpm-workspace.yaml",
|
|
1643
|
+
content: pnpmWorkspace
|
|
1644
|
+
},
|
|
1645
|
+
{
|
|
1646
|
+
path: "tsconfig.json",
|
|
1647
|
+
content: JSON.stringify(tsConfig, null, 2) + "\n"
|
|
1648
|
+
},
|
|
1649
|
+
{
|
|
1650
|
+
path: "biome.json",
|
|
1651
|
+
content: JSON.stringify(biomeConfig, null, 2) + "\n"
|
|
1652
|
+
},
|
|
1653
|
+
{
|
|
1654
|
+
path: "turbo.json",
|
|
1655
|
+
content: JSON.stringify(turboConfig, null, 2) + "\n"
|
|
1656
|
+
},
|
|
1657
|
+
{
|
|
1658
|
+
path: ".gitignore",
|
|
1659
|
+
content: gitignore
|
|
1660
|
+
}
|
|
1661
|
+
];
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
//#endregion
|
|
1665
|
+
//#region src/init/templates/api.ts
|
|
1666
|
+
const apiTemplate = {
|
|
1667
|
+
name: "api",
|
|
1668
|
+
description: "Full API with auth, database, services",
|
|
1669
|
+
dependencies: {
|
|
1670
|
+
"@geekmidas/constructs": "workspace:*",
|
|
1671
|
+
"@geekmidas/envkit": "workspace:*",
|
|
1672
|
+
"@geekmidas/logger": "workspace:*",
|
|
1673
|
+
"@geekmidas/services": "workspace:*",
|
|
1674
|
+
"@geekmidas/errors": "workspace:*",
|
|
1675
|
+
"@geekmidas/auth": "workspace:*",
|
|
1676
|
+
hono: "~4.8.2",
|
|
1677
|
+
pino: "~9.6.0"
|
|
1678
|
+
},
|
|
1679
|
+
devDependencies: {
|
|
1680
|
+
"@biomejs/biome": "~1.9.4",
|
|
1681
|
+
"@geekmidas/cli": "workspace:*",
|
|
1682
|
+
"@types/node": "~22.0.0",
|
|
1683
|
+
tsx: "~4.20.0",
|
|
1684
|
+
turbo: "~2.3.0",
|
|
1685
|
+
typescript: "~5.8.2",
|
|
1686
|
+
vitest: "~4.0.0"
|
|
1687
|
+
},
|
|
1688
|
+
scripts: {
|
|
1689
|
+
dev: "gkm dev",
|
|
1690
|
+
build: "gkm build",
|
|
1691
|
+
test: "vitest",
|
|
1692
|
+
"test:once": "vitest run",
|
|
1693
|
+
typecheck: "tsc --noEmit",
|
|
1694
|
+
lint: "biome lint .",
|
|
1695
|
+
fmt: "biome format . --write",
|
|
1696
|
+
"fmt:check": "biome format ."
|
|
1697
|
+
},
|
|
1698
|
+
files: (options) => {
|
|
1699
|
+
const { loggerType, routesStructure } = options;
|
|
1700
|
+
const loggerContent = `import { createLogger } from '@geekmidas/logger/${loggerType}';
|
|
1701
|
+
|
|
1702
|
+
export const logger = createLogger();
|
|
1703
|
+
`;
|
|
1704
|
+
const getRoutePath = (file) => {
|
|
1705
|
+
switch (routesStructure) {
|
|
1706
|
+
case "centralized-endpoints": return `src/endpoints/${file}`;
|
|
1707
|
+
case "centralized-routes": return `src/routes/${file}`;
|
|
1708
|
+
case "domain-based": {
|
|
1709
|
+
const parts = file.split("/");
|
|
1710
|
+
if (parts.length === 1) return `src/${file.replace(".ts", "")}/routes/index.ts`;
|
|
1711
|
+
return `src/${parts[0]}/routes/${parts.slice(1).join("/")}`;
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
};
|
|
1715
|
+
const files = [
|
|
1716
|
+
{
|
|
1717
|
+
path: "src/config/env.ts",
|
|
1718
|
+
content: `import { EnvironmentParser } from '@geekmidas/envkit';
|
|
1719
|
+
|
|
1720
|
+
export const envParser = new EnvironmentParser(process.env);
|
|
1721
|
+
|
|
1722
|
+
export const config = envParser
|
|
1723
|
+
.create((get) => ({
|
|
1724
|
+
port: get('PORT').string().transform(Number).default(3000),
|
|
1725
|
+
nodeEnv: get('NODE_ENV').string().default('development'),
|
|
1726
|
+
jwtSecret: get('JWT_SECRET').string().default('change-me-in-production'),${options.database ? `
|
|
1727
|
+
database: {
|
|
1728
|
+
url: get('DATABASE_URL').string().default('postgresql://localhost:5432/mydb'),
|
|
1729
|
+
},` : ""}
|
|
1730
|
+
}))
|
|
1731
|
+
.parse();
|
|
1732
|
+
`
|
|
1733
|
+
},
|
|
1734
|
+
{
|
|
1735
|
+
path: "src/config/logger.ts",
|
|
1736
|
+
content: loggerContent
|
|
1737
|
+
},
|
|
1738
|
+
{
|
|
1739
|
+
path: getRoutePath("health.ts"),
|
|
1740
|
+
content: `import { e } from '@geekmidas/constructs/endpoints';
|
|
1741
|
+
|
|
1742
|
+
export default e
|
|
1743
|
+
.get('/health')
|
|
1744
|
+
.handle(async () => ({
|
|
1745
|
+
status: 'ok',
|
|
1746
|
+
timestamp: new Date().toISOString(),
|
|
1747
|
+
}));
|
|
1748
|
+
`
|
|
1749
|
+
},
|
|
1750
|
+
{
|
|
1751
|
+
path: getRoutePath("users/list.ts"),
|
|
1752
|
+
content: `import { e } from '@geekmidas/constructs/endpoints';
|
|
1753
|
+
|
|
1754
|
+
export default e
|
|
1755
|
+
.get('/users')
|
|
1756
|
+
.handle(async () => ({
|
|
1757
|
+
users: [
|
|
1758
|
+
{ id: '1', name: 'Alice' },
|
|
1759
|
+
{ id: '2', name: 'Bob' },
|
|
1760
|
+
],
|
|
1761
|
+
}));
|
|
1762
|
+
`
|
|
1763
|
+
},
|
|
1764
|
+
{
|
|
1765
|
+
path: getRoutePath("users/get.ts"),
|
|
1766
|
+
content: `import { e } from '@geekmidas/constructs/endpoints';
|
|
1767
|
+
import { z } from 'zod';
|
|
1768
|
+
|
|
1769
|
+
export default e
|
|
1770
|
+
.get('/users/:id')
|
|
1771
|
+
.params(z.object({ id: z.string() }))
|
|
1772
|
+
.handle(async ({ params }) => ({
|
|
1773
|
+
id: params.id,
|
|
1774
|
+
name: 'Alice',
|
|
1775
|
+
email: 'alice@example.com',
|
|
1776
|
+
}));
|
|
1777
|
+
`
|
|
1778
|
+
}
|
|
1779
|
+
];
|
|
1780
|
+
if (options.database) files.push({
|
|
1781
|
+
path: "src/services/database.ts",
|
|
1782
|
+
content: `import type { Service } from '@geekmidas/services';
|
|
1783
|
+
import { Kysely, PostgresDialect } from 'kysely';
|
|
1784
|
+
import pg from 'pg';
|
|
1785
|
+
|
|
1786
|
+
// Define your database schema
|
|
1787
|
+
export interface Database {
|
|
1788
|
+
users: {
|
|
1789
|
+
id: string;
|
|
1790
|
+
name: string;
|
|
1791
|
+
email: string;
|
|
1792
|
+
created_at: Date;
|
|
1793
|
+
};
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
export const databaseService = {
|
|
1797
|
+
serviceName: 'database' as const,
|
|
1798
|
+
async register(envParser) {
|
|
1799
|
+
const config = envParser
|
|
1800
|
+
.create((get) => ({
|
|
1801
|
+
url: get('DATABASE_URL').string(),
|
|
1802
|
+
}))
|
|
1803
|
+
.parse();
|
|
1804
|
+
|
|
1805
|
+
return new Kysely<Database>({
|
|
1806
|
+
dialect: new PostgresDialect({
|
|
1807
|
+
pool: new pg.Pool({ connectionString: config.url }),
|
|
1808
|
+
}),
|
|
1809
|
+
});
|
|
1810
|
+
},
|
|
1811
|
+
} satisfies Service<'database', Kysely<Database>>;
|
|
1812
|
+
`
|
|
1813
|
+
});
|
|
1814
|
+
if (options.telescope) files.push({
|
|
1815
|
+
path: "src/config/telescope.ts",
|
|
1816
|
+
content: `import { Telescope } from '@geekmidas/telescope';
|
|
1817
|
+
import { InMemoryStorage } from '@geekmidas/telescope/storage/memory';
|
|
1818
|
+
|
|
1819
|
+
export const telescope = new Telescope({
|
|
1820
|
+
storage: new InMemoryStorage({ maxEntries: 100 }),
|
|
1821
|
+
enabled: process.env.NODE_ENV === 'development',
|
|
1822
|
+
});
|
|
1823
|
+
`
|
|
1824
|
+
});
|
|
1825
|
+
if (options.studio && options.database) files.push({
|
|
1826
|
+
path: "src/config/studio.ts",
|
|
1827
|
+
content: `import { Direction, InMemoryMonitoringStorage, Studio } from '@geekmidas/studio';
|
|
1828
|
+
import { Kysely, PostgresDialect } from 'kysely';
|
|
1829
|
+
import pg from 'pg';
|
|
1830
|
+
import type { Database } from '../services/database';
|
|
1831
|
+
import { config } from './env';
|
|
1832
|
+
|
|
1833
|
+
// Create a Kysely instance for Studio
|
|
1834
|
+
const db = new Kysely<Database>({
|
|
1835
|
+
dialect: new PostgresDialect({
|
|
1836
|
+
pool: new pg.Pool({ connectionString: config.database.url }),
|
|
1837
|
+
}),
|
|
1838
|
+
});
|
|
1839
|
+
|
|
1840
|
+
export const studio = new Studio<Database>({
|
|
1841
|
+
monitoring: {
|
|
1842
|
+
storage: new InMemoryMonitoringStorage({ maxEntries: 100 }),
|
|
1843
|
+
},
|
|
1844
|
+
data: {
|
|
1845
|
+
db,
|
|
1846
|
+
cursor: { field: 'id', direction: Direction.Desc },
|
|
1847
|
+
},
|
|
1848
|
+
enabled: process.env.NODE_ENV === 'development',
|
|
1849
|
+
});
|
|
1850
|
+
`
|
|
1851
|
+
});
|
|
1852
|
+
return files;
|
|
1853
|
+
}
|
|
1854
|
+
};
|
|
1855
|
+
|
|
1856
|
+
//#endregion
|
|
1857
|
+
//#region src/init/templates/minimal.ts
|
|
1858
|
+
const minimalTemplate = {
|
|
1859
|
+
name: "minimal",
|
|
1860
|
+
description: "Basic health endpoint",
|
|
1861
|
+
dependencies: {
|
|
1862
|
+
"@geekmidas/constructs": "workspace:*",
|
|
1863
|
+
"@geekmidas/envkit": "workspace:*",
|
|
1864
|
+
"@geekmidas/logger": "workspace:*",
|
|
1865
|
+
hono: "~4.8.2",
|
|
1866
|
+
pino: "~9.6.0"
|
|
1867
|
+
},
|
|
1868
|
+
devDependencies: {
|
|
1869
|
+
"@biomejs/biome": "~1.9.4",
|
|
1870
|
+
"@geekmidas/cli": "workspace:*",
|
|
1871
|
+
"@types/node": "~22.0.0",
|
|
1872
|
+
tsx: "~4.20.0",
|
|
1873
|
+
turbo: "~2.3.0",
|
|
1874
|
+
typescript: "~5.8.2",
|
|
1875
|
+
vitest: "~4.0.0"
|
|
1876
|
+
},
|
|
1877
|
+
scripts: {
|
|
1878
|
+
dev: "gkm dev",
|
|
1879
|
+
build: "gkm build",
|
|
1880
|
+
test: "vitest",
|
|
1881
|
+
"test:once": "vitest run",
|
|
1882
|
+
typecheck: "tsc --noEmit",
|
|
1883
|
+
lint: "biome lint .",
|
|
1884
|
+
fmt: "biome format . --write",
|
|
1885
|
+
"fmt:check": "biome format ."
|
|
1886
|
+
},
|
|
1887
|
+
files: (options) => {
|
|
1888
|
+
const { loggerType, routesStructure } = options;
|
|
1889
|
+
const loggerContent = `import { createLogger } from '@geekmidas/logger/${loggerType}';
|
|
1890
|
+
|
|
1891
|
+
export const logger = createLogger();
|
|
1892
|
+
`;
|
|
1893
|
+
const getRoutePath = (file) => {
|
|
1894
|
+
switch (routesStructure) {
|
|
1895
|
+
case "centralized-endpoints": return `src/endpoints/${file}`;
|
|
1896
|
+
case "centralized-routes": return `src/routes/${file}`;
|
|
1897
|
+
case "domain-based": return `src/${file.replace(".ts", "")}/routes/index.ts`;
|
|
1898
|
+
}
|
|
1899
|
+
};
|
|
1900
|
+
const files = [
|
|
1901
|
+
{
|
|
1902
|
+
path: "src/config/env.ts",
|
|
1903
|
+
content: `import { EnvironmentParser } from '@geekmidas/envkit';
|
|
1904
|
+
|
|
1905
|
+
export const envParser = new EnvironmentParser(process.env);
|
|
1906
|
+
|
|
1907
|
+
export const config = envParser
|
|
1908
|
+
.create((get) => ({
|
|
1909
|
+
port: get('PORT').string().transform(Number).default(3000),
|
|
1910
|
+
nodeEnv: get('NODE_ENV').string().default('development'),
|
|
1911
|
+
}))
|
|
1912
|
+
.parse();
|
|
1913
|
+
`
|
|
1914
|
+
},
|
|
1915
|
+
{
|
|
1916
|
+
path: "src/config/logger.ts",
|
|
1917
|
+
content: loggerContent
|
|
1918
|
+
},
|
|
1919
|
+
{
|
|
1920
|
+
path: getRoutePath("health.ts"),
|
|
1921
|
+
content: `import { e } from '@geekmidas/constructs/endpoints';
|
|
1922
|
+
|
|
1923
|
+
export default e
|
|
1924
|
+
.get('/health')
|
|
1925
|
+
.handle(async () => ({
|
|
1926
|
+
status: 'ok',
|
|
1927
|
+
timestamp: new Date().toISOString(),
|
|
1928
|
+
}));
|
|
1929
|
+
`
|
|
1930
|
+
}
|
|
1931
|
+
];
|
|
1932
|
+
if (options.database) {
|
|
1933
|
+
files[0] = {
|
|
1934
|
+
path: "src/config/env.ts",
|
|
1935
|
+
content: `import { EnvironmentParser } from '@geekmidas/envkit';
|
|
1936
|
+
|
|
1937
|
+
export const envParser = new EnvironmentParser(process.env);
|
|
1938
|
+
|
|
1939
|
+
export const config = envParser
|
|
1940
|
+
.create((get) => ({
|
|
1941
|
+
port: get('PORT').string().transform(Number).default(3000),
|
|
1942
|
+
nodeEnv: get('NODE_ENV').string().default('development'),
|
|
1943
|
+
database: {
|
|
1944
|
+
url: get('DATABASE_URL').string().default('postgresql://localhost:5432/mydb'),
|
|
1945
|
+
},
|
|
1946
|
+
}))
|
|
1947
|
+
.parse();
|
|
1948
|
+
`
|
|
1949
|
+
};
|
|
1950
|
+
files.push({
|
|
1951
|
+
path: "src/services/database.ts",
|
|
1952
|
+
content: `import type { Service } from '@geekmidas/services';
|
|
1953
|
+
import { Kysely, PostgresDialect } from 'kysely';
|
|
1954
|
+
import pg from 'pg';
|
|
1955
|
+
|
|
1956
|
+
// Define your database schema
|
|
1957
|
+
export interface Database {
|
|
1958
|
+
// Add your tables here
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1961
|
+
export const databaseService = {
|
|
1962
|
+
serviceName: 'database' as const,
|
|
1963
|
+
async register(envParser) {
|
|
1964
|
+
const config = envParser
|
|
1965
|
+
.create((get) => ({
|
|
1966
|
+
url: get('DATABASE_URL').string(),
|
|
1967
|
+
}))
|
|
1968
|
+
.parse();
|
|
1969
|
+
|
|
1970
|
+
return new Kysely<Database>({
|
|
1971
|
+
dialect: new PostgresDialect({
|
|
1972
|
+
pool: new pg.Pool({ connectionString: config.url }),
|
|
1973
|
+
}),
|
|
1974
|
+
});
|
|
1975
|
+
},
|
|
1976
|
+
} satisfies Service<'database', Kysely<Database>>;
|
|
1977
|
+
`
|
|
1978
|
+
});
|
|
1979
|
+
}
|
|
1980
|
+
if (options.telescope) files.push({
|
|
1981
|
+
path: "src/config/telescope.ts",
|
|
1982
|
+
content: `import { Telescope } from '@geekmidas/telescope';
|
|
1983
|
+
import { InMemoryStorage } from '@geekmidas/telescope/storage/memory';
|
|
1984
|
+
|
|
1985
|
+
export const telescope = new Telescope({
|
|
1986
|
+
storage: new InMemoryStorage({ maxEntries: 100 }),
|
|
1987
|
+
enabled: process.env.NODE_ENV === 'development',
|
|
1988
|
+
});
|
|
1989
|
+
`
|
|
1990
|
+
});
|
|
1991
|
+
if (options.studio && options.database) files.push({
|
|
1992
|
+
path: "src/config/studio.ts",
|
|
1993
|
+
content: `import { Direction, InMemoryMonitoringStorage, Studio } from '@geekmidas/studio';
|
|
1994
|
+
import { Kysely, PostgresDialect } from 'kysely';
|
|
1995
|
+
import pg from 'pg';
|
|
1996
|
+
import type { Database } from '../services/database';
|
|
1997
|
+
import { config } from './env';
|
|
1998
|
+
|
|
1999
|
+
// Create a Kysely instance for Studio
|
|
2000
|
+
const db = new Kysely<Database>({
|
|
2001
|
+
dialect: new PostgresDialect({
|
|
2002
|
+
pool: new pg.Pool({ connectionString: config.database.url }),
|
|
2003
|
+
}),
|
|
2004
|
+
});
|
|
2005
|
+
|
|
2006
|
+
export const studio = new Studio<Database>({
|
|
2007
|
+
monitoring: {
|
|
2008
|
+
storage: new InMemoryMonitoringStorage({ maxEntries: 100 }),
|
|
2009
|
+
},
|
|
2010
|
+
data: {
|
|
2011
|
+
db,
|
|
2012
|
+
cursor: { field: 'id', direction: Direction.Desc },
|
|
2013
|
+
},
|
|
2014
|
+
enabled: process.env.NODE_ENV === 'development',
|
|
2015
|
+
});
|
|
2016
|
+
`
|
|
2017
|
+
});
|
|
2018
|
+
return files;
|
|
2019
|
+
}
|
|
2020
|
+
};
|
|
2021
|
+
|
|
2022
|
+
//#endregion
|
|
2023
|
+
//#region src/init/templates/serverless.ts
|
|
2024
|
+
const serverlessTemplate = {
|
|
2025
|
+
name: "serverless",
|
|
2026
|
+
description: "AWS Lambda handlers",
|
|
2027
|
+
dependencies: {
|
|
2028
|
+
"@geekmidas/constructs": "workspace:*",
|
|
2029
|
+
"@geekmidas/envkit": "workspace:*",
|
|
2030
|
+
"@geekmidas/logger": "workspace:*",
|
|
2031
|
+
"@geekmidas/cloud": "workspace:*",
|
|
2032
|
+
hono: "~4.8.2",
|
|
2033
|
+
pino: "~9.6.0"
|
|
2034
|
+
},
|
|
2035
|
+
devDependencies: {
|
|
2036
|
+
"@biomejs/biome": "~1.9.4",
|
|
2037
|
+
"@geekmidas/cli": "workspace:*",
|
|
2038
|
+
"@types/aws-lambda": "~8.10.92",
|
|
2039
|
+
"@types/node": "~22.0.0",
|
|
2040
|
+
tsx: "~4.20.0",
|
|
2041
|
+
turbo: "~2.3.0",
|
|
2042
|
+
typescript: "~5.8.2",
|
|
2043
|
+
vitest: "~4.0.0"
|
|
2044
|
+
},
|
|
2045
|
+
scripts: {
|
|
2046
|
+
dev: "gkm dev",
|
|
2047
|
+
build: "gkm build --provider aws-apigatewayv2",
|
|
2048
|
+
test: "vitest",
|
|
2049
|
+
"test:once": "vitest run",
|
|
2050
|
+
typecheck: "tsc --noEmit",
|
|
2051
|
+
lint: "biome lint .",
|
|
2052
|
+
fmt: "biome format . --write",
|
|
2053
|
+
"fmt:check": "biome format ."
|
|
2054
|
+
},
|
|
2055
|
+
files: (options) => {
|
|
2056
|
+
const { loggerType, routesStructure } = options;
|
|
2057
|
+
const loggerContent = `import { createLogger } from '@geekmidas/logger/${loggerType}';
|
|
2058
|
+
|
|
2059
|
+
export const logger = createLogger();
|
|
2060
|
+
`;
|
|
2061
|
+
const getRoutePath = (file) => {
|
|
2062
|
+
switch (routesStructure) {
|
|
2063
|
+
case "centralized-endpoints": return `src/endpoints/${file}`;
|
|
2064
|
+
case "centralized-routes": return `src/routes/${file}`;
|
|
2065
|
+
case "domain-based": return `src/${file.replace(".ts", "")}/routes/index.ts`;
|
|
2066
|
+
}
|
|
2067
|
+
};
|
|
2068
|
+
const files = [
|
|
2069
|
+
{
|
|
2070
|
+
path: "src/config/env.ts",
|
|
2071
|
+
content: `import { EnvironmentParser } from '@geekmidas/envkit';
|
|
2072
|
+
|
|
2073
|
+
export const envParser = new EnvironmentParser(process.env);
|
|
2074
|
+
|
|
2075
|
+
export const config = envParser
|
|
2076
|
+
.create((get) => ({
|
|
2077
|
+
stage: get('STAGE').string().default('dev'),
|
|
2078
|
+
region: get('AWS_REGION').string().default('us-east-1'),${options.database ? `
|
|
2079
|
+
database: {
|
|
2080
|
+
url: get('DATABASE_URL').string(),
|
|
2081
|
+
},` : ""}
|
|
2082
|
+
}))
|
|
2083
|
+
.parse();
|
|
2084
|
+
`
|
|
2085
|
+
},
|
|
2086
|
+
{
|
|
2087
|
+
path: "src/config/logger.ts",
|
|
2088
|
+
content: loggerContent
|
|
2089
|
+
},
|
|
2090
|
+
{
|
|
2091
|
+
path: getRoutePath("health.ts"),
|
|
2092
|
+
content: `import { e } from '@geekmidas/constructs/endpoints';
|
|
2093
|
+
|
|
2094
|
+
export default e
|
|
2095
|
+
.get('/health')
|
|
2096
|
+
.handle(async () => ({
|
|
2097
|
+
status: 'ok',
|
|
2098
|
+
timestamp: new Date().toISOString(),
|
|
2099
|
+
region: process.env.AWS_REGION || 'local',
|
|
2100
|
+
}));
|
|
2101
|
+
`
|
|
2102
|
+
},
|
|
2103
|
+
{
|
|
2104
|
+
path: "src/functions/hello.ts",
|
|
2105
|
+
content: `import { f } from '@geekmidas/constructs/functions';
|
|
2106
|
+
import { z } from 'zod';
|
|
2107
|
+
|
|
2108
|
+
export default f
|
|
2109
|
+
.input(z.object({ name: z.string() }))
|
|
2110
|
+
.output(z.object({ message: z.string() }))
|
|
2111
|
+
.handle(async ({ input }) => ({
|
|
2112
|
+
message: \`Hello, \${input.name}!\`,
|
|
2113
|
+
}));
|
|
2114
|
+
`
|
|
2115
|
+
}
|
|
2116
|
+
];
|
|
2117
|
+
if (options.telescope) files.push({
|
|
2118
|
+
path: "src/config/telescope.ts",
|
|
2119
|
+
content: `import { Telescope } from '@geekmidas/telescope';
|
|
2120
|
+
import { InMemoryStorage } from '@geekmidas/telescope/storage/memory';
|
|
2121
|
+
|
|
2122
|
+
// Note: For production Lambda, consider using a persistent storage
|
|
2123
|
+
export const telescope = new Telescope({
|
|
2124
|
+
storage: new InMemoryStorage({ maxEntries: 50 }),
|
|
2125
|
+
enabled: process.env.STAGE === 'dev',
|
|
2126
|
+
});
|
|
2127
|
+
`
|
|
2128
|
+
});
|
|
2129
|
+
return files;
|
|
2130
|
+
}
|
|
2131
|
+
};
|
|
2132
|
+
|
|
2133
|
+
//#endregion
|
|
2134
|
+
//#region src/init/templates/worker.ts
|
|
2135
|
+
const workerTemplate = {
|
|
2136
|
+
name: "worker",
|
|
2137
|
+
description: "Background job processing",
|
|
2138
|
+
dependencies: {
|
|
2139
|
+
"@geekmidas/constructs": "workspace:*",
|
|
2140
|
+
"@geekmidas/envkit": "workspace:*",
|
|
2141
|
+
"@geekmidas/logger": "workspace:*",
|
|
2142
|
+
"@geekmidas/events": "workspace:*",
|
|
2143
|
+
hono: "~4.8.2",
|
|
2144
|
+
pino: "~9.6.0"
|
|
2145
|
+
},
|
|
2146
|
+
devDependencies: {
|
|
2147
|
+
"@biomejs/biome": "~1.9.4",
|
|
2148
|
+
"@geekmidas/cli": "workspace:*",
|
|
2149
|
+
"@types/node": "~22.0.0",
|
|
2150
|
+
tsx: "~4.20.0",
|
|
2151
|
+
turbo: "~2.3.0",
|
|
2152
|
+
typescript: "~5.8.2",
|
|
2153
|
+
vitest: "~4.0.0"
|
|
2154
|
+
},
|
|
2155
|
+
scripts: {
|
|
2156
|
+
dev: "gkm dev",
|
|
2157
|
+
build: "gkm build",
|
|
2158
|
+
test: "vitest",
|
|
2159
|
+
"test:once": "vitest run",
|
|
2160
|
+
typecheck: "tsc --noEmit",
|
|
2161
|
+
lint: "biome lint .",
|
|
2162
|
+
fmt: "biome format . --write",
|
|
2163
|
+
"fmt:check": "biome format ."
|
|
2164
|
+
},
|
|
2165
|
+
files: (options) => {
|
|
2166
|
+
const { loggerType, routesStructure } = options;
|
|
2167
|
+
const loggerContent = `import { createLogger } from '@geekmidas/logger/${loggerType}';
|
|
2168
|
+
|
|
2169
|
+
export const logger = createLogger();
|
|
2170
|
+
`;
|
|
2171
|
+
const getRoutePath = (file) => {
|
|
2172
|
+
switch (routesStructure) {
|
|
2173
|
+
case "centralized-endpoints": return `src/endpoints/${file}`;
|
|
2174
|
+
case "centralized-routes": return `src/routes/${file}`;
|
|
2175
|
+
case "domain-based": return `src/${file.replace(".ts", "")}/routes/index.ts`;
|
|
2176
|
+
}
|
|
2177
|
+
};
|
|
2178
|
+
const files = [
|
|
2179
|
+
{
|
|
2180
|
+
path: "src/config/env.ts",
|
|
2181
|
+
content: `import { EnvironmentParser } from '@geekmidas/envkit';
|
|
2182
|
+
|
|
2183
|
+
export const envParser = new EnvironmentParser(process.env);
|
|
2184
|
+
|
|
2185
|
+
export const config = envParser
|
|
2186
|
+
.create((get) => ({
|
|
2187
|
+
port: get('PORT').string().transform(Number).default(3000),
|
|
2188
|
+
nodeEnv: get('NODE_ENV').string().default('development'),
|
|
2189
|
+
rabbitmq: {
|
|
2190
|
+
url: get('RABBITMQ_URL').string().default('amqp://localhost:5672'),
|
|
2191
|
+
},${options.database ? `
|
|
2192
|
+
database: {
|
|
2193
|
+
url: get('DATABASE_URL').string().default('postgresql://localhost:5432/mydb'),
|
|
2194
|
+
},` : ""}
|
|
2195
|
+
}))
|
|
2196
|
+
.parse();
|
|
2197
|
+
`
|
|
2198
|
+
},
|
|
2199
|
+
{
|
|
2200
|
+
path: "src/config/logger.ts",
|
|
2201
|
+
content: loggerContent
|
|
2202
|
+
},
|
|
2203
|
+
{
|
|
2204
|
+
path: getRoutePath("health.ts"),
|
|
2205
|
+
content: `import { e } from '@geekmidas/constructs/endpoints';
|
|
2206
|
+
|
|
2207
|
+
export default e
|
|
2208
|
+
.get('/health')
|
|
2209
|
+
.handle(async () => ({
|
|
2210
|
+
status: 'ok',
|
|
2211
|
+
timestamp: new Date().toISOString(),
|
|
2212
|
+
}));
|
|
2213
|
+
`
|
|
2214
|
+
},
|
|
2215
|
+
{
|
|
2216
|
+
path: "src/events/types.ts",
|
|
2217
|
+
content: `import type { PublishableMessage } from '@geekmidas/events';
|
|
2218
|
+
|
|
2219
|
+
// Define your event types here
|
|
2220
|
+
export type AppEvents =
|
|
2221
|
+
| PublishableMessage<'user.created', { userId: string; email: string }>
|
|
2222
|
+
| PublishableMessage<'user.updated', { userId: string; changes: Record<string, unknown> }>
|
|
2223
|
+
| PublishableMessage<'order.placed', { orderId: string; userId: string; total: number }>;
|
|
2224
|
+
`
|
|
2225
|
+
},
|
|
2226
|
+
{
|
|
2227
|
+
path: "src/subscribers/user-events.ts",
|
|
2228
|
+
content: `import { s } from '@geekmidas/constructs/subscribers';
|
|
2229
|
+
import type { AppEvents } from '../events/types.js';
|
|
2230
|
+
|
|
2231
|
+
export default s<AppEvents>()
|
|
2232
|
+
.events(['user.created', 'user.updated'])
|
|
2233
|
+
.handle(async ({ event, logger }) => {
|
|
2234
|
+
logger.info({ type: event.type, payload: event.payload }, 'Processing user event');
|
|
2235
|
+
|
|
2236
|
+
switch (event.type) {
|
|
2237
|
+
case 'user.created':
|
|
2238
|
+
// Handle user creation
|
|
2239
|
+
logger.info({ userId: event.payload.userId }, 'New user created');
|
|
2240
|
+
break;
|
|
2241
|
+
case 'user.updated':
|
|
2242
|
+
// Handle user update
|
|
2243
|
+
logger.info({ userId: event.payload.userId }, 'User updated');
|
|
2244
|
+
break;
|
|
2245
|
+
}
|
|
2246
|
+
});
|
|
2247
|
+
`
|
|
2248
|
+
},
|
|
2249
|
+
{
|
|
2250
|
+
path: "src/crons/cleanup.ts",
|
|
2251
|
+
content: `import { cron } from '@geekmidas/constructs/crons';
|
|
2252
|
+
|
|
2253
|
+
// Run every day at midnight
|
|
2254
|
+
export default cron('0 0 * * *')
|
|
2255
|
+
.handle(async ({ logger }) => {
|
|
2256
|
+
logger.info('Running cleanup job');
|
|
2257
|
+
|
|
2258
|
+
// Add your cleanup logic here
|
|
2259
|
+
// e.g., delete old sessions, clean up temp files, etc.
|
|
2260
|
+
|
|
2261
|
+
logger.info('Cleanup job completed');
|
|
2262
|
+
});
|
|
2263
|
+
`
|
|
2264
|
+
}
|
|
2265
|
+
];
|
|
2266
|
+
if (options.telescope) files.push({
|
|
2267
|
+
path: "src/config/telescope.ts",
|
|
2268
|
+
content: `import { Telescope } from '@geekmidas/telescope';
|
|
2269
|
+
import { InMemoryStorage } from '@geekmidas/telescope/storage/memory';
|
|
2270
|
+
|
|
2271
|
+
export const telescope = new Telescope({
|
|
2272
|
+
storage: new InMemoryStorage({ maxEntries: 100 }),
|
|
2273
|
+
enabled: process.env.NODE_ENV === 'development',
|
|
2274
|
+
});
|
|
2275
|
+
`
|
|
2276
|
+
});
|
|
2277
|
+
return files;
|
|
2278
|
+
}
|
|
2279
|
+
};
|
|
2280
|
+
|
|
2281
|
+
//#endregion
|
|
2282
|
+
//#region src/init/templates/index.ts
|
|
2283
|
+
/**
|
|
2284
|
+
* OpenAPI output path (fixed, not configurable)
|
|
2285
|
+
*/
|
|
2286
|
+
const OPENAPI_OUTPUT_PATH = "./.gkm/openapi.ts";
|
|
2287
|
+
/**
|
|
2288
|
+
* All available templates
|
|
2289
|
+
*/
|
|
2290
|
+
const templates = {
|
|
2291
|
+
minimal: minimalTemplate,
|
|
2292
|
+
api: apiTemplate,
|
|
2293
|
+
serverless: serverlessTemplate,
|
|
2294
|
+
worker: workerTemplate
|
|
2295
|
+
};
|
|
2296
|
+
/**
|
|
2297
|
+
* Template choices for prompts
|
|
2298
|
+
*/
|
|
2299
|
+
const templateChoices = [
|
|
2300
|
+
{
|
|
2301
|
+
title: "Minimal",
|
|
2302
|
+
value: "minimal",
|
|
2303
|
+
description: "Basic health endpoint"
|
|
2304
|
+
},
|
|
2305
|
+
{
|
|
2306
|
+
title: "API",
|
|
2307
|
+
value: "api",
|
|
2308
|
+
description: "Full API with auth, database, services"
|
|
2309
|
+
},
|
|
2310
|
+
{
|
|
2311
|
+
title: "Serverless",
|
|
2312
|
+
value: "serverless",
|
|
2313
|
+
description: "AWS Lambda handlers"
|
|
2314
|
+
},
|
|
2315
|
+
{
|
|
2316
|
+
title: "Worker",
|
|
2317
|
+
value: "worker",
|
|
2318
|
+
description: "Background job processing"
|
|
2319
|
+
}
|
|
2320
|
+
];
|
|
2321
|
+
/**
|
|
2322
|
+
* Logger type choices for prompts
|
|
2323
|
+
*/
|
|
2324
|
+
const loggerTypeChoices = [{
|
|
2325
|
+
title: "Pino",
|
|
2326
|
+
value: "pino",
|
|
2327
|
+
description: "Fast JSON logger for production (recommended)"
|
|
2328
|
+
}, {
|
|
2329
|
+
title: "Console",
|
|
2330
|
+
value: "console",
|
|
2331
|
+
description: "Simple console logger for development"
|
|
2332
|
+
}];
|
|
2333
|
+
/**
|
|
2334
|
+
* Routes structure choices for prompts
|
|
2335
|
+
*/
|
|
2336
|
+
const routesStructureChoices = [
|
|
2337
|
+
{
|
|
2338
|
+
title: "Centralized (endpoints)",
|
|
2339
|
+
value: "centralized-endpoints",
|
|
2340
|
+
description: "src/endpoints/**/*.ts"
|
|
2341
|
+
},
|
|
2342
|
+
{
|
|
2343
|
+
title: "Centralized (routes)",
|
|
2344
|
+
value: "centralized-routes",
|
|
2345
|
+
description: "src/routes/**/*.ts"
|
|
2346
|
+
},
|
|
2347
|
+
{
|
|
2348
|
+
title: "Domain-based",
|
|
2349
|
+
value: "domain-based",
|
|
2350
|
+
description: "src/**/routes/*.ts (e.g., src/users/routes/list.ts)"
|
|
2351
|
+
}
|
|
2352
|
+
];
|
|
2353
|
+
/**
|
|
2354
|
+
* Get a template by name
|
|
2355
|
+
*/
|
|
2356
|
+
function getTemplate(name$1) {
|
|
2357
|
+
const template = templates[name$1];
|
|
2358
|
+
if (!template) throw new Error(`Unknown template: ${name$1}`);
|
|
2359
|
+
return template;
|
|
2360
|
+
}
|
|
2361
|
+
|
|
2362
|
+
//#endregion
|
|
2363
|
+
//#region src/init/generators/package.ts
|
|
2364
|
+
/**
|
|
2365
|
+
* Generate package.json with dependencies based on template and options
|
|
2366
|
+
*/
|
|
2367
|
+
function generatePackageJson(options, template) {
|
|
2368
|
+
const { name: name$1, telescope, database, studio, monorepo } = options;
|
|
2369
|
+
const dependencies$1 = { ...template.dependencies };
|
|
2370
|
+
const devDependencies$1 = { ...template.devDependencies };
|
|
2371
|
+
const scripts$1 = { ...template.scripts };
|
|
2372
|
+
if (telescope) dependencies$1["@geekmidas/telescope"] = "workspace:*";
|
|
2373
|
+
if (studio) dependencies$1["@geekmidas/studio"] = "workspace:*";
|
|
2374
|
+
if (database) {
|
|
2375
|
+
dependencies$1["@geekmidas/db"] = "workspace:*";
|
|
2376
|
+
dependencies$1["kysely"] = "~0.28.2";
|
|
2377
|
+
dependencies$1["pg"] = "~8.16.0";
|
|
2378
|
+
devDependencies$1["@types/pg"] = "~8.15.0";
|
|
2379
|
+
}
|
|
2380
|
+
dependencies$1["zod"] = "~4.1.0";
|
|
2381
|
+
if (monorepo) {
|
|
2382
|
+
delete devDependencies$1["@biomejs/biome"];
|
|
2383
|
+
delete devDependencies$1["turbo"];
|
|
2384
|
+
delete scripts$1["lint"];
|
|
2385
|
+
delete scripts$1["fmt"];
|
|
2386
|
+
delete scripts$1["fmt:check"];
|
|
2387
|
+
dependencies$1[`@${name$1}/models`] = "workspace:*";
|
|
2388
|
+
delete dependencies$1["zod"];
|
|
2389
|
+
}
|
|
2390
|
+
const sortObject = (obj) => Object.fromEntries(Object.entries(obj).sort(([a], [b]) => a.localeCompare(b)));
|
|
2391
|
+
let packageName = name$1;
|
|
2392
|
+
if (monorepo && options.apiPath) {
|
|
2393
|
+
const pathParts = options.apiPath.split("/");
|
|
2394
|
+
const appName = pathParts[pathParts.length - 1] || "api";
|
|
2395
|
+
packageName = `@${name$1}/${appName}`;
|
|
2396
|
+
}
|
|
2397
|
+
const packageJson = {
|
|
2398
|
+
name: packageName,
|
|
2399
|
+
version: "0.0.1",
|
|
2400
|
+
private: true,
|
|
2401
|
+
type: "module",
|
|
2402
|
+
exports: { "./client": {
|
|
2403
|
+
types: OPENAPI_OUTPUT_PATH,
|
|
2404
|
+
import: OPENAPI_OUTPUT_PATH
|
|
2405
|
+
} },
|
|
2406
|
+
scripts: scripts$1,
|
|
2407
|
+
dependencies: sortObject(dependencies$1),
|
|
2408
|
+
devDependencies: sortObject(devDependencies$1)
|
|
2409
|
+
};
|
|
2410
|
+
return [{
|
|
2411
|
+
path: "package.json",
|
|
2412
|
+
content: JSON.stringify(packageJson, null, 2) + "\n"
|
|
2413
|
+
}];
|
|
2414
|
+
}
|
|
2415
|
+
|
|
2416
|
+
//#endregion
|
|
2417
|
+
//#region src/init/generators/source.ts
|
|
2418
|
+
/**
|
|
2419
|
+
* Generate source files from template
|
|
2420
|
+
*/
|
|
2421
|
+
function generateSourceFiles(options, template) {
|
|
2422
|
+
return template.files(options);
|
|
2423
|
+
}
|
|
2424
|
+
|
|
2425
|
+
//#endregion
|
|
2426
|
+
//#region src/init/utils.ts
|
|
2427
|
+
/**
|
|
2428
|
+
* Detect the package manager being used based on lockfiles or npm_config_user_agent
|
|
2429
|
+
*/
|
|
2430
|
+
function detectPackageManager(cwd = process.cwd()) {
|
|
2431
|
+
if ((0, node_fs.existsSync)((0, node_path.join)(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
2432
|
+
if ((0, node_fs.existsSync)((0, node_path.join)(cwd, "yarn.lock"))) return "yarn";
|
|
2433
|
+
if ((0, node_fs.existsSync)((0, node_path.join)(cwd, "bun.lockb"))) return "bun";
|
|
2434
|
+
if ((0, node_fs.existsSync)((0, node_path.join)(cwd, "package-lock.json"))) return "npm";
|
|
2435
|
+
const userAgent = process.env.npm_config_user_agent || "";
|
|
2436
|
+
if (userAgent.includes("pnpm")) return "pnpm";
|
|
2437
|
+
if (userAgent.includes("yarn")) return "yarn";
|
|
2438
|
+
if (userAgent.includes("bun")) return "bun";
|
|
2439
|
+
return "npm";
|
|
2440
|
+
}
|
|
2441
|
+
/**
|
|
2442
|
+
* Validate project name for npm package naming conventions
|
|
2443
|
+
*/
|
|
2444
|
+
function validateProjectName(name$1) {
|
|
2445
|
+
if (!name$1) return "Project name is required";
|
|
2446
|
+
if (!/^[a-z0-9-_@/.]+$/i.test(name$1)) return "Project name can only contain letters, numbers, hyphens, underscores, @, /, and .";
|
|
2447
|
+
const reserved = [
|
|
2448
|
+
"node_modules",
|
|
2449
|
+
".git",
|
|
2450
|
+
"package.json",
|
|
2451
|
+
"src"
|
|
2452
|
+
];
|
|
2453
|
+
if (reserved.includes(name$1.toLowerCase())) return `"${name$1}" is a reserved name`;
|
|
2454
|
+
return true;
|
|
2455
|
+
}
|
|
2456
|
+
/**
|
|
2457
|
+
* Check if a directory already exists at the target path
|
|
2458
|
+
*/
|
|
2459
|
+
function checkDirectoryExists(name$1, cwd = process.cwd()) {
|
|
2460
|
+
const targetPath = (0, node_path.join)(cwd, name$1);
|
|
2461
|
+
if ((0, node_fs.existsSync)(targetPath)) return `Directory "${name$1}" already exists`;
|
|
2462
|
+
return true;
|
|
2463
|
+
}
|
|
2464
|
+
/**
|
|
2465
|
+
* Get the install command for a package manager
|
|
2466
|
+
*/
|
|
2467
|
+
function getInstallCommand(pkgManager) {
|
|
2468
|
+
switch (pkgManager) {
|
|
2469
|
+
case "pnpm": return "pnpm install";
|
|
2470
|
+
case "yarn": return "yarn";
|
|
2471
|
+
case "bun": return "bun install";
|
|
2472
|
+
case "npm":
|
|
2473
|
+
default: return "npm install";
|
|
2474
|
+
}
|
|
2475
|
+
}
|
|
2476
|
+
/**
|
|
2477
|
+
* Get the dev command for a package manager
|
|
2478
|
+
*/
|
|
2479
|
+
function getRunCommand(pkgManager, script) {
|
|
2480
|
+
switch (pkgManager) {
|
|
2481
|
+
case "pnpm": return `pnpm ${script}`;
|
|
2482
|
+
case "yarn": return `yarn ${script}`;
|
|
2483
|
+
case "bun": return `bun run ${script}`;
|
|
2484
|
+
case "npm":
|
|
2485
|
+
default: return `npm run ${script}`;
|
|
2486
|
+
}
|
|
2487
|
+
}
|
|
2488
|
+
|
|
2489
|
+
//#endregion
|
|
2490
|
+
//#region src/init/index.ts
|
|
2491
|
+
/**
|
|
2492
|
+
* Main init command - scaffolds a new project
|
|
2493
|
+
*/
|
|
2494
|
+
async function initCommand(projectName, options = {}) {
|
|
2495
|
+
const cwd = process.cwd();
|
|
2496
|
+
const pkgManager = detectPackageManager(cwd);
|
|
2497
|
+
prompts.default.override({});
|
|
2498
|
+
const onCancel = () => {
|
|
2499
|
+
process.exit(0);
|
|
2500
|
+
};
|
|
2501
|
+
const answers = await (0, prompts.default)([
|
|
2502
|
+
{
|
|
2503
|
+
type: projectName ? null : "text",
|
|
2504
|
+
name: "name",
|
|
2505
|
+
message: "Project name:",
|
|
2506
|
+
initial: "my-api",
|
|
2507
|
+
validate: (value) => {
|
|
2508
|
+
const nameValid = validateProjectName(value);
|
|
2509
|
+
if (nameValid !== true) return nameValid;
|
|
2510
|
+
const dirValid = checkDirectoryExists(value, cwd);
|
|
2511
|
+
if (dirValid !== true) return dirValid;
|
|
2512
|
+
return true;
|
|
2513
|
+
}
|
|
2514
|
+
},
|
|
2515
|
+
{
|
|
2516
|
+
type: options.template || options.yes ? null : "select",
|
|
2517
|
+
name: "template",
|
|
2518
|
+
message: "Template:",
|
|
2519
|
+
choices: templateChoices,
|
|
2520
|
+
initial: 0
|
|
2521
|
+
},
|
|
2522
|
+
{
|
|
2523
|
+
type: options.yes ? null : "confirm",
|
|
2524
|
+
name: "telescope",
|
|
2525
|
+
message: "Include Telescope (debugging dashboard)?",
|
|
2526
|
+
initial: true
|
|
2527
|
+
},
|
|
2528
|
+
{
|
|
2529
|
+
type: options.yes ? null : "confirm",
|
|
2530
|
+
name: "database",
|
|
2531
|
+
message: "Include database support (Kysely)?",
|
|
2532
|
+
initial: true
|
|
2533
|
+
},
|
|
2534
|
+
{
|
|
2535
|
+
type: (prev) => options.yes ? null : prev ? "confirm" : null,
|
|
2536
|
+
name: "studio",
|
|
2537
|
+
message: "Include Studio (database browser)?",
|
|
2538
|
+
initial: true
|
|
2539
|
+
},
|
|
2540
|
+
{
|
|
2541
|
+
type: options.yes ? null : "select",
|
|
2542
|
+
name: "loggerType",
|
|
2543
|
+
message: "Logger:",
|
|
2544
|
+
choices: loggerTypeChoices,
|
|
2545
|
+
initial: 0
|
|
2546
|
+
},
|
|
2547
|
+
{
|
|
2548
|
+
type: options.yes ? null : "select",
|
|
2549
|
+
name: "routesStructure",
|
|
2550
|
+
message: "Routes structure:",
|
|
2551
|
+
choices: routesStructureChoices,
|
|
2552
|
+
initial: 0
|
|
2553
|
+
},
|
|
2554
|
+
{
|
|
2555
|
+
type: options.yes || options.monorepo !== void 0 ? null : "confirm",
|
|
2556
|
+
name: "monorepo",
|
|
2557
|
+
message: "Setup as monorepo?",
|
|
2558
|
+
initial: false
|
|
2559
|
+
},
|
|
2560
|
+
{
|
|
2561
|
+
type: (prev) => (prev === true || options.monorepo) && !options.apiPath ? "text" : null,
|
|
2562
|
+
name: "apiPath",
|
|
2563
|
+
message: "API app path:",
|
|
2564
|
+
initial: "apps/api"
|
|
2565
|
+
}
|
|
2566
|
+
], { onCancel });
|
|
2567
|
+
const name$1 = projectName || answers.name;
|
|
2568
|
+
if (!name$1) {
|
|
2569
|
+
console.error(" Error: Project name is required\n");
|
|
2570
|
+
process.exit(1);
|
|
2571
|
+
}
|
|
2572
|
+
if (projectName) {
|
|
2573
|
+
const nameValid = validateProjectName(projectName);
|
|
2574
|
+
if (nameValid !== true) {
|
|
2575
|
+
console.error(` Error: ${nameValid}\n`);
|
|
2576
|
+
process.exit(1);
|
|
2577
|
+
}
|
|
2578
|
+
const dirValid = checkDirectoryExists(projectName, cwd);
|
|
2579
|
+
if (dirValid !== true) {
|
|
2580
|
+
console.error(` Error: ${dirValid}\n`);
|
|
2581
|
+
process.exit(1);
|
|
2582
|
+
}
|
|
2583
|
+
}
|
|
2584
|
+
const monorepo = options.monorepo ?? (options.yes ? false : answers.monorepo ?? false);
|
|
2585
|
+
const database = options.yes ? true : answers.database ?? true;
|
|
2586
|
+
const templateOptions = {
|
|
2587
|
+
name: name$1,
|
|
2588
|
+
template: options.template || answers.template || "minimal",
|
|
2589
|
+
telescope: options.yes ? true : answers.telescope ?? true,
|
|
2590
|
+
database,
|
|
2591
|
+
studio: database && (options.yes ? true : answers.studio ?? true),
|
|
2592
|
+
loggerType: options.yes ? "pino" : answers.loggerType ?? "pino",
|
|
2593
|
+
routesStructure: options.yes ? "centralized-endpoints" : answers.routesStructure ?? "centralized-endpoints",
|
|
2594
|
+
monorepo,
|
|
2595
|
+
apiPath: monorepo ? options.apiPath ?? answers.apiPath ?? "apps/api" : ""
|
|
2596
|
+
};
|
|
2597
|
+
const targetDir = (0, node_path.join)(cwd, name$1);
|
|
2598
|
+
const template = getTemplate(templateOptions.template);
|
|
2599
|
+
const isMonorepo = templateOptions.monorepo;
|
|
2600
|
+
const apiPath = templateOptions.apiPath;
|
|
2601
|
+
await (0, node_fs_promises.mkdir)(targetDir, { recursive: true });
|
|
2602
|
+
const appDir = isMonorepo ? (0, node_path.join)(targetDir, apiPath) : targetDir;
|
|
2603
|
+
if (isMonorepo) await (0, node_fs_promises.mkdir)(appDir, { recursive: true });
|
|
2604
|
+
const appFiles = [
|
|
2605
|
+
...generatePackageJson(templateOptions, template),
|
|
2606
|
+
...generateConfigFiles(templateOptions, template),
|
|
2607
|
+
...generateEnvFiles(templateOptions, template),
|
|
2608
|
+
...generateSourceFiles(templateOptions, template),
|
|
2609
|
+
...generateDockerFiles(templateOptions, template)
|
|
2610
|
+
];
|
|
2611
|
+
const rootFiles = [...generateMonorepoFiles(templateOptions, template), ...generateModelsPackage(templateOptions)];
|
|
2612
|
+
for (const { path: path$1, content } of rootFiles) {
|
|
2613
|
+
const fullPath = (0, node_path.join)(targetDir, path$1);
|
|
2614
|
+
await (0, node_fs_promises.mkdir)((0, node_path.dirname)(fullPath), { recursive: true });
|
|
2615
|
+
await (0, node_fs_promises.writeFile)(fullPath, content);
|
|
2616
|
+
}
|
|
2617
|
+
for (const { path: path$1, content } of appFiles) {
|
|
2618
|
+
const fullPath = (0, node_path.join)(appDir, path$1);
|
|
2619
|
+
const displayPath = isMonorepo ? `${apiPath}/${path$1}` : path$1;
|
|
2620
|
+
await (0, node_fs_promises.mkdir)((0, node_path.dirname)(fullPath), { recursive: true });
|
|
2621
|
+
await (0, node_fs_promises.writeFile)(fullPath, content);
|
|
2622
|
+
}
|
|
2623
|
+
if (!options.skipInstall) {
|
|
2624
|
+
try {
|
|
2625
|
+
(0, node_child_process.execSync)(getInstallCommand(pkgManager), {
|
|
2626
|
+
cwd: targetDir,
|
|
2627
|
+
stdio: "inherit"
|
|
2628
|
+
});
|
|
2629
|
+
} catch {
|
|
2630
|
+
console.error("\n Warning: Failed to install dependencies.");
|
|
2631
|
+
}
|
|
2632
|
+
try {
|
|
2633
|
+
(0, node_child_process.execSync)("npx @biomejs/biome format --write --unsafe .", {
|
|
2634
|
+
cwd: targetDir,
|
|
2635
|
+
stdio: "inherit"
|
|
2636
|
+
});
|
|
2637
|
+
} catch {}
|
|
2638
|
+
}
|
|
2639
|
+
const devCommand$1 = getRunCommand(pkgManager, "dev");
|
|
2640
|
+
}
|
|
2641
|
+
|
|
115
2642
|
//#endregion
|
|
116
2643
|
//#region src/index.ts
|
|
117
2644
|
const program = new commander.Command();
|
|
@@ -120,7 +2647,7 @@ program.command("init").description("Scaffold a new project").argument("[name]",
|
|
|
120
2647
|
try {
|
|
121
2648
|
const globalOptions = program.opts();
|
|
122
2649
|
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|
|
123
|
-
await
|
|
2650
|
+
await initCommand(name$1, options);
|
|
124
2651
|
} catch (error) {
|
|
125
2652
|
console.error("Init failed:", error.message);
|
|
126
2653
|
process.exit(1);
|
|
@@ -135,18 +2662,18 @@ program.command("build").description("Build handlers from endpoints, functions,
|
|
|
135
2662
|
console.error(`Invalid provider: ${options.provider}. Must be 'aws' or 'server'.`);
|
|
136
2663
|
process.exit(1);
|
|
137
2664
|
}
|
|
138
|
-
await
|
|
2665
|
+
await buildCommand({
|
|
139
2666
|
provider: options.provider,
|
|
140
2667
|
enableOpenApi: options.enableOpenapi || false
|
|
141
2668
|
});
|
|
142
2669
|
} else if (options.providers) {
|
|
143
2670
|
console.warn("⚠️ --providers flag is deprecated. Use --provider instead.");
|
|
144
2671
|
const providerList = [...new Set(options.providers.split(",").map((p) => p.trim()))];
|
|
145
|
-
await
|
|
2672
|
+
await buildCommand({
|
|
146
2673
|
providers: providerList,
|
|
147
2674
|
enableOpenApi: options.enableOpenapi || false
|
|
148
2675
|
});
|
|
149
|
-
} else await
|
|
2676
|
+
} else await buildCommand({ enableOpenApi: options.enableOpenapi || false });
|
|
150
2677
|
} catch (error) {
|
|
151
2678
|
console.error("Build failed:", error.message);
|
|
152
2679
|
process.exit(1);
|
|
@@ -156,7 +2683,7 @@ program.command("dev").description("Start development server with automatic relo
|
|
|
156
2683
|
try {
|
|
157
2684
|
const globalOptions = program.opts();
|
|
158
2685
|
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|
|
159
|
-
await
|
|
2686
|
+
await devCommand({
|
|
160
2687
|
port: options.port ? Number.parseInt(options.port) : 3e3,
|
|
161
2688
|
enableOpenApi: options.enableOpenapi ?? true
|
|
162
2689
|
});
|