@geekmidas/cli 0.17.0 → 0.19.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/dist/{bundler-C74EKlNa.cjs → bundler-CyHg1v_T.cjs} +3 -3
- package/dist/{bundler-C74EKlNa.cjs.map → bundler-CyHg1v_T.cjs.map} +1 -1
- package/dist/{bundler-B6z6HEeh.mjs → bundler-DQIuE3Kn.mjs} +3 -3
- package/dist/{bundler-B6z6HEeh.mjs.map → bundler-DQIuE3Kn.mjs.map} +1 -1
- package/dist/{config-DYULeEv8.mjs → config-BaYqrF3n.mjs} +48 -10
- package/dist/config-BaYqrF3n.mjs.map +1 -0
- package/dist/{config-AmInkU7k.cjs → config-CxrLu8ia.cjs} +53 -9
- package/dist/config-CxrLu8ia.cjs.map +1 -0
- package/dist/config.cjs +4 -1
- package/dist/config.d.cts +27 -2
- package/dist/config.d.cts.map +1 -1
- package/dist/config.d.mts +27 -2
- package/dist/config.d.mts.map +1 -1
- package/dist/config.mjs +3 -2
- package/dist/dokploy-api-B0w17y4_.mjs +3 -0
- package/dist/{dokploy-api-CaETb2L6.mjs → dokploy-api-B9qR2Yn1.mjs} +1 -1
- package/dist/{dokploy-api-CaETb2L6.mjs.map → dokploy-api-B9qR2Yn1.mjs.map} +1 -1
- package/dist/dokploy-api-BnGeUqN4.cjs +3 -0
- package/dist/{dokploy-api-C7F9VykY.cjs → dokploy-api-C5czOZoc.cjs} +1 -1
- package/dist/{dokploy-api-C7F9VykY.cjs.map → dokploy-api-C5czOZoc.cjs.map} +1 -1
- package/dist/{encryption-D7Efcdi9.cjs → encryption-BAz0xQ1Q.cjs} +1 -1
- package/dist/{encryption-D7Efcdi9.cjs.map → encryption-BAz0xQ1Q.cjs.map} +1 -1
- package/dist/{encryption-h4Nb6W-M.mjs → encryption-JtMsiGNp.mjs} +2 -2
- package/dist/{encryption-h4Nb6W-M.mjs.map → encryption-JtMsiGNp.mjs.map} +1 -1
- package/dist/index-CWN-bgrO.d.mts +495 -0
- package/dist/index-CWN-bgrO.d.mts.map +1 -0
- package/dist/index-DEWYvYvg.d.cts +495 -0
- package/dist/index-DEWYvYvg.d.cts.map +1 -0
- package/dist/index.cjs +2644 -564
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +2639 -564
- package/dist/index.mjs.map +1 -1
- package/dist/{openapi-CZVcfxk-.mjs → openapi-CgqR6Jkw.mjs} +3 -3
- package/dist/{openapi-CZVcfxk-.mjs.map → openapi-CgqR6Jkw.mjs.map} +1 -1
- package/dist/{openapi-C89hhkZC.cjs → openapi-DfpxS0xv.cjs} +8 -2
- package/dist/{openapi-C89hhkZC.cjs.map → openapi-DfpxS0xv.cjs.map} +1 -1
- package/dist/{openapi-react-query-CM2_qlW9.mjs → openapi-react-query-5rSortLH.mjs} +1 -1
- package/dist/{openapi-react-query-CM2_qlW9.mjs.map → openapi-react-query-5rSortLH.mjs.map} +1 -1
- package/dist/{openapi-react-query-iKjfLzff.cjs → openapi-react-query-DvNpdDpM.cjs} +1 -1
- package/dist/{openapi-react-query-iKjfLzff.cjs.map → openapi-react-query-DvNpdDpM.cjs.map} +1 -1
- package/dist/openapi-react-query.cjs +1 -1
- package/dist/openapi-react-query.mjs +1 -1
- package/dist/openapi.cjs +3 -2
- package/dist/openapi.d.cts +1 -1
- package/dist/openapi.d.mts +1 -1
- package/dist/openapi.mjs +3 -2
- package/dist/{storage-Bn3K9Ccu.cjs → storage-BPRgh3DU.cjs} +136 -5
- package/dist/storage-BPRgh3DU.cjs.map +1 -0
- package/dist/{storage-nkGIjeXt.mjs → storage-DNj_I11J.mjs} +1 -1
- package/dist/storage-Dhst7BhI.mjs +272 -0
- package/dist/storage-Dhst7BhI.mjs.map +1 -0
- package/dist/{storage-UfyTn7Zm.cjs → storage-fOR8dMu5.cjs} +1 -1
- package/dist/{types-iFk5ms7y.d.mts → types-K2uQJ-FO.d.mts} +2 -2
- package/dist/{types-BgaMXsUa.d.cts.map → types-K2uQJ-FO.d.mts.map} +1 -1
- package/dist/{types-BgaMXsUa.d.cts → types-l53qUmGt.d.cts} +2 -2
- package/dist/{types-iFk5ms7y.d.mts.map → types-l53qUmGt.d.cts.map} +1 -1
- package/dist/workspace/index.cjs +19 -0
- package/dist/workspace/index.d.cts +3 -0
- package/dist/workspace/index.d.mts +3 -0
- package/dist/workspace/index.mjs +3 -0
- package/dist/workspace-CPLEZDZf.mjs +3788 -0
- package/dist/workspace-CPLEZDZf.mjs.map +1 -0
- package/dist/workspace-iWgBlX6h.cjs +3885 -0
- package/dist/workspace-iWgBlX6h.cjs.map +1 -0
- package/package.json +9 -4
- package/src/build/__tests__/workspace-build.spec.ts +215 -0
- package/src/build/index.ts +189 -1
- package/src/config.ts +71 -14
- package/src/deploy/__tests__/docker.spec.ts +1 -1
- package/src/deploy/__tests__/index.spec.ts +305 -1
- package/src/deploy/index.ts +426 -4
- package/src/deploy/types.ts +32 -0
- package/src/dev/__tests__/index.spec.ts +572 -1
- package/src/dev/index.ts +582 -2
- package/src/docker/__tests__/compose.spec.ts +425 -0
- package/src/docker/__tests__/templates.spec.ts +145 -0
- package/src/docker/compose.ts +248 -0
- package/src/docker/index.ts +159 -3
- package/src/docker/templates.ts +223 -4
- package/src/index.ts +24 -0
- package/src/init/__tests__/generators.spec.ts +17 -24
- package/src/init/__tests__/init.spec.ts +157 -5
- package/src/init/generators/auth.ts +220 -0
- package/src/init/generators/config.ts +61 -4
- package/src/init/generators/docker.ts +115 -8
- package/src/init/generators/env.ts +7 -127
- package/src/init/generators/index.ts +1 -0
- package/src/init/generators/models.ts +3 -1
- package/src/init/generators/monorepo.ts +154 -10
- package/src/init/generators/package.ts +5 -3
- package/src/init/generators/web.ts +213 -0
- package/src/init/index.ts +290 -58
- package/src/init/templates/api.ts +38 -29
- package/src/init/templates/index.ts +132 -4
- package/src/init/templates/minimal.ts +33 -35
- package/src/init/templates/serverless.ts +16 -19
- package/src/init/templates/worker.ts +50 -25
- package/src/init/versions.ts +47 -0
- package/src/secrets/keystore.ts +144 -0
- package/src/secrets/storage.ts +109 -6
- package/src/test/index.ts +97 -0
- package/src/workspace/__tests__/client-generator.spec.ts +357 -0
- package/src/workspace/__tests__/index.spec.ts +543 -0
- package/src/workspace/__tests__/schema.spec.ts +519 -0
- package/src/workspace/__tests__/type-inference.spec.ts +251 -0
- package/src/workspace/client-generator.ts +307 -0
- package/src/workspace/index.ts +372 -0
- package/src/workspace/schema.ts +368 -0
- package/src/workspace/types.ts +336 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/tsdown.config.ts +1 -0
- package/dist/config-AmInkU7k.cjs.map +0 -1
- package/dist/config-DYULeEv8.mjs.map +0 -1
- package/dist/dokploy-api-B7KxOQr3.cjs +0 -3
- package/dist/dokploy-api-DHvfmWbi.mjs +0 -3
- package/dist/storage-BaOP55oq.mjs +0 -147
- package/dist/storage-BaOP55oq.mjs.map +0 -1
- package/dist/storage-Bn3K9Ccu.cjs.map +0 -1
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { describe, expect, expectTypeOf, it } from 'vitest';
|
|
2
|
+
import { defineWorkspace } from '../index.js';
|
|
3
|
+
import type { InferAppNames, InferredWorkspaceConfig } from '../types.js';
|
|
4
|
+
|
|
5
|
+
describe('Type Inference', () => {
|
|
6
|
+
describe('defineWorkspace type inference', () => {
|
|
7
|
+
it('should infer app names as literal types', () => {
|
|
8
|
+
const config = defineWorkspace({
|
|
9
|
+
apps: {
|
|
10
|
+
api: {
|
|
11
|
+
type: 'backend',
|
|
12
|
+
path: 'apps/api',
|
|
13
|
+
port: 3000,
|
|
14
|
+
routes: './src/**/*.ts',
|
|
15
|
+
},
|
|
16
|
+
web: {
|
|
17
|
+
type: 'frontend',
|
|
18
|
+
path: 'apps/web',
|
|
19
|
+
port: 3001,
|
|
20
|
+
framework: 'nextjs',
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// Type-level test: apps should have 'api' and 'web' as keys
|
|
26
|
+
expectTypeOf(config.apps).toHaveProperty('api');
|
|
27
|
+
expectTypeOf(config.apps).toHaveProperty('web');
|
|
28
|
+
|
|
29
|
+
// Runtime test
|
|
30
|
+
expect(Object.keys(config.apps)).toEqual(['api', 'web']);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should allow valid dependencies', () => {
|
|
34
|
+
const config = defineWorkspace({
|
|
35
|
+
apps: {
|
|
36
|
+
api: {
|
|
37
|
+
type: 'backend',
|
|
38
|
+
path: 'apps/api',
|
|
39
|
+
port: 3000,
|
|
40
|
+
routes: './src/**/*.ts',
|
|
41
|
+
},
|
|
42
|
+
auth: {
|
|
43
|
+
type: 'backend',
|
|
44
|
+
path: 'apps/auth',
|
|
45
|
+
port: 3001,
|
|
46
|
+
routes: './src/**/*.ts',
|
|
47
|
+
},
|
|
48
|
+
web: {
|
|
49
|
+
type: 'frontend',
|
|
50
|
+
path: 'apps/web',
|
|
51
|
+
port: 3002,
|
|
52
|
+
framework: 'nextjs',
|
|
53
|
+
dependencies: ['api', 'auth'],
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
expect(config.apps.web.dependencies).toEqual(['api', 'auth']);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should throw error for invalid dependency at runtime', () => {
|
|
62
|
+
expect(() =>
|
|
63
|
+
defineWorkspace({
|
|
64
|
+
apps: {
|
|
65
|
+
api: {
|
|
66
|
+
type: 'backend',
|
|
67
|
+
path: 'apps/api',
|
|
68
|
+
port: 3000,
|
|
69
|
+
routes: './src/**/*.ts',
|
|
70
|
+
},
|
|
71
|
+
web: {
|
|
72
|
+
type: 'frontend',
|
|
73
|
+
path: 'apps/web',
|
|
74
|
+
port: 3001,
|
|
75
|
+
framework: 'nextjs',
|
|
76
|
+
dependencies: ['nonexistent'],
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
}),
|
|
80
|
+
).toThrow(/Invalid dependency.*"nonexistent"/);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should throw error for self-dependency at runtime', () => {
|
|
84
|
+
expect(() =>
|
|
85
|
+
defineWorkspace({
|
|
86
|
+
apps: {
|
|
87
|
+
api: {
|
|
88
|
+
type: 'backend',
|
|
89
|
+
path: 'apps/api',
|
|
90
|
+
port: 3000,
|
|
91
|
+
routes: './src/**/*.ts',
|
|
92
|
+
dependencies: ['api'],
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
}),
|
|
96
|
+
).toThrow(/cannot depend on itself/);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should preserve all config properties with inference', () => {
|
|
100
|
+
const config = defineWorkspace({
|
|
101
|
+
name: 'my-workspace',
|
|
102
|
+
apps: {
|
|
103
|
+
api: {
|
|
104
|
+
type: 'backend',
|
|
105
|
+
path: 'apps/api',
|
|
106
|
+
port: 3000,
|
|
107
|
+
routes: './src/**/*.ts',
|
|
108
|
+
envParser: './src/env',
|
|
109
|
+
logger: './src/logger',
|
|
110
|
+
telescope: { enabled: true, port: 9000 },
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
services: {
|
|
114
|
+
db: true,
|
|
115
|
+
cache: { version: '7.2' },
|
|
116
|
+
},
|
|
117
|
+
deploy: {
|
|
118
|
+
default: 'dokploy',
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
expect(config.name).toBe('my-workspace');
|
|
123
|
+
expect(config.apps.api.envParser).toBe('./src/env');
|
|
124
|
+
expect(config.apps.api.telescope).toEqual({ enabled: true, port: 9000 });
|
|
125
|
+
expect(config.services?.db).toBe(true);
|
|
126
|
+
expect(config.deploy?.default).toBe('dokploy');
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe('InferAppNames type utility', () => {
|
|
131
|
+
it('should extract app names as union type', () => {
|
|
132
|
+
type TestApps = {
|
|
133
|
+
api: { path: string; port: number };
|
|
134
|
+
web: { path: string; port: number };
|
|
135
|
+
worker: { path: string; port: number };
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
type AppNames = InferAppNames<TestApps>;
|
|
139
|
+
|
|
140
|
+
// Type-level assertion: AppNames should be 'api' | 'web' | 'worker'
|
|
141
|
+
expectTypeOf<AppNames>().toEqualTypeOf<'api' | 'web' | 'worker'>();
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe('InferredWorkspaceConfig type utility', () => {
|
|
146
|
+
it('should create correct inferred config type', () => {
|
|
147
|
+
type TestApps = {
|
|
148
|
+
api: { path: string; port: number; routes: string };
|
|
149
|
+
web: { path: string; port: number; framework: 'nextjs' };
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
type Inferred = InferredWorkspaceConfig<TestApps>;
|
|
153
|
+
|
|
154
|
+
// The inferred type should have the same structure
|
|
155
|
+
expectTypeOf<Inferred['apps']['api']>().toHaveProperty('path');
|
|
156
|
+
expectTypeOf<Inferred['apps']['web']>().toHaveProperty('framework');
|
|
157
|
+
|
|
158
|
+
// Dependencies should be typed as 'api' | 'web'
|
|
159
|
+
type WebDeps = NonNullable<Inferred['apps']['web']['dependencies']>;
|
|
160
|
+
expectTypeOf<WebDeps>().toEqualTypeOf<('api' | 'web')[]>();
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
describe('Complex workspace scenarios', () => {
|
|
165
|
+
it('should handle workspace with multiple backend and frontend apps', () => {
|
|
166
|
+
const config = defineWorkspace({
|
|
167
|
+
name: 'complex-saas',
|
|
168
|
+
apps: {
|
|
169
|
+
'api-gateway': {
|
|
170
|
+
type: 'backend',
|
|
171
|
+
path: 'apps/api-gateway',
|
|
172
|
+
port: 3000,
|
|
173
|
+
routes: './src/**/*.ts',
|
|
174
|
+
},
|
|
175
|
+
'user-service': {
|
|
176
|
+
type: 'backend',
|
|
177
|
+
path: 'apps/user-service',
|
|
178
|
+
port: 3001,
|
|
179
|
+
routes: './src/**/*.ts',
|
|
180
|
+
},
|
|
181
|
+
'payment-service': {
|
|
182
|
+
type: 'backend',
|
|
183
|
+
path: 'apps/payment-service',
|
|
184
|
+
port: 3002,
|
|
185
|
+
routes: './src/**/*.ts',
|
|
186
|
+
dependencies: ['user-service'],
|
|
187
|
+
},
|
|
188
|
+
'web-app': {
|
|
189
|
+
type: 'frontend',
|
|
190
|
+
path: 'apps/web',
|
|
191
|
+
port: 3003,
|
|
192
|
+
framework: 'nextjs',
|
|
193
|
+
dependencies: ['api-gateway'],
|
|
194
|
+
},
|
|
195
|
+
'admin-dashboard': {
|
|
196
|
+
type: 'frontend',
|
|
197
|
+
path: 'apps/admin',
|
|
198
|
+
port: 3004,
|
|
199
|
+
framework: 'nextjs',
|
|
200
|
+
dependencies: ['api-gateway', 'user-service'],
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
services: {
|
|
204
|
+
db: { version: '16-alpine' },
|
|
205
|
+
cache: true,
|
|
206
|
+
mail: { smtp: { host: 'smtp.example.com', port: 587 } },
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
expect(Object.keys(config.apps)).toHaveLength(5);
|
|
211
|
+
expect(config.apps['payment-service'].dependencies).toEqual([
|
|
212
|
+
'user-service',
|
|
213
|
+
]);
|
|
214
|
+
expect(config.apps['admin-dashboard'].dependencies).toEqual([
|
|
215
|
+
'api-gateway',
|
|
216
|
+
'user-service',
|
|
217
|
+
]);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('should handle empty dependencies array', () => {
|
|
221
|
+
const config = defineWorkspace({
|
|
222
|
+
apps: {
|
|
223
|
+
api: {
|
|
224
|
+
type: 'backend',
|
|
225
|
+
path: 'apps/api',
|
|
226
|
+
port: 3000,
|
|
227
|
+
routes: './src/**/*.ts',
|
|
228
|
+
dependencies: [],
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
expect(config.apps.api.dependencies).toEqual([]);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('should handle undefined dependencies', () => {
|
|
237
|
+
const config = defineWorkspace({
|
|
238
|
+
apps: {
|
|
239
|
+
api: {
|
|
240
|
+
type: 'backend',
|
|
241
|
+
path: 'apps/api',
|
|
242
|
+
port: 3000,
|
|
243
|
+
routes: './src/**/*.ts',
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
expect(config.apps.api.dependencies).toBeUndefined();
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
});
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { dirname, join, relative } from 'node:path';
|
|
4
|
+
import { EndpointGenerator } from '../generators/EndpointGenerator.js';
|
|
5
|
+
import { OpenApiTsGenerator } from '../generators/OpenApiTsGenerator.js';
|
|
6
|
+
import type { NormalizedWorkspace } from './types.js';
|
|
7
|
+
|
|
8
|
+
const logger = console;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Result of generating a client for a frontend app.
|
|
12
|
+
*/
|
|
13
|
+
export interface ClientGenerationResult {
|
|
14
|
+
frontendApp: string;
|
|
15
|
+
backendApp: string;
|
|
16
|
+
outputPath: string;
|
|
17
|
+
endpointCount: number;
|
|
18
|
+
generated: boolean;
|
|
19
|
+
reason?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Cache of OpenAPI spec hashes to detect changes.
|
|
24
|
+
*/
|
|
25
|
+
const specHashCache = new Map<string, string>();
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Calculate hash of content for change detection.
|
|
29
|
+
*/
|
|
30
|
+
function hashContent(content: string): string {
|
|
31
|
+
return createHash('sha256').update(content).digest('hex').slice(0, 16);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Normalize routes to an array of patterns.
|
|
36
|
+
* @internal Exported for use in dev command
|
|
37
|
+
*/
|
|
38
|
+
export function normalizeRoutes(
|
|
39
|
+
routes: string | string[] | undefined,
|
|
40
|
+
): string[] {
|
|
41
|
+
if (!routes) return [];
|
|
42
|
+
return Array.isArray(routes) ? routes : [routes];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Get the first routes pattern as a string (for simple cases).
|
|
47
|
+
* @internal Exported for use in dev command
|
|
48
|
+
*/
|
|
49
|
+
export function getFirstRoute(
|
|
50
|
+
routes: string | string[] | undefined,
|
|
51
|
+
): string | null {
|
|
52
|
+
const normalized = normalizeRoutes(routes);
|
|
53
|
+
return normalized[0] || null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Generate OpenAPI spec for a backend app.
|
|
58
|
+
* Returns the spec content and endpoint count.
|
|
59
|
+
*/
|
|
60
|
+
export async function generateBackendOpenApi(
|
|
61
|
+
workspace: NormalizedWorkspace,
|
|
62
|
+
appName: string,
|
|
63
|
+
): Promise<{ content: string; endpointCount: number } | null> {
|
|
64
|
+
const app = workspace.apps[appName];
|
|
65
|
+
if (!app || app.type !== 'backend' || !app.routes) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const appPath = join(workspace.root, app.path);
|
|
70
|
+
const routesPatterns = normalizeRoutes(app.routes);
|
|
71
|
+
|
|
72
|
+
if (routesPatterns.length === 0) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Load endpoints from all routes patterns
|
|
77
|
+
const endpointGenerator = new EndpointGenerator();
|
|
78
|
+
const allLoadedEndpoints = [];
|
|
79
|
+
|
|
80
|
+
for (const pattern of routesPatterns) {
|
|
81
|
+
const fullPattern = join(appPath, pattern);
|
|
82
|
+
const loaded = await endpointGenerator.load(fullPattern);
|
|
83
|
+
allLoadedEndpoints.push(...loaded);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const loadedEndpoints = allLoadedEndpoints;
|
|
87
|
+
|
|
88
|
+
if (loadedEndpoints.length === 0) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const endpoints = loadedEndpoints.map(({ construct }) => construct);
|
|
93
|
+
|
|
94
|
+
const tsGenerator = new OpenApiTsGenerator();
|
|
95
|
+
const content = await tsGenerator.generate(endpoints, {
|
|
96
|
+
title: `${appName} API`,
|
|
97
|
+
version: '1.0.0',
|
|
98
|
+
description: `Auto-generated API client for ${appName}`,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
return { content, endpointCount: loadedEndpoints.length };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Generate client for a frontend app from its backend dependencies.
|
|
106
|
+
* Only regenerates if the OpenAPI spec has changed.
|
|
107
|
+
*/
|
|
108
|
+
export async function generateClientForFrontend(
|
|
109
|
+
workspace: NormalizedWorkspace,
|
|
110
|
+
frontendAppName: string,
|
|
111
|
+
options: { force?: boolean } = {},
|
|
112
|
+
): Promise<ClientGenerationResult[]> {
|
|
113
|
+
const results: ClientGenerationResult[] = [];
|
|
114
|
+
const frontendApp = workspace.apps[frontendAppName];
|
|
115
|
+
|
|
116
|
+
if (!frontendApp || frontendApp.type !== 'frontend') {
|
|
117
|
+
return results;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const dependencies = frontendApp.dependencies || [];
|
|
121
|
+
const backendDeps = dependencies.filter((dep) => {
|
|
122
|
+
const depApp = workspace.apps[dep];
|
|
123
|
+
return depApp?.type === 'backend' && depApp.routes;
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
if (backendDeps.length === 0) {
|
|
127
|
+
return results;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Determine output directory
|
|
131
|
+
const clientOutput = frontendApp.client?.output || 'src/api';
|
|
132
|
+
const frontendPath = join(workspace.root, frontendApp.path);
|
|
133
|
+
const outputDir = join(frontendPath, clientOutput);
|
|
134
|
+
|
|
135
|
+
for (const backendAppName of backendDeps) {
|
|
136
|
+
const result: ClientGenerationResult = {
|
|
137
|
+
frontendApp: frontendAppName,
|
|
138
|
+
backendApp: backendAppName,
|
|
139
|
+
outputPath: '',
|
|
140
|
+
endpointCount: 0,
|
|
141
|
+
generated: false,
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
// Generate OpenAPI spec for backend
|
|
146
|
+
const spec = await generateBackendOpenApi(workspace, backendAppName);
|
|
147
|
+
|
|
148
|
+
if (!spec) {
|
|
149
|
+
result.reason = 'No endpoints found in backend';
|
|
150
|
+
results.push(result);
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
result.endpointCount = spec.endpointCount;
|
|
155
|
+
|
|
156
|
+
// Check if spec has changed (unless force)
|
|
157
|
+
const cacheKey = `${backendAppName}:${frontendAppName}`;
|
|
158
|
+
const newHash = hashContent(spec.content);
|
|
159
|
+
const oldHash = specHashCache.get(cacheKey);
|
|
160
|
+
|
|
161
|
+
if (!options.force && oldHash === newHash) {
|
|
162
|
+
result.reason = 'No schema changes detected';
|
|
163
|
+
results.push(result);
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Generate client file
|
|
168
|
+
await mkdir(outputDir, { recursive: true });
|
|
169
|
+
|
|
170
|
+
// For single dependency, use openapi.ts; for multiple, use {backend}-api.ts
|
|
171
|
+
const fileName =
|
|
172
|
+
backendDeps.length === 1 ? 'openapi.ts' : `${backendAppName}-api.ts`;
|
|
173
|
+
const outputPath = join(outputDir, fileName);
|
|
174
|
+
|
|
175
|
+
// Add header comment with backend reference
|
|
176
|
+
const backendRelPath = relative(
|
|
177
|
+
dirname(outputPath),
|
|
178
|
+
join(workspace.root, workspace.apps[backendAppName]!.path),
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
const clientContent = `/**
|
|
182
|
+
* Auto-generated API client for ${backendAppName}
|
|
183
|
+
* Generated from: ${backendRelPath}
|
|
184
|
+
*
|
|
185
|
+
* DO NOT EDIT - This file is automatically regenerated when backend schemas change.
|
|
186
|
+
*/
|
|
187
|
+
|
|
188
|
+
${spec.content}
|
|
189
|
+
`;
|
|
190
|
+
|
|
191
|
+
await writeFile(outputPath, clientContent);
|
|
192
|
+
|
|
193
|
+
// Update cache
|
|
194
|
+
specHashCache.set(cacheKey, newHash);
|
|
195
|
+
|
|
196
|
+
result.outputPath = outputPath;
|
|
197
|
+
result.generated = true;
|
|
198
|
+
results.push(result);
|
|
199
|
+
} catch (error) {
|
|
200
|
+
result.reason = `Error: ${(error as Error).message}`;
|
|
201
|
+
results.push(result);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return results;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Generate clients for all frontend apps in the workspace.
|
|
210
|
+
*/
|
|
211
|
+
export async function generateAllClients(
|
|
212
|
+
workspace: NormalizedWorkspace,
|
|
213
|
+
options: { force?: boolean; silent?: boolean } = {},
|
|
214
|
+
): Promise<ClientGenerationResult[]> {
|
|
215
|
+
const log = options.silent ? () => {} : logger.log.bind(logger);
|
|
216
|
+
const allResults: ClientGenerationResult[] = [];
|
|
217
|
+
|
|
218
|
+
for (const [appName, app] of Object.entries(workspace.apps)) {
|
|
219
|
+
if (app.type === 'frontend' && app.dependencies.length > 0) {
|
|
220
|
+
const results = await generateClientForFrontend(workspace, appName, {
|
|
221
|
+
force: options.force,
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
for (const result of results) {
|
|
225
|
+
if (result.generated) {
|
|
226
|
+
log(
|
|
227
|
+
`📦 Generated client for ${result.frontendApp} from ${result.backendApp} (${result.endpointCount} endpoints)`,
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
allResults.push(result);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return allResults;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Check if a file path matches endpoint patterns that could affect OpenAPI schema.
|
|
240
|
+
* Returns true for changes that should trigger client regeneration.
|
|
241
|
+
*/
|
|
242
|
+
export function shouldRegenerateClient(
|
|
243
|
+
filePath: string,
|
|
244
|
+
routesPattern: string,
|
|
245
|
+
): boolean {
|
|
246
|
+
// Normalize path separators
|
|
247
|
+
const normalizedPath = filePath.replace(/\\/g, '/');
|
|
248
|
+
const normalizedPattern = routesPattern.replace(/\\/g, '/');
|
|
249
|
+
|
|
250
|
+
// Check if the file matches the routes pattern
|
|
251
|
+
// This is a simple check - the file should be within the routes directory
|
|
252
|
+
const patternDir = normalizedPattern.split('*')[0] || '';
|
|
253
|
+
|
|
254
|
+
if (!normalizedPath.includes(patternDir.replace('./', ''))) {
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Check file extension - only TypeScript endpoint files
|
|
259
|
+
if (!normalizedPath.endsWith('.ts') && !normalizedPath.endsWith('.tsx')) {
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return true;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Get backend apps that a frontend depends on.
|
|
268
|
+
*/
|
|
269
|
+
export function getBackendDependencies(
|
|
270
|
+
workspace: NormalizedWorkspace,
|
|
271
|
+
frontendAppName: string,
|
|
272
|
+
): string[] {
|
|
273
|
+
const frontendApp = workspace.apps[frontendAppName];
|
|
274
|
+
if (!frontendApp || frontendApp.type !== 'frontend') {
|
|
275
|
+
return [];
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return frontendApp.dependencies.filter((dep) => {
|
|
279
|
+
const depApp = workspace.apps[dep];
|
|
280
|
+
return depApp?.type === 'backend' && depApp.routes;
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Get frontend apps that depend on a backend app.
|
|
286
|
+
*/
|
|
287
|
+
export function getDependentFrontends(
|
|
288
|
+
workspace: NormalizedWorkspace,
|
|
289
|
+
backendAppName: string,
|
|
290
|
+
): string[] {
|
|
291
|
+
const dependentApps: string[] = [];
|
|
292
|
+
|
|
293
|
+
for (const [appName, app] of Object.entries(workspace.apps)) {
|
|
294
|
+
if (app.type === 'frontend' && app.dependencies.includes(backendAppName)) {
|
|
295
|
+
dependentApps.push(appName);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return dependentApps;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Clear the spec hash cache (useful for testing).
|
|
304
|
+
*/
|
|
305
|
+
export function clearSpecHashCache(): void {
|
|
306
|
+
specHashCache.clear();
|
|
307
|
+
}
|