@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.
- package/CHANGELOG.md +15 -0
- package/README.md +42 -6
- package/dist/{config-6JHOwLCx.cjs → config-D3ORuiUs.cjs} +2 -2
- package/dist/{config-6JHOwLCx.cjs.map → config-D3ORuiUs.cjs.map} +1 -1
- package/dist/{config-DxASSNjr.mjs → config-jsRYHOHU.mjs} +2 -2
- package/dist/{config-DxASSNjr.mjs.map → config-jsRYHOHU.mjs.map} +1 -1
- package/dist/config.cjs +2 -2
- package/dist/config.d.cts +2 -2
- package/dist/config.d.mts +2 -2
- package/dist/config.mjs +2 -2
- package/dist/{index-Bt2kX0-R.d.mts → index-3n-giNaw.d.mts} +18 -6
- package/dist/index-3n-giNaw.d.mts.map +1 -0
- package/dist/{index-Cyk2rTyj.d.cts → index-CiEOtKEX.d.cts} +18 -6
- package/dist/index-CiEOtKEX.d.cts.map +1 -0
- package/dist/index.cjs +182 -158
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +179 -155
- package/dist/index.mjs.map +1 -1
- package/dist/{openapi-CnvwSRDU.cjs → openapi-BYxAWwok.cjs} +178 -32
- package/dist/openapi-BYxAWwok.cjs.map +1 -0
- package/dist/{openapi-BYlyAbH3.mjs → openapi-DenF-okj.mjs} +148 -32
- package/dist/openapi-DenF-okj.mjs.map +1 -0
- package/dist/openapi.cjs +3 -3
- package/dist/openapi.d.cts +1 -1
- package/dist/openapi.d.cts.map +1 -1
- package/dist/openapi.d.mts +1 -1
- package/dist/openapi.d.mts.map +1 -1
- package/dist/openapi.mjs +3 -3
- package/dist/{types-l53qUmGt.d.cts → types-C7QJJl9f.d.cts} +6 -2
- package/dist/types-C7QJJl9f.d.cts.map +1 -0
- package/dist/{types-wXMIMOyK.d.mts → types-Iqsq_FIG.d.mts} +6 -2
- package/dist/types-Iqsq_FIG.d.mts.map +1 -0
- package/dist/workspace/index.cjs +1 -1
- package/dist/workspace/index.d.cts +2 -2
- package/dist/workspace/index.d.mts +2 -2
- package/dist/workspace/index.mjs +1 -1
- package/dist/{workspace-D2ocAlpl.cjs → workspace-4SP3Gx4Y.cjs} +11 -3
- package/dist/{workspace-D2ocAlpl.cjs.map → workspace-4SP3Gx4Y.cjs.map} +1 -1
- package/dist/{workspace-9IQIjwkQ.mjs → workspace-D4z4A4cq.mjs} +11 -3
- package/dist/{workspace-9IQIjwkQ.mjs.map → workspace-D4z4A4cq.mjs.map} +1 -1
- package/package.json +4 -4
- package/src/build/__tests__/manifests.spec.ts +171 -0
- package/src/build/__tests__/partitions.spec.ts +110 -0
- package/src/build/index.ts +58 -15
- package/src/build/manifests.ts +153 -32
- package/src/build/partitions.ts +58 -0
- package/src/deploy/sniffer.ts +6 -1
- package/src/generators/Generator.ts +27 -7
- package/src/generators/OpenApiTsGenerator.ts +4 -4
- package/src/openapi.ts +2 -1
- package/src/types.ts +17 -1
- package/src/workspace/client-generator.ts +6 -3
- package/src/workspace/schema.ts +13 -3
- package/dist/index-Bt2kX0-R.d.mts.map +0 -1
- package/dist/index-Cyk2rTyj.d.cts.map +0 -1
- package/dist/openapi-BYlyAbH3.mjs.map +0 -1
- package/dist/openapi-CnvwSRDU.cjs.map +0 -1
- package/dist/types-l53qUmGt.d.cts.map +0 -1
- 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.
|
|
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": "~
|
|
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
|
+
});
|
package/src/build/index.ts
CHANGED
|
@@ -26,11 +26,13 @@ import {
|
|
|
26
26
|
type GeneratedConstruct,
|
|
27
27
|
SubscriberGenerator,
|
|
28
28
|
} from '../generators';
|
|
29
|
-
import
|
|
30
|
-
BuildOptions,
|
|
31
|
-
BuildResult,
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
282
|
-
|
|
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
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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
|
+
}
|
package/src/build/manifests.ts
CHANGED
|
@@ -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
|
|
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: ${
|
|
34
|
-
functions: ${
|
|
35
|
-
crons: ${
|
|
36
|
-
subscribers: ${
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
|
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
|
|
80
|
-
|
|
81
|
-
|
|
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: ${
|
|
87
|
-
subscribers: ${
|
|
144
|
+
routes: ${serializeField(serverRoutes)},
|
|
145
|
+
subscribers: ${serializeField(serverSubscribers)},
|
|
88
146
|
} as const;
|
|
89
147
|
|
|
90
148
|
// Derived types
|
|
91
|
-
|
|
92
|
-
|
|
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
|
|
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
|
+
}
|