@geekmidas/cli 1.2.2 → 1.2.3
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 +6 -0
- package/dist/index.cjs +88 -161
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +89 -162
- package/dist/index.mjs.map +1 -1
- package/dist/{openapi-BZ4Qik9w.mjs → openapi-NthphEWK.mjs} +2 -2
- package/dist/{openapi-BZ4Qik9w.mjs.map → openapi-NthphEWK.mjs.map} +1 -1
- package/dist/{openapi-CzfnHlhG.cjs → openapi-ZhO7wwya.cjs} +1 -7
- package/dist/{openapi-CzfnHlhG.cjs.map → openapi-ZhO7wwya.cjs.map} +1 -1
- package/dist/openapi.cjs +1 -1
- package/dist/openapi.mjs +1 -1
- package/package.json +2 -2
- package/src/dev/index.ts +69 -106
- package/src/workspace/__tests__/client-generator.spec.ts +330 -301
- package/src/workspace/client-generator.ts +139 -199
|
@@ -1,34 +1,20 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { mkdir, writeFile } from 'node:fs/promises';
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
3
3
|
import { dirname, join, relative } from 'node:path';
|
|
4
|
-
import { EndpointGenerator } from '../generators/EndpointGenerator.js';
|
|
5
|
-
import { OpenApiTsGenerator } from '../generators/OpenApiTsGenerator.js';
|
|
6
4
|
import type { NormalizedWorkspace } from './types.js';
|
|
7
5
|
|
|
8
6
|
const logger = console;
|
|
9
7
|
|
|
10
8
|
/**
|
|
11
|
-
* Result of
|
|
9
|
+
* Result of copying a client to a frontend app.
|
|
12
10
|
*/
|
|
13
|
-
export interface
|
|
11
|
+
export interface ClientCopyResult {
|
|
14
12
|
frontendApp: string;
|
|
15
13
|
backendApp: string;
|
|
16
14
|
outputPath: string;
|
|
17
15
|
endpointCount: number;
|
|
18
|
-
|
|
19
|
-
|
|
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);
|
|
16
|
+
success: boolean;
|
|
17
|
+
error?: string;
|
|
32
18
|
}
|
|
33
19
|
|
|
34
20
|
/**
|
|
@@ -54,128 +40,162 @@ export function getFirstRoute(
|
|
|
54
40
|
}
|
|
55
41
|
|
|
56
42
|
/**
|
|
57
|
-
*
|
|
58
|
-
* Returns
|
|
43
|
+
* Check if a file path matches endpoint patterns that could affect OpenAPI schema.
|
|
44
|
+
* Returns true for changes that should trigger client regeneration.
|
|
59
45
|
*/
|
|
60
|
-
export
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
):
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
46
|
+
export function shouldRegenerateClient(
|
|
47
|
+
filePath: string,
|
|
48
|
+
routesPattern: string,
|
|
49
|
+
): boolean {
|
|
50
|
+
// Normalize path separators
|
|
51
|
+
const normalizedPath = filePath.replace(/\\/g, '/');
|
|
52
|
+
const normalizedPattern = routesPattern.replace(/\\/g, '/');
|
|
53
|
+
|
|
54
|
+
// Check if the file matches the routes pattern
|
|
55
|
+
// This is a simple check - the file should be within the routes directory
|
|
56
|
+
const patternDir = normalizedPattern.split('*')[0] || '';
|
|
57
|
+
|
|
58
|
+
if (!normalizedPath.includes(patternDir.replace('./', ''))) {
|
|
59
|
+
return false;
|
|
67
60
|
}
|
|
68
61
|
|
|
69
|
-
|
|
70
|
-
|
|
62
|
+
// Check file extension - only TypeScript endpoint files
|
|
63
|
+
if (!normalizedPath.endsWith('.ts') && !normalizedPath.endsWith('.tsx')) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
71
66
|
|
|
72
|
-
|
|
73
|
-
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Get backend apps that a frontend depends on.
|
|
72
|
+
*/
|
|
73
|
+
export function getBackendDependencies(
|
|
74
|
+
workspace: NormalizedWorkspace,
|
|
75
|
+
frontendAppName: string,
|
|
76
|
+
): string[] {
|
|
77
|
+
const frontendApp = workspace.apps[frontendAppName];
|
|
78
|
+
if (!frontendApp || frontendApp.type !== 'frontend') {
|
|
79
|
+
return [];
|
|
74
80
|
}
|
|
75
81
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
82
|
+
return frontendApp.dependencies.filter((dep) => {
|
|
83
|
+
const depApp = workspace.apps[dep];
|
|
84
|
+
return depApp?.type === 'backend' && depApp.routes;
|
|
85
|
+
});
|
|
86
|
+
}
|
|
79
87
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
88
|
+
/**
|
|
89
|
+
* Get frontend apps that depend on a backend app.
|
|
90
|
+
*/
|
|
91
|
+
export function getDependentFrontends(
|
|
92
|
+
workspace: NormalizedWorkspace,
|
|
93
|
+
backendAppName: string,
|
|
94
|
+
): string[] {
|
|
95
|
+
const dependentApps: string[] = [];
|
|
96
|
+
|
|
97
|
+
for (const [appName, app] of Object.entries(workspace.apps)) {
|
|
98
|
+
if (app.type === 'frontend' && app.dependencies.includes(backendAppName)) {
|
|
99
|
+
dependentApps.push(appName);
|
|
100
|
+
}
|
|
84
101
|
}
|
|
85
102
|
|
|
86
|
-
|
|
103
|
+
return dependentApps;
|
|
104
|
+
}
|
|
87
105
|
|
|
88
|
-
|
|
106
|
+
/**
|
|
107
|
+
* Get the path to a backend's OpenAPI spec file.
|
|
108
|
+
*/
|
|
109
|
+
export function getBackendOpenApiPath(
|
|
110
|
+
workspace: NormalizedWorkspace,
|
|
111
|
+
backendAppName: string,
|
|
112
|
+
): string | null {
|
|
113
|
+
const app = workspace.apps[backendAppName];
|
|
114
|
+
if (!app || app.type !== 'backend') {
|
|
89
115
|
return null;
|
|
90
116
|
}
|
|
91
117
|
|
|
92
|
-
|
|
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
|
-
});
|
|
118
|
+
return join(workspace.root, app.path, '.gkm', 'openapi.ts');
|
|
119
|
+
}
|
|
100
120
|
|
|
101
|
-
|
|
121
|
+
/**
|
|
122
|
+
* Count endpoints in an OpenAPI spec content.
|
|
123
|
+
*/
|
|
124
|
+
function countEndpoints(content: string): number {
|
|
125
|
+
const endpointMatches = content.match(
|
|
126
|
+
/'(GET|POST|PUT|PATCH|DELETE)\s+\/[^']+'/g,
|
|
127
|
+
);
|
|
128
|
+
return endpointMatches?.length ?? 0;
|
|
102
129
|
}
|
|
103
130
|
|
|
104
131
|
/**
|
|
105
|
-
*
|
|
106
|
-
*
|
|
132
|
+
* Copy the OpenAPI client from a backend to all dependent frontend apps.
|
|
133
|
+
* Called when the backend's .gkm/openapi.ts file changes.
|
|
107
134
|
*/
|
|
108
|
-
export async function
|
|
135
|
+
export async function copyClientToFrontends(
|
|
109
136
|
workspace: NormalizedWorkspace,
|
|
110
|
-
|
|
111
|
-
options: {
|
|
112
|
-
): Promise<
|
|
113
|
-
const
|
|
114
|
-
const
|
|
137
|
+
backendAppName: string,
|
|
138
|
+
options: { silent?: boolean } = {},
|
|
139
|
+
): Promise<ClientCopyResult[]> {
|
|
140
|
+
const log = options.silent ? () => {} : logger.log.bind(logger);
|
|
141
|
+
const results: ClientCopyResult[] = [];
|
|
115
142
|
|
|
116
|
-
|
|
143
|
+
const backendApp = workspace.apps[backendAppName];
|
|
144
|
+
if (!backendApp || backendApp.type !== 'backend') {
|
|
117
145
|
return results;
|
|
118
146
|
}
|
|
119
147
|
|
|
120
|
-
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
148
|
+
// Get the backend's OpenAPI spec
|
|
149
|
+
const openApiPath = join(
|
|
150
|
+
workspace.root,
|
|
151
|
+
backendApp.path,
|
|
152
|
+
'.gkm',
|
|
153
|
+
'openapi.ts',
|
|
154
|
+
);
|
|
125
155
|
|
|
126
|
-
if (
|
|
156
|
+
if (!existsSync(openApiPath)) {
|
|
127
157
|
return results;
|
|
128
158
|
}
|
|
129
159
|
|
|
130
|
-
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
160
|
+
const content = await readFile(openApiPath, 'utf-8');
|
|
161
|
+
const endpointCount = countEndpoints(content);
|
|
162
|
+
|
|
163
|
+
// Get all frontends that depend on this backend
|
|
164
|
+
const dependentFrontends = getDependentFrontends(workspace, backendAppName);
|
|
134
165
|
|
|
135
|
-
for (const
|
|
136
|
-
const
|
|
166
|
+
for (const frontendAppName of dependentFrontends) {
|
|
167
|
+
const frontendApp = workspace.apps[frontendAppName];
|
|
168
|
+
if (!frontendApp || frontendApp.type !== 'frontend') {
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Check if frontend has client output configured
|
|
173
|
+
const clientOutput = frontendApp.client?.output;
|
|
174
|
+
if (!clientOutput) {
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const result: ClientCopyResult = {
|
|
137
179
|
frontendApp: frontendAppName,
|
|
138
180
|
backendApp: backendAppName,
|
|
139
181
|
outputPath: '',
|
|
140
|
-
endpointCount
|
|
141
|
-
|
|
182
|
+
endpointCount,
|
|
183
|
+
success: false,
|
|
142
184
|
};
|
|
143
185
|
|
|
144
186
|
try {
|
|
145
|
-
|
|
146
|
-
const
|
|
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
|
|
187
|
+
const frontendPath = join(workspace.root, frontendApp.path);
|
|
188
|
+
const outputDir = join(frontendPath, clientOutput);
|
|
168
189
|
await mkdir(outputDir, { recursive: true });
|
|
169
190
|
|
|
170
|
-
//
|
|
171
|
-
const fileName =
|
|
172
|
-
backendDeps.length === 1 ? 'openapi.ts' : `${backendAppName}-api.ts`;
|
|
191
|
+
// Use backend app name as filename
|
|
192
|
+
const fileName = `${backendAppName}.ts`;
|
|
173
193
|
const outputPath = join(outputDir, fileName);
|
|
174
194
|
|
|
175
195
|
// Add header comment with backend reference
|
|
176
196
|
const backendRelPath = relative(
|
|
177
197
|
dirname(outputPath),
|
|
178
|
-
join(workspace.root,
|
|
198
|
+
join(workspace.root, backendApp.path),
|
|
179
199
|
);
|
|
180
200
|
|
|
181
201
|
const clientContent = `/**
|
|
@@ -185,123 +205,43 @@ export async function generateClientForFrontend(
|
|
|
185
205
|
* DO NOT EDIT - This file is automatically regenerated when backend schemas change.
|
|
186
206
|
*/
|
|
187
207
|
|
|
188
|
-
${
|
|
208
|
+
${content}
|
|
189
209
|
`;
|
|
190
210
|
|
|
191
211
|
await writeFile(outputPath, clientContent);
|
|
192
212
|
|
|
193
|
-
// Update cache
|
|
194
|
-
specHashCache.set(cacheKey, newHash);
|
|
195
|
-
|
|
196
213
|
result.outputPath = outputPath;
|
|
197
|
-
result.
|
|
198
|
-
|
|
214
|
+
result.success = true;
|
|
215
|
+
|
|
216
|
+
log(
|
|
217
|
+
`📦 Copied client to ${frontendAppName} from ${backendAppName} (${endpointCount} endpoints)`,
|
|
218
|
+
);
|
|
199
219
|
} catch (error) {
|
|
200
|
-
result.
|
|
201
|
-
results.push(result);
|
|
220
|
+
result.error = (error as Error).message;
|
|
202
221
|
}
|
|
222
|
+
|
|
223
|
+
results.push(result);
|
|
203
224
|
}
|
|
204
225
|
|
|
205
226
|
return results;
|
|
206
227
|
}
|
|
207
228
|
|
|
208
229
|
/**
|
|
209
|
-
*
|
|
230
|
+
* Copy clients from all backends to their dependent frontends.
|
|
231
|
+
* Useful for initial setup or force refresh.
|
|
210
232
|
*/
|
|
211
|
-
export async function
|
|
233
|
+
export async function copyAllClients(
|
|
212
234
|
workspace: NormalizedWorkspace,
|
|
213
|
-
options: {
|
|
214
|
-
): Promise<
|
|
215
|
-
const
|
|
216
|
-
const allResults: ClientGenerationResult[] = [];
|
|
235
|
+
options: { silent?: boolean } = {},
|
|
236
|
+
): Promise<ClientCopyResult[]> {
|
|
237
|
+
const allResults: ClientCopyResult[] = [];
|
|
217
238
|
|
|
218
239
|
for (const [appName, app] of Object.entries(workspace.apps)) {
|
|
219
|
-
if (app.type === '
|
|
220
|
-
const results = await
|
|
221
|
-
|
|
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
|
-
}
|
|
240
|
+
if (app.type === 'backend' && app.routes) {
|
|
241
|
+
const results = await copyClientToFrontends(workspace, appName, options);
|
|
242
|
+
allResults.push(...results);
|
|
232
243
|
}
|
|
233
244
|
}
|
|
234
245
|
|
|
235
246
|
return allResults;
|
|
236
247
|
}
|
|
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
|
-
}
|