@geekmidas/cli 0.2.4 → 0.3.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.
@@ -0,0 +1,287 @@
1
+ # Manifest Refactoring Design
2
+
3
+ ## Current State
4
+
5
+ Currently, the CLI generates a single `manifest.json` file at `.gkm/manifest.json` that aggregates all routes, functions, crons, and subscribers across all providers.
6
+
7
+ ```
8
+ .gkm/
9
+ ├── manifest.json # Single JSON manifest
10
+ ├── aws-apigatewayv2/ # AWS handlers
11
+ │ ├── getUsers.ts
12
+ │ └── createUser.ts
13
+ └── server/ # Server handlers
14
+ ├── app.ts
15
+ └── endpoints.ts
16
+ ```
17
+
18
+ ### Problems
19
+
20
+ 1. **Mixed provider data**: AWS and server routes are combined, causing `method: 'ALL'` to appear in manifests used for AWS infrastructure
21
+ 2. **JSON format**: No TypeScript types available, consumers must define their own types
22
+ 3. **No derived types**: Consumers can't easily extract types like `Authorizer` from the manifest
23
+
24
+ ## Proposed Changes
25
+
26
+ ### 1. Folder Structure
27
+
28
+ Change from single `manifest.json` to a `manifest/` folder with TypeScript files per provider:
29
+
30
+ ```
31
+ .gkm/
32
+ ├── manifest/
33
+ │ ├── aws.ts # AWS-specific manifest
34
+ │ └── server.ts # Server-specific manifest
35
+ ├── aws-apigatewayv2/
36
+ │ ├── getUsers.ts
37
+ │ └── createUser.ts
38
+ └── server/
39
+ ├── app.ts
40
+ └── endpoints.ts
41
+ ```
42
+
43
+ ### 2. AWS Manifest (`manifest/aws.ts`)
44
+
45
+ Contains only AWS routes with actual HTTP methods (no `method: 'ALL'`):
46
+
47
+ ```typescript
48
+ export const manifest = {
49
+ routes: [
50
+ {
51
+ path: '/users',
52
+ method: 'GET',
53
+ handler: '.gkm/aws-apigatewayv2/getUsers.handler',
54
+ timeout: 30,
55
+ memorySize: 256,
56
+ environment: ['DATABASE_URL', 'API_KEY'],
57
+ authorizer: 'jwt',
58
+ },
59
+ {
60
+ path: '/users',
61
+ method: 'POST',
62
+ handler: '.gkm/aws-apigatewayv2/createUser.handler',
63
+ timeout: 30,
64
+ memorySize: 256,
65
+ environment: ['DATABASE_URL'],
66
+ authorizer: 'jwt',
67
+ },
68
+ ],
69
+ functions: [
70
+ {
71
+ name: 'processPayment',
72
+ handler: '.gkm/aws-lambda/processPayment.handler',
73
+ timeout: 60,
74
+ memorySize: 512,
75
+ environment: ['STRIPE_KEY'],
76
+ },
77
+ ],
78
+ crons: [
79
+ {
80
+ name: 'dailyReport',
81
+ handler: '.gkm/aws-lambda/dailyReport.handler',
82
+ schedule: 'rate(1 day)',
83
+ timeout: 300,
84
+ memorySize: 256,
85
+ environment: [],
86
+ },
87
+ ],
88
+ subscribers: [
89
+ {
90
+ name: 'orderCreated',
91
+ handler: '.gkm/aws-lambda/orderCreated.handler',
92
+ subscribedEvents: ['order.created'],
93
+ timeout: 30,
94
+ memorySize: 256,
95
+ environment: [],
96
+ },
97
+ ],
98
+ } as const;
99
+
100
+ // Derived types
101
+ export type Route = (typeof manifest.routes)[number];
102
+ export type Function = (typeof manifest.functions)[number];
103
+ export type Cron = (typeof manifest.crons)[number];
104
+ export type Subscriber = (typeof manifest.subscribers)[number];
105
+
106
+ // Useful union types
107
+ export type Authorizer = Route['authorizer'];
108
+ export type HttpMethod = Route['method'];
109
+ export type RoutePath = Route['path'];
110
+ ```
111
+
112
+ ### 3. Server Manifest (`manifest/server.ts`)
113
+
114
+ Contains server app info and route metadata for documentation/type derivation:
115
+
116
+ ```typescript
117
+ export const manifest = {
118
+ app: {
119
+ handler: '.gkm/server/app.ts',
120
+ endpoints: '.gkm/server/endpoints.ts',
121
+ },
122
+ routes: [
123
+ {
124
+ path: '/users',
125
+ method: 'GET',
126
+ authorizer: 'jwt',
127
+ },
128
+ {
129
+ path: '/users',
130
+ method: 'POST',
131
+ authorizer: 'jwt',
132
+ },
133
+ ],
134
+ subscribers: [
135
+ {
136
+ name: 'orderCreated',
137
+ subscribedEvents: ['order.created'],
138
+ },
139
+ ],
140
+ } as const;
141
+
142
+ // Derived types
143
+ export type Route = (typeof manifest.routes)[number];
144
+ export type Subscriber = (typeof manifest.subscribers)[number];
145
+
146
+ // Useful union types
147
+ export type Authorizer = Route['authorizer'];
148
+ export type HttpMethod = Route['method'];
149
+ export type RoutePath = Route['path'];
150
+ ```
151
+
152
+ ## Implementation Details
153
+
154
+ ### Changes to `manifests.ts`
155
+
156
+ ```typescript
157
+ export async function generateManifests(
158
+ outputDir: string,
159
+ provider: 'aws' | 'server',
160
+ routes: RouteInfo[],
161
+ functions: FunctionInfo[],
162
+ crons: CronInfo[],
163
+ subscribers: SubscriberInfo[],
164
+ ): Promise<void> {
165
+ const manifestDir = join(outputDir, 'manifest');
166
+ await mkdir(manifestDir, { recursive: true });
167
+
168
+ if (provider === 'aws') {
169
+ await generateAwsManifest(manifestDir, routes, functions, crons, subscribers);
170
+ } else {
171
+ await generateServerManifest(manifestDir, routes, subscribers);
172
+ }
173
+ }
174
+
175
+ async function generateAwsManifest(
176
+ manifestDir: string,
177
+ routes: RouteInfo[],
178
+ functions: FunctionInfo[],
179
+ crons: CronInfo[],
180
+ subscribers: SubscriberInfo[],
181
+ ): Promise<void> {
182
+ // Filter out 'ALL' method routes (server-specific)
183
+ const awsRoutes = routes.filter(r => r.method !== 'ALL');
184
+
185
+ const content = `export const manifest = {
186
+ routes: ${JSON.stringify(awsRoutes, null, 2)},
187
+ functions: ${JSON.stringify(functions, null, 2)},
188
+ crons: ${JSON.stringify(crons, null, 2)},
189
+ subscribers: ${JSON.stringify(subscribers, null, 2)},
190
+ } as const;
191
+
192
+ // Derived types
193
+ export type Route = (typeof manifest.routes)[number];
194
+ export type Function = (typeof manifest.functions)[number];
195
+ export type Cron = (typeof manifest.crons)[number];
196
+ export type Subscriber = (typeof manifest.subscribers)[number];
197
+
198
+ // Useful union types
199
+ export type Authorizer = Route['authorizer'];
200
+ export type HttpMethod = Route['method'];
201
+ export type RoutePath = Route['path'];
202
+ `;
203
+
204
+ await writeFile(join(manifestDir, 'aws.ts'), content);
205
+ }
206
+ ```
207
+
208
+ ### Changes to Build Flow
209
+
210
+ Currently `buildCommand` aggregates all providers into one manifest. Change to:
211
+
212
+ 1. Generate per-provider manifests during each provider's build
213
+ 2. Remove aggregation step
214
+ 3. Each provider generates its own TypeScript manifest file
215
+
216
+ ```typescript
217
+ async function buildForProvider(
218
+ provider: LegacyProvider,
219
+ // ...
220
+ ): Promise<BuildResult> {
221
+ // ... existing build logic ...
222
+
223
+ // Generate provider-specific manifest
224
+ const manifestProvider = provider.startsWith('aws') ? 'aws' : 'server';
225
+ await generateManifests(
226
+ rootOutputDir,
227
+ manifestProvider,
228
+ routes,
229
+ functionInfos,
230
+ cronInfos,
231
+ subscriberInfos,
232
+ );
233
+
234
+ return { routes, functions: functionInfos, crons: cronInfos, subscribers: subscriberInfos };
235
+ }
236
+ ```
237
+
238
+ ## Usage Examples
239
+
240
+ ### AWS CDK / SST Integration
241
+
242
+ ```typescript
243
+ import { manifest, type Route, type Authorizer } from './.gkm/manifest/aws';
244
+
245
+ // Type-safe iteration over routes
246
+ for (const route of manifest.routes) {
247
+ new ApiGatewayRoute(this, route.path, {
248
+ method: route.method,
249
+ handler: route.handler,
250
+ authorizer: getAuthorizer(route.authorizer),
251
+ });
252
+ }
253
+
254
+ // Use derived types
255
+ function getAuthorizer(name: Authorizer): IAuthorizer {
256
+ switch (name) {
257
+ case 'jwt':
258
+ return jwtAuthorizer;
259
+ case 'apiKey':
260
+ return apiKeyAuthorizer;
261
+ case 'none':
262
+ return undefined;
263
+ }
264
+ }
265
+ ```
266
+
267
+ ### Type Checking Authorizer Values
268
+
269
+ ```typescript
270
+ import type { Authorizer } from './.gkm/manifest/aws';
271
+
272
+ // Authorizer is a union type of all authorizer values
273
+ // e.g., 'jwt' | 'apiKey' | 'none'
274
+ const authorizer: Authorizer = 'jwt'; // ✓
275
+ const invalid: Authorizer = 'invalid'; // ✗ Type error
276
+ ```
277
+
278
+ ## Migration Notes
279
+
280
+ 1. **Breaking change**: Consumers using `manifest.json` need to switch to TypeScript imports
281
+ 2. **Benefit**: Full TypeScript support with derived types
282
+
283
+ ## Design Decisions
284
+
285
+ 1. **No backward compatibility**: `manifest.json` will not be generated
286
+ 2. **Server manifest includes route metadata**: For documentation and type derivation
287
+ 3. **No re-export index**: Each manifest is imported directly
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geekmidas/cli",
3
- "version": "0.2.4",
3
+ "version": "0.3.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "exports": {
@@ -44,7 +44,7 @@
44
44
  "@geekmidas/testkit": "0.0.17"
45
45
  },
46
46
  "peerDependencies": {
47
- "@geekmidas/constructs": "~0.0.21",
47
+ "@geekmidas/constructs": "~0.0.22",
48
48
  "@geekmidas/envkit": "~0.0.8",
49
49
  "@geekmidas/logger": "~0.0.1",
50
50
  "@geekmidas/schema": "~0.0.2"
@@ -82,6 +82,20 @@ export default {
82
82
  'utf-8',
83
83
  );
84
84
  expect(endpointsContent).toContain('HonoEndpoint');
85
+
86
+ // Verify server manifest was created at .gkm/manifest/server.ts
87
+ const manifestPath = join(dir, '.gkm', 'manifest', 'server.ts');
88
+ const manifestContent = await readFile(manifestPath, 'utf-8');
89
+
90
+ // Verify manifest structure
91
+ expect(manifestContent).toContain('export const manifest = {');
92
+ expect(manifestContent).toContain('} as const;');
93
+ expect(manifestContent).toContain('app:');
94
+ expect(manifestContent).toContain('routes:');
95
+
96
+ // Verify derived types are exported
97
+ expect(manifestContent).toContain('export type Route =');
98
+ expect(manifestContent).toContain('export type Authorizer =');
85
99
  } finally {
86
100
  process.chdir(originalCwd);
87
101
  }
@@ -202,78 +216,35 @@ export default {
202
216
  ),
203
217
  ).toContain('AmazonApiGatewayV2Endpoint');
204
218
 
205
- // Verify unified manifest was created at root .gkm directory
206
- const manifestPath = join(dir, '.gkm', 'manifest.json');
207
- const manifest = JSON.parse(await readFile(manifestPath, 'utf-8'));
208
-
209
- // Verify manifest structure includes all routes from both providers
210
- expect(manifest).toMatchObject({
211
- routes: expect.arrayContaining([
212
- // Routes from aws-lambda provider
213
- expect.objectContaining({
214
- path: '/users',
215
- method: 'GET',
216
- handler: expect.stringContaining(
217
- 'routes/getUsersEndpoint.handler',
218
- ),
219
- }),
220
- expect.objectContaining({
221
- path: '/posts',
222
- method: 'POST',
223
- handler: expect.stringContaining(
224
- 'routes/getPostsEndpoint.handler',
225
- ),
226
- }),
227
- // Routes from aws-apigatewayv2 provider
228
- expect.objectContaining({
229
- path: '/users',
230
- method: 'GET',
231
- handler: expect.stringContaining('getUsersEndpoint.handler'),
232
- }),
233
- expect.objectContaining({
234
- path: '/posts',
235
- method: 'POST',
236
- handler: expect.stringContaining('getPostsEndpoint.handler'),
237
- }),
238
- ]),
239
- functions: expect.arrayContaining([
240
- expect.objectContaining({
241
- name: 'processDataFunction',
242
- handler: expect.stringContaining(
243
- 'functions/processDataFunction.handler',
244
- ),
245
- timeout: 300,
246
- }),
247
- expect.objectContaining({
248
- name: 'sendEmailFunction',
249
- handler: expect.stringContaining(
250
- 'functions/sendEmailFunction.handler',
251
- ),
252
- timeout: 30,
253
- }),
254
- ]),
255
- crons: expect.arrayContaining([
256
- expect.objectContaining({
257
- name: 'dailyCleanupCron',
258
- handler: expect.stringContaining(
259
- 'crons/dailyCleanupCron.handler',
260
- ),
261
- schedule: 'rate(1 day)',
262
- }),
263
- expect.objectContaining({
264
- name: 'hourlyReportCron',
265
- handler: expect.stringContaining(
266
- 'crons/hourlyReportCron.handler',
267
- ),
268
- schedule: 'cron(0 * * * ? *)',
269
- }),
270
- ]),
271
- });
272
-
273
- // Verify counts - should have duplicated routes (one for each provider)
274
- expect(manifest.routes).toHaveLength(4); // 2 routes x 2 providers
275
- expect(manifest.functions).toHaveLength(2);
276
- expect(manifest.crons).toHaveLength(2);
219
+ // Verify AWS manifest was created at .gkm/manifest/aws.ts
220
+ const manifestPath = join(dir, '.gkm', 'manifest', 'aws.ts');
221
+ const manifestContent = await readFile(manifestPath, 'utf-8');
222
+
223
+ // Verify manifest is TypeScript with as const
224
+ expect(manifestContent).toContain('export const manifest = {');
225
+ expect(manifestContent).toContain('} as const;');
226
+
227
+ // Verify derived types are exported
228
+ expect(manifestContent).toContain('export type Route =');
229
+ expect(manifestContent).toContain('export type Function =');
230
+ expect(manifestContent).toContain('export type Cron =');
231
+ expect(manifestContent).toContain('export type Authorizer =');
232
+
233
+ // Verify routes are included (JSON.stringify output uses quoted keys/values)
234
+ expect(manifestContent).toContain('/users');
235
+ expect(manifestContent).toContain('/posts');
236
+ expect(manifestContent).toContain('GET');
237
+ expect(manifestContent).toContain('POST');
238
+
239
+ // Verify functions are included
240
+ expect(manifestContent).toContain('processDataFunction');
241
+ expect(manifestContent).toContain('sendEmailFunction');
242
+
243
+ // Verify crons are included
244
+ expect(manifestContent).toContain('dailyCleanupCron');
245
+ expect(manifestContent).toContain('hourlyReportCron');
246
+ expect(manifestContent).toContain('rate(1 day)');
247
+ expect(manifestContent).toContain('cron(0 * * * ? *)');
277
248
  } finally {
278
249
  process.chdir(originalCwd);
279
250
  }