@geekmidas/cli 1.9.1 → 1.10.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.
Files changed (59) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +42 -6
  3. package/dist/{config-6JHOwLCx.cjs → config-D3ORuiUs.cjs} +2 -2
  4. package/dist/{config-6JHOwLCx.cjs.map → config-D3ORuiUs.cjs.map} +1 -1
  5. package/dist/{config-DxASSNjr.mjs → config-jsRYHOHU.mjs} +2 -2
  6. package/dist/{config-DxASSNjr.mjs.map → config-jsRYHOHU.mjs.map} +1 -1
  7. package/dist/config.cjs +2 -2
  8. package/dist/config.d.cts +2 -2
  9. package/dist/config.d.mts +2 -2
  10. package/dist/config.mjs +2 -2
  11. package/dist/{index-Bt2kX0-R.d.mts → index-3n-giNaw.d.mts} +18 -6
  12. package/dist/index-3n-giNaw.d.mts.map +1 -0
  13. package/dist/{index-Cyk2rTyj.d.cts → index-CiEOtKEX.d.cts} +18 -6
  14. package/dist/index-CiEOtKEX.d.cts.map +1 -0
  15. package/dist/index.cjs +182 -158
  16. package/dist/index.cjs.map +1 -1
  17. package/dist/index.mjs +179 -155
  18. package/dist/index.mjs.map +1 -1
  19. package/dist/{openapi-CnvwSRDU.cjs → openapi-BYxAWwok.cjs} +178 -32
  20. package/dist/openapi-BYxAWwok.cjs.map +1 -0
  21. package/dist/{openapi-BYlyAbH3.mjs → openapi-DenF-okj.mjs} +148 -32
  22. package/dist/openapi-DenF-okj.mjs.map +1 -0
  23. package/dist/openapi.cjs +3 -3
  24. package/dist/openapi.d.cts +1 -1
  25. package/dist/openapi.d.cts.map +1 -1
  26. package/dist/openapi.d.mts +1 -1
  27. package/dist/openapi.d.mts.map +1 -1
  28. package/dist/openapi.mjs +3 -3
  29. package/dist/{types-l53qUmGt.d.cts → types-C7QJJl9f.d.cts} +6 -2
  30. package/dist/types-C7QJJl9f.d.cts.map +1 -0
  31. package/dist/{types-wXMIMOyK.d.mts → types-Iqsq_FIG.d.mts} +6 -2
  32. package/dist/types-Iqsq_FIG.d.mts.map +1 -0
  33. package/dist/workspace/index.cjs +1 -1
  34. package/dist/workspace/index.d.cts +2 -2
  35. package/dist/workspace/index.d.mts +2 -2
  36. package/dist/workspace/index.mjs +1 -1
  37. package/dist/{workspace-D2ocAlpl.cjs → workspace-4SP3Gx4Y.cjs} +11 -3
  38. package/dist/{workspace-D2ocAlpl.cjs.map → workspace-4SP3Gx4Y.cjs.map} +1 -1
  39. package/dist/{workspace-9IQIjwkQ.mjs → workspace-D4z4A4cq.mjs} +11 -3
  40. package/dist/{workspace-9IQIjwkQ.mjs.map → workspace-D4z4A4cq.mjs.map} +1 -1
  41. package/package.json +4 -4
  42. package/src/build/__tests__/manifests.spec.ts +171 -0
  43. package/src/build/__tests__/partitions.spec.ts +110 -0
  44. package/src/build/index.ts +58 -15
  45. package/src/build/manifests.ts +153 -32
  46. package/src/build/partitions.ts +58 -0
  47. package/src/deploy/sniffer.ts +6 -1
  48. package/src/generators/Generator.ts +27 -7
  49. package/src/generators/OpenApiTsGenerator.ts +4 -4
  50. package/src/openapi.ts +2 -1
  51. package/src/types.ts +17 -1
  52. package/src/workspace/client-generator.ts +6 -3
  53. package/src/workspace/schema.ts +13 -3
  54. package/dist/index-Bt2kX0-R.d.mts.map +0 -1
  55. package/dist/index-Cyk2rTyj.d.cts.map +0 -1
  56. package/dist/openapi-BYlyAbH3.mjs.map +0 -1
  57. package/dist/openapi-CnvwSRDU.cjs.map +0 -1
  58. package/dist/types-l53qUmGt.d.cts.map +0 -1
  59. package/dist/types-wXMIMOyK.d.mts.map +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geekmidas/cli",
3
- "version": "1.9.1",
3
+ "version": "1.10.0",
4
4
  "description": "CLI tools for building Lambda handlers, server applications, and generating OpenAPI specs",
5
5
  "private": false,
6
6
  "type": "module",
@@ -56,11 +56,11 @@
56
56
  "prompts": "~2.4.2",
57
57
  "tsx": "~4.20.3",
58
58
  "yaml": "~2.8.2",
59
- "@geekmidas/constructs": "~2.0.0",
59
+ "@geekmidas/constructs": "~3.0.0",
60
60
  "@geekmidas/envkit": "~1.0.3",
61
+ "@geekmidas/errors": "~1.0.0",
61
62
  "@geekmidas/logger": "~1.0.0",
62
- "@geekmidas/schema": "~1.0.0",
63
- "@geekmidas/errors": "~1.0.0"
63
+ "@geekmidas/schema": "~1.0.0"
64
64
  },
65
65
  "devDependencies": {
66
66
  "@types/lodash.kebabcase": "^4.1.9",
@@ -8,6 +8,7 @@ import type {
8
8
  RouteInfo,
9
9
  SubscriberInfo,
10
10
  } from '../../types';
11
+ import type { ManifestField } from '../manifests';
11
12
  import { generateAwsManifest, generateServerManifest } from '../manifests';
12
13
 
13
14
  describe('generateAwsManifest', () => {
@@ -344,3 +345,173 @@ describe('generateServerManifest', () => {
344
345
  logSpy.mockRestore();
345
346
  });
346
347
  });
348
+
349
+ describe('generateAwsManifest (partitioned)', () => {
350
+ itWithDir(
351
+ 'should generate manifest with partitioned routes and flat functions',
352
+ async ({ dir }) => {
353
+ const routes: ManifestField<RouteInfo> = {
354
+ admin: [
355
+ {
356
+ path: '/admin/users',
357
+ method: 'GET',
358
+ handler: '.gkm/aws/adminGetUsers.handler',
359
+ authorizer: 'jwt',
360
+ },
361
+ ],
362
+ public: [
363
+ {
364
+ path: '/users',
365
+ method: 'GET',
366
+ handler: '.gkm/aws/getUsers.handler',
367
+ authorizer: 'none',
368
+ },
369
+ ],
370
+ };
371
+
372
+ const functions: FunctionInfo[] = [
373
+ {
374
+ name: 'processData',
375
+ handler: '.gkm/aws/processData.handler',
376
+ timeout: 300,
377
+ memorySize: 512,
378
+ environment: [],
379
+ },
380
+ ];
381
+
382
+ await generateAwsManifest(dir, routes, functions, [], []);
383
+
384
+ const manifestPath = join(dir, 'manifest', 'aws.ts');
385
+ const content = await readFile(manifestPath, 'utf-8');
386
+
387
+ // Routes should be partitioned (nested object)
388
+ expect(content).toContain('"admin":');
389
+ expect(content).toContain('"public":');
390
+ expect(content).toContain('/admin/users');
391
+ expect(content).toContain('/users');
392
+
393
+ // Functions should be flat (array)
394
+ expect(content).toContain('processData');
395
+
396
+ // Partitioned routes should have partition-aware derived types
397
+ expect(content).toContain('RoutePartition');
398
+ expect(content).toContain('keyof typeof manifest.routes');
399
+
400
+ // Flat functions should have standard derived types
401
+ expect(content).toContain(
402
+ 'export type Function = (typeof manifest.functions)[number]',
403
+ );
404
+ },
405
+ );
406
+
407
+ itWithDir(
408
+ 'should generate flat manifest when all fields are arrays',
409
+ async ({ dir }) => {
410
+ const routes: RouteInfo[] = [
411
+ {
412
+ path: '/users',
413
+ method: 'GET',
414
+ handler: '.gkm/aws/getUsers.handler',
415
+ authorizer: 'jwt',
416
+ },
417
+ ];
418
+
419
+ await generateAwsManifest(dir, routes, [], [], []);
420
+
421
+ const manifestPath = join(dir, 'manifest', 'aws.ts');
422
+ const content = await readFile(manifestPath, 'utf-8');
423
+
424
+ // Should have flat derived types (no partition types)
425
+ expect(content).toContain(
426
+ 'export type Route = (typeof manifest.routes)[number]',
427
+ );
428
+ expect(content).not.toContain('RoutePartition');
429
+ },
430
+ );
431
+
432
+ itWithDir(
433
+ 'should filter ALL method from partitioned routes',
434
+ async ({ dir }) => {
435
+ const routes: ManifestField<RouteInfo> = {
436
+ admin: [
437
+ {
438
+ path: '/admin/users',
439
+ method: 'GET',
440
+ handler: '.gkm/aws/adminGetUsers.handler',
441
+ authorizer: 'jwt',
442
+ },
443
+ {
444
+ path: '*',
445
+ method: 'ALL',
446
+ handler: '.gkm/server/app.ts',
447
+ authorizer: 'none',
448
+ },
449
+ ],
450
+ };
451
+
452
+ await generateAwsManifest(dir, routes, [], [], []);
453
+
454
+ const manifestPath = join(dir, 'manifest', 'aws.ts');
455
+ const content = await readFile(manifestPath, 'utf-8');
456
+
457
+ expect(content).toContain('/admin/users');
458
+ expect(content).not.toContain('"ALL"');
459
+ },
460
+ );
461
+ });
462
+
463
+ describe('generateServerManifest (partitioned)', () => {
464
+ itWithDir(
465
+ 'should generate manifest with partitioned routes',
466
+ async ({ dir }) => {
467
+ const appInfo = {
468
+ handler: '.gkm/server/app.ts',
469
+ endpoints: '.gkm/server/endpoints.ts',
470
+ };
471
+
472
+ const routes: ManifestField<RouteInfo> = {
473
+ admin: [
474
+ {
475
+ path: '/admin/users',
476
+ method: 'GET',
477
+ handler: '',
478
+ authorizer: 'jwt',
479
+ },
480
+ ],
481
+ default: [
482
+ {
483
+ path: '/health',
484
+ method: 'GET',
485
+ handler: '',
486
+ authorizer: 'none',
487
+ },
488
+ ],
489
+ };
490
+
491
+ const subscribers: SubscriberInfo[] = [
492
+ {
493
+ name: 'orderHandler',
494
+ handler: '.gkm/server/orderHandler.ts',
495
+ subscribedEvents: ['order.created'],
496
+ timeout: 30,
497
+ memorySize: 256,
498
+ environment: [],
499
+ },
500
+ ];
501
+
502
+ await generateServerManifest(dir, appInfo, routes, subscribers);
503
+
504
+ const manifestPath = join(dir, 'manifest', 'server.ts');
505
+ const content = await readFile(manifestPath, 'utf-8');
506
+
507
+ // Routes should be partitioned
508
+ expect(content).toContain('"admin":');
509
+ expect(content).toContain('"default":');
510
+ expect(content).toContain('RoutePartition');
511
+
512
+ // Subscribers should be flat
513
+ expect(content).toContain('orderHandler');
514
+ expect(content).not.toContain('SubscriberPartition');
515
+ },
516
+ );
517
+ });
@@ -0,0 +1,110 @@
1
+ import type { Construct } from '@geekmidas/constructs';
2
+ import { describe, expect, it } from 'vitest';
3
+ import type { GeneratedConstruct } from '../../generators/Generator';
4
+ import {
5
+ DEFAULT_PARTITION,
6
+ groupByPartition,
7
+ groupInfosByPartition,
8
+ hasPartitions,
9
+ } from '../partitions';
10
+
11
+ function makeConstruct(
12
+ name: string,
13
+ partition?: string,
14
+ ): GeneratedConstruct<Construct> {
15
+ return {
16
+ key: name,
17
+ name,
18
+ construct: {} as Construct,
19
+ path: { absolute: `/src/${name}.ts`, relative: `src/${name}.ts` },
20
+ partition,
21
+ };
22
+ }
23
+
24
+ describe('groupByPartition', () => {
25
+ it('should group all constructs into default when no partition set', () => {
26
+ const constructs = [makeConstruct('a'), makeConstruct('b')];
27
+ const groups = groupByPartition(constructs);
28
+
29
+ expect(groups.size).toBe(1);
30
+ expect(groups.get(DEFAULT_PARTITION)).toHaveLength(2);
31
+ });
32
+
33
+ it('should group constructs by partition name', () => {
34
+ const constructs = [
35
+ makeConstruct('a', 'admin'),
36
+ makeConstruct('b', 'public'),
37
+ makeConstruct('c', 'admin'),
38
+ ];
39
+ const groups = groupByPartition(constructs);
40
+
41
+ expect(groups.size).toBe(2);
42
+ expect(groups.get('admin')).toHaveLength(2);
43
+ expect(groups.get('public')).toHaveLength(1);
44
+ });
45
+
46
+ it('should handle mix of partitioned and un-partitioned', () => {
47
+ const constructs = [
48
+ makeConstruct('a', 'admin'),
49
+ makeConstruct('b'), // no partition → default
50
+ makeConstruct('c', 'admin'),
51
+ ];
52
+ const groups = groupByPartition(constructs);
53
+
54
+ expect(groups.size).toBe(2);
55
+ expect(groups.get('admin')).toHaveLength(2);
56
+ expect(groups.get(DEFAULT_PARTITION)).toHaveLength(1);
57
+ });
58
+
59
+ it('should return empty map for empty input', () => {
60
+ const groups = groupByPartition([]);
61
+ expect(groups.size).toBe(0);
62
+ });
63
+ });
64
+
65
+ describe('hasPartitions', () => {
66
+ it('should return false when no constructs have partitions', () => {
67
+ const constructs = [makeConstruct('a'), makeConstruct('b')];
68
+ expect(hasPartitions(constructs)).toBe(false);
69
+ });
70
+
71
+ it('should return true when any construct has a partition', () => {
72
+ const constructs = [makeConstruct('a', 'admin'), makeConstruct('b')];
73
+ expect(hasPartitions(constructs)).toBe(true);
74
+ });
75
+
76
+ it('should return false for empty array', () => {
77
+ expect(hasPartitions([])).toBe(false);
78
+ });
79
+ });
80
+
81
+ describe('groupInfosByPartition', () => {
82
+ it('should group infos matching construct partitions', () => {
83
+ const constructs = [
84
+ makeConstruct('a', 'admin'),
85
+ makeConstruct('b', 'public'),
86
+ makeConstruct('c', 'admin'),
87
+ ];
88
+ const infos = [
89
+ { name: 'a', handler: 'a.handler' },
90
+ { name: 'b', handler: 'b.handler' },
91
+ { name: 'c', handler: 'c.handler' },
92
+ ];
93
+
94
+ const grouped = groupInfosByPartition(infos, constructs);
95
+
96
+ expect(grouped.admin).toHaveLength(2);
97
+ expect(grouped.public).toHaveLength(1);
98
+ expect(grouped.admin![0]).toEqual({ name: 'a', handler: 'a.handler' });
99
+ expect(grouped.admin![1]).toEqual({ name: 'c', handler: 'c.handler' });
100
+ });
101
+
102
+ it('should use default partition for un-partitioned constructs', () => {
103
+ const constructs = [makeConstruct('a'), makeConstruct('b')];
104
+ const infos = [{ name: 'a' }, { name: 'b' }];
105
+
106
+ const grouped = groupInfosByPartition(infos, constructs);
107
+
108
+ expect(grouped[DEFAULT_PARTITION]).toHaveLength(2);
109
+ });
110
+ });
@@ -26,11 +26,13 @@ import {
26
26
  type GeneratedConstruct,
27
27
  SubscriberGenerator,
28
28
  } from '../generators';
29
- import type {
30
- BuildOptions,
31
- BuildResult,
32
- LegacyProvider,
33
- RouteInfo,
29
+ import {
30
+ type BuildOptions,
31
+ type BuildResult,
32
+ isPartitionedRoutes,
33
+ type LegacyProvider,
34
+ type RouteInfo,
35
+ type Routes,
34
36
  } from '../types';
35
37
  import {
36
38
  getAppBuildOrder,
@@ -40,8 +42,10 @@ import {
40
42
  import {
41
43
  generateAwsManifest,
42
44
  generateServerManifest,
45
+ type ManifestField,
43
46
  type ServerAppInfo,
44
47
  } from './manifests';
48
+ import { groupInfosByPartition, hasPartitions } from './partitions';
45
49
  import { resolveProviders } from './providerResolver';
46
50
  import type { BuildContext } from './types';
47
51
 
@@ -89,15 +93,15 @@ export async function buildCommand(
89
93
  }
90
94
 
91
95
  logger.log(`Building with providers: ${resolved.providers.join(', ')}`);
92
- logger.log(`Loading routes from: ${config.routes}`);
96
+ logger.log(`Loading routes from: ${formatRoutes(config.routes)}`);
93
97
  if (config.functions) {
94
- logger.log(`Loading functions from: ${config.functions}`);
98
+ logger.log(`Loading functions from: ${formatRoutes(config.functions)}`);
95
99
  }
96
100
  if (config.crons) {
97
- logger.log(`Loading crons from: ${config.crons}`);
101
+ logger.log(`Loading crons from: ${formatRoutes(config.crons)}`);
98
102
  }
99
103
  if (config.subscribers) {
100
- logger.log(`Loading subscribers from: ${config.subscribers}`);
104
+ logger.log(`Loading subscribers from: ${formatRoutes(config.subscribers)}`);
101
105
  }
102
106
  logger.log(`Using envParser: ${config.envParser}`);
103
107
 
@@ -258,6 +262,15 @@ async function buildForProvider(
258
262
  `Generated ${routes.length} routes, ${functionInfos.length} functions, ${cronInfos.length} crons, ${subscriberInfos.length} subscribers for ${provider}`,
259
263
  );
260
264
 
265
+ // Assemble manifest fields (flat or partitioned per construct type)
266
+ const manifestRoutes = assembleManifestField(routes, endpoints);
267
+ const manifestFunctions = assembleManifestField(functionInfos, functions);
268
+ const manifestCrons = assembleManifestField(cronInfos, crons);
269
+ const manifestSubscribers = assembleManifestField(
270
+ subscriberInfos,
271
+ subscribers,
272
+ );
273
+
261
274
  // Generate provider-specific manifest
262
275
  if (provider === 'server') {
263
276
  // For server, collect actual route metadata from endpoint constructs
@@ -270,6 +283,8 @@ async function buildForProvider(
270
283
  })),
271
284
  );
272
285
 
286
+ const serverRouteField = assembleManifestField(routeMetadata, endpoints);
287
+
273
288
  const appInfo: ServerAppInfo = {
274
289
  handler: relative(process.cwd(), join(outputDir, 'app.ts')),
275
290
  endpoints: relative(process.cwd(), join(outputDir, 'endpoints.ts')),
@@ -278,8 +293,8 @@ async function buildForProvider(
278
293
  await generateServerManifest(
279
294
  rootOutputDir,
280
295
  appInfo,
281
- routeMetadata,
282
- subscriberInfos,
296
+ serverRouteField,
297
+ manifestSubscribers,
283
298
  );
284
299
 
285
300
  // Bundle for production if enabled
@@ -324,10 +339,10 @@ async function buildForProvider(
324
339
  // For AWS providers, generate AWS manifest
325
340
  await generateAwsManifest(
326
341
  rootOutputDir,
327
- routes,
328
- functionInfos,
329
- cronInfos,
330
- subscriberInfos,
342
+ manifestRoutes,
343
+ manifestFunctions,
344
+ manifestCrons,
345
+ manifestSubscribers,
331
346
  );
332
347
  }
333
348
 
@@ -504,3 +519,31 @@ function getAppOutputPath(
504
519
  return join(appPath, '.gkm');
505
520
  }
506
521
  }
522
+
523
+ /**
524
+ * Format routes for logging, handling PartitionedRoutes.
525
+ */
526
+ function formatRoutes(routes: Routes): string {
527
+ if (isPartitionedRoutes(routes)) {
528
+ const paths = Array.isArray(routes.paths)
529
+ ? routes.paths.join(', ')
530
+ : routes.paths;
531
+ return `${paths} (partitioned)`;
532
+ }
533
+ return Array.isArray(routes) ? routes.join(', ') : routes;
534
+ }
535
+
536
+ /**
537
+ * Assemble a ManifestField from build infos and constructs.
538
+ * If any construct has a partition, returns a Record<string, T[]>.
539
+ * Otherwise, returns a flat T[].
540
+ */
541
+ function assembleManifestField<T>(
542
+ infos: T[],
543
+ constructs: GeneratedConstruct<any>[],
544
+ ): ManifestField<T> {
545
+ if (!hasPartitions(constructs)) {
546
+ return infos;
547
+ }
548
+ return groupInfosByPartition(infos, constructs);
549
+ }
@@ -16,31 +16,95 @@ export interface ServerAppInfo {
16
16
  endpoints: string;
17
17
  }
18
18
 
19
+ /**
20
+ * A manifest field is either a flat array (no partition) or
21
+ * an object keyed by partition name (partitioned).
22
+ */
23
+ export type ManifestField<T> = T[] | Record<string, T[]>;
24
+
25
+ function isPartitioned<T>(
26
+ field: ManifestField<T>,
27
+ ): field is Record<string, T[]> {
28
+ return !Array.isArray(field);
29
+ }
30
+
31
+ /**
32
+ * Serialize a manifest field to a TypeScript string.
33
+ * Flat arrays serialize as JSON arrays, partitioned fields as objects of arrays.
34
+ */
35
+ function serializeField<T>(field: ManifestField<T>, indent = 2): string {
36
+ if (Array.isArray(field)) {
37
+ return JSON.stringify(field, null, indent);
38
+ }
39
+ // Partitioned: { admin: [...], default: [...] }
40
+ const entries = Object.entries(field)
41
+ .map(
42
+ ([key, value]) =>
43
+ ` ${JSON.stringify(key)}: ${JSON.stringify(value, null, indent)}`,
44
+ )
45
+ .join(',\n');
46
+ return `{\n${entries},\n }`;
47
+ }
48
+
49
+ /**
50
+ * Count total items across a manifest field (flat or partitioned).
51
+ */
52
+ function countItems<T>(field: ManifestField<T>): number {
53
+ if (Array.isArray(field)) return field.length;
54
+ return Object.values(field).reduce((sum, arr) => sum + arr.length, 0);
55
+ }
56
+
57
+ /**
58
+ * Generate derived types for a construct field.
59
+ * @param fieldName - The field name in the manifest (e.g., 'routes')
60
+ * @param typeName - The exported type name (e.g., 'Route')
61
+ * @param partitioned - Whether this field is partitioned
62
+ */
63
+ function generateDerivedType(
64
+ fieldName: string,
65
+ typeName: string,
66
+ partitioned: boolean,
67
+ ): string {
68
+ if (partitioned) {
69
+ const partitionTypeName = `${typeName}Partition`;
70
+ return [
71
+ `export type ${partitionTypeName} = keyof typeof manifest.${fieldName};`,
72
+ `export type ${typeName}<P extends ${partitionTypeName} = ${partitionTypeName}> = (typeof manifest.${fieldName})[P][number];`,
73
+ ].join('\n');
74
+ }
75
+ return `export type ${typeName} = (typeof manifest.${fieldName})[number];`;
76
+ }
77
+
19
78
  export async function generateAwsManifest(
20
79
  outputDir: string,
21
- routes: RouteInfo[],
22
- functions: FunctionInfo[],
23
- crons: CronInfo[],
24
- subscribers: SubscriberInfo[],
80
+ routes: ManifestField<RouteInfo>,
81
+ functions: ManifestField<FunctionInfo>,
82
+ crons: ManifestField<CronInfo>,
83
+ subscribers: ManifestField<SubscriberInfo>,
25
84
  ): Promise<void> {
26
85
  const manifestDir = join(outputDir, 'manifest');
27
86
  await mkdir(manifestDir, { recursive: true });
28
87
 
29
88
  // Filter out 'ALL' method routes (server-specific)
30
- const awsRoutes = routes.filter((r) => r.method !== 'ALL');
89
+ const awsRoutes = filterAllRoutes(routes);
90
+
91
+ const routesPartitioned = isPartitioned(awsRoutes);
92
+ const functionsPartitioned = isPartitioned(functions);
93
+ const cronsPartitioned = isPartitioned(crons);
94
+ const subscribersPartitioned = isPartitioned(subscribers);
31
95
 
32
96
  const content = `export const manifest = {
33
- routes: ${JSON.stringify(awsRoutes, null, 2)},
34
- functions: ${JSON.stringify(functions, null, 2)},
35
- crons: ${JSON.stringify(crons, null, 2)},
36
- subscribers: ${JSON.stringify(subscribers, null, 2)},
97
+ routes: ${serializeField(awsRoutes)},
98
+ functions: ${serializeField(functions)},
99
+ crons: ${serializeField(crons)},
100
+ subscribers: ${serializeField(subscribers)},
37
101
  } as const;
38
102
 
39
103
  // Derived types
40
- export type Route = (typeof manifest.routes)[number];
41
- export type Function = (typeof manifest.functions)[number];
42
- export type Cron = (typeof manifest.crons)[number];
43
- export type Subscriber = (typeof manifest.subscribers)[number];
104
+ ${generateDerivedType('routes', 'Route', routesPartitioned)}
105
+ ${generateDerivedType('functions', 'Function', functionsPartitioned)}
106
+ ${generateDerivedType('crons', 'Cron', cronsPartitioned)}
107
+ ${generateDerivedType('subscribers', 'Subscriber', subscribersPartitioned)}
44
108
 
45
109
  // Useful union types
46
110
  export type Authorizer = Route['authorizer'];
@@ -52,7 +116,7 @@ export type RoutePath = Route['path'];
52
116
  await writeFile(manifestPath, content);
53
117
 
54
118
  logger.log(
55
- `Generated AWS manifest with ${awsRoutes.length} routes, ${functions.length} functions, ${crons.length} crons, ${subscribers.length} subscribers`,
119
+ `Generated AWS manifest with ${countItems(awsRoutes)} routes, ${countItems(functions)} functions, ${countItems(crons)} crons, ${countItems(subscribers)} subscribers`,
56
120
  );
57
121
  logger.log(`Manifest: ${relative(process.cwd(), manifestPath)}`);
58
122
  }
@@ -60,36 +124,30 @@ export type RoutePath = Route['path'];
60
124
  export async function generateServerManifest(
61
125
  outputDir: string,
62
126
  appInfo: ServerAppInfo,
63
- routes: RouteInfo[],
64
- subscribers: SubscriberInfo[],
127
+ routes: ManifestField<RouteInfo>,
128
+ subscribers: ManifestField<SubscriberInfo>,
65
129
  ): Promise<void> {
66
130
  const manifestDir = join(outputDir, 'manifest');
67
131
  await mkdir(manifestDir, { recursive: true });
68
132
 
69
133
  // For server, extract route metadata (path, method, authorizer)
70
- const serverRoutes = routes
71
- .filter((r) => r.method !== 'ALL')
72
- .map((r) => ({
73
- path: r.path,
74
- method: r.method,
75
- authorizer: r.authorizer,
76
- }));
134
+ const serverRoutes = mapRouteMetadata(filterAllRoutes(routes));
77
135
 
78
136
  // Server subscribers only need name and events
79
- const serverSubscribers = subscribers.map((s) => ({
80
- name: s.name,
81
- subscribedEvents: s.subscribedEvents,
82
- }));
137
+ const serverSubscribers = mapSubscriberMetadata(subscribers);
138
+
139
+ const routesPartitioned = isPartitioned(serverRoutes);
140
+ const subscribersPartitioned = isPartitioned(serverSubscribers);
83
141
 
84
142
  const content = `export const manifest = {
85
143
  app: ${JSON.stringify(appInfo, null, 2)},
86
- routes: ${JSON.stringify(serverRoutes, null, 2)},
87
- subscribers: ${JSON.stringify(serverSubscribers, null, 2)},
144
+ routes: ${serializeField(serverRoutes)},
145
+ subscribers: ${serializeField(serverSubscribers)},
88
146
  } as const;
89
147
 
90
148
  // Derived types
91
- export type Route = (typeof manifest.routes)[number];
92
- export type Subscriber = (typeof manifest.subscribers)[number];
149
+ ${generateDerivedType('routes', 'Route', routesPartitioned)}
150
+ ${generateDerivedType('subscribers', 'Subscriber', subscribersPartitioned)}
93
151
 
94
152
  // Useful union types
95
153
  export type Authorizer = Route['authorizer'];
@@ -101,7 +159,70 @@ export type RoutePath = Route['path'];
101
159
  await writeFile(manifestPath, content);
102
160
 
103
161
  logger.log(
104
- `Generated server manifest with ${serverRoutes.length} routes, ${serverSubscribers.length} subscribers`,
162
+ `Generated server manifest with ${countItems(serverRoutes)} routes, ${countItems(serverSubscribers)} subscribers`,
105
163
  );
106
164
  logger.log(`Manifest: ${relative(process.cwd(), manifestPath)}`);
107
165
  }
166
+
167
+ /**
168
+ * Filter out 'ALL' method routes from a manifest field (flat or partitioned).
169
+ */
170
+ function filterAllRoutes(
171
+ routes: ManifestField<RouteInfo>,
172
+ ): ManifestField<RouteInfo> {
173
+ if (Array.isArray(routes)) {
174
+ return routes.filter((r) => r.method !== 'ALL');
175
+ }
176
+ const result: Record<string, RouteInfo[]> = {};
177
+ for (const [partition, partitionRoutes] of Object.entries(routes)) {
178
+ result[partition] = partitionRoutes.filter((r) => r.method !== 'ALL');
179
+ }
180
+ return result;
181
+ }
182
+
183
+ /**
184
+ * Map routes to server metadata (path, method, authorizer only).
185
+ */
186
+ function mapRouteMetadata(
187
+ routes: ManifestField<RouteInfo>,
188
+ ): ManifestField<{ path: string; method: string; authorizer: string }> {
189
+ const mapFn = (r: RouteInfo) => ({
190
+ path: r.path,
191
+ method: r.method,
192
+ authorizer: r.authorizer,
193
+ });
194
+
195
+ if (Array.isArray(routes)) {
196
+ return routes.map(mapFn);
197
+ }
198
+ const result: Record<
199
+ string,
200
+ { path: string; method: string; authorizer: string }[]
201
+ > = {};
202
+ for (const [partition, partitionRoutes] of Object.entries(routes)) {
203
+ result[partition] = partitionRoutes.map(mapFn);
204
+ }
205
+ return result;
206
+ }
207
+
208
+ /**
209
+ * Map subscribers to server metadata (name, subscribedEvents only).
210
+ */
211
+ function mapSubscriberMetadata(
212
+ subscribers: ManifestField<SubscriberInfo>,
213
+ ): ManifestField<{ name: string; subscribedEvents: string[] }> {
214
+ const mapFn = (s: SubscriberInfo) => ({
215
+ name: s.name,
216
+ subscribedEvents: s.subscribedEvents,
217
+ });
218
+
219
+ if (Array.isArray(subscribers)) {
220
+ return subscribers.map(mapFn);
221
+ }
222
+ const result: Record<string, { name: string; subscribedEvents: string[] }[]> =
223
+ {};
224
+ for (const [partition, partitionSubs] of Object.entries(subscribers)) {
225
+ result[partition] = partitionSubs.map(mapFn);
226
+ }
227
+ return result;
228
+ }