@geekmidas/cli 0.23.0 → 0.24.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/config-BogU0_oQ.mjs +189 -0
- package/dist/config-BogU0_oQ.mjs.map +1 -0
- package/dist/{config-CxrLu8ia.cjs → config-CTftATBX.cjs} +95 -9
- package/dist/config-CTftATBX.cjs.map +1 -0
- package/dist/config.cjs +3 -1
- package/dist/config.d.cts +36 -2
- package/dist/config.d.cts.map +1 -1
- package/dist/config.d.mts +36 -2
- package/dist/config.d.mts.map +1 -1
- package/dist/config.mjs +2 -2
- package/dist/index.cjs +71 -31
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +71 -31
- package/dist/index.mjs.map +1 -1
- package/dist/{openapi-DfpxS0xv.cjs → openapi-BrhkPKM7.cjs} +2 -2
- package/dist/{openapi-DfpxS0xv.cjs.map → openapi-BrhkPKM7.cjs.map} +1 -1
- package/dist/{openapi-CgqR6Jkw.mjs → openapi-DNbXfhXE.mjs} +2 -2
- package/dist/{openapi-CgqR6Jkw.mjs.map → openapi-DNbXfhXE.mjs.map} +1 -1
- package/dist/openapi.cjs +2 -2
- package/dist/openapi.mjs +2 -2
- package/package.json +4 -4
- package/src/__tests__/config.spec.ts +314 -2
- package/src/config.ts +151 -15
- package/src/dev/index.ts +78 -26
- package/src/init/generators/docker.ts +1 -1
- package/src/init/versions.ts +13 -2
- package/dist/config-BaYqrF3n.mjs +0 -115
- package/dist/config-BaYqrF3n.mjs.map +0 -1
- package/dist/config-CxrLu8ia.cjs.map +0 -1
package/dist/openapi.cjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env -S npx tsx
|
|
2
2
|
require('./workspace-iWgBlX6h.cjs');
|
|
3
|
-
require('./config-
|
|
4
|
-
const require_openapi = require('./openapi-
|
|
3
|
+
require('./config-CTftATBX.cjs');
|
|
4
|
+
const require_openapi = require('./openapi-BrhkPKM7.cjs');
|
|
5
5
|
|
|
6
6
|
exports.OPENAPI_OUTPUT_PATH = require_openapi.OPENAPI_OUTPUT_PATH;
|
|
7
7
|
exports.generateOpenApi = require_openapi.generateOpenApi;
|
package/dist/openapi.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env -S npx tsx
|
|
2
2
|
import "./workspace-CPLEZDZf.mjs";
|
|
3
|
-
import "./config-
|
|
4
|
-
import { OPENAPI_OUTPUT_PATH, generateOpenApi, openapiCommand, resolveOpenApiConfig } from "./openapi-
|
|
3
|
+
import "./config-BogU0_oQ.mjs";
|
|
4
|
+
import { OPENAPI_OUTPUT_PATH, generateOpenApi, openapiCommand, resolveOpenApiConfig } from "./openapi-DNbXfhXE.mjs";
|
|
5
5
|
|
|
6
6
|
export { OPENAPI_OUTPUT_PATH, generateOpenApi, openapiCommand, resolveOpenApiConfig };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@geekmidas/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.0",
|
|
4
4
|
"description": "CLI tools for building Lambda handlers, server applications, and generating OpenAPI specs",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -48,11 +48,11 @@
|
|
|
48
48
|
"lodash.kebabcase": "^4.1.1",
|
|
49
49
|
"openapi-typescript": "^7.4.2",
|
|
50
50
|
"prompts": "~2.4.2",
|
|
51
|
-
"@geekmidas/errors": "~0.1.0",
|
|
52
51
|
"@geekmidas/constructs": "~0.6.0",
|
|
53
|
-
"@geekmidas/
|
|
52
|
+
"@geekmidas/errors": "~0.1.0",
|
|
54
53
|
"@geekmidas/logger": "~0.4.0",
|
|
55
|
-
"@geekmidas/schema": "~0.1.0"
|
|
54
|
+
"@geekmidas/schema": "~0.1.0",
|
|
55
|
+
"@geekmidas/envkit": "~0.4.0"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
58
|
"@types/lodash.kebabcase": "^4.1.9",
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { realpathSync } from 'node:fs';
|
|
2
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
2
3
|
import { join } from 'node:path';
|
|
3
4
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
4
|
-
import { loadConfig } from '../config';
|
|
5
|
+
import { getAppNameFromCwd, loadAppConfig, loadConfig } from '../config';
|
|
5
6
|
import { cleanupDir, createTempDir } from './test-helpers';
|
|
6
7
|
|
|
7
8
|
describe('loadConfig', () => {
|
|
@@ -108,3 +109,314 @@ export default {
|
|
|
108
109
|
expect(config.routes).toBe('./ts-routes/**/*.ts');
|
|
109
110
|
});
|
|
110
111
|
});
|
|
112
|
+
|
|
113
|
+
describe('getAppNameFromCwd', () => {
|
|
114
|
+
let tempDir: string;
|
|
115
|
+
|
|
116
|
+
beforeEach(async () => {
|
|
117
|
+
tempDir = await createTempDir();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
afterEach(async () => {
|
|
121
|
+
await cleanupDir(tempDir);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('should return app name from package.json', async () => {
|
|
125
|
+
const packageJson = { name: 'my-app', version: '1.0.0' };
|
|
126
|
+
await writeFile(join(tempDir, 'package.json'), JSON.stringify(packageJson));
|
|
127
|
+
|
|
128
|
+
const appName = getAppNameFromCwd(tempDir);
|
|
129
|
+
|
|
130
|
+
expect(appName).toBe('my-app');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should extract name from scoped package', async () => {
|
|
134
|
+
const packageJson = { name: '@myorg/api', version: '1.0.0' };
|
|
135
|
+
await writeFile(join(tempDir, 'package.json'), JSON.stringify(packageJson));
|
|
136
|
+
|
|
137
|
+
const appName = getAppNameFromCwd(tempDir);
|
|
138
|
+
|
|
139
|
+
expect(appName).toBe('api');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('should handle scoped package with nested scope', async () => {
|
|
143
|
+
const packageJson = { name: '@my-company/auth-service', version: '1.0.0' };
|
|
144
|
+
await writeFile(join(tempDir, 'package.json'), JSON.stringify(packageJson));
|
|
145
|
+
|
|
146
|
+
const appName = getAppNameFromCwd(tempDir);
|
|
147
|
+
|
|
148
|
+
expect(appName).toBe('auth-service');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('should return null if package.json does not exist', async () => {
|
|
152
|
+
const appName = getAppNameFromCwd(tempDir);
|
|
153
|
+
|
|
154
|
+
expect(appName).toBeNull();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('should return null if package.json has no name field', async () => {
|
|
158
|
+
const packageJson = { version: '1.0.0' };
|
|
159
|
+
await writeFile(join(tempDir, 'package.json'), JSON.stringify(packageJson));
|
|
160
|
+
|
|
161
|
+
const appName = getAppNameFromCwd(tempDir);
|
|
162
|
+
|
|
163
|
+
expect(appName).toBeNull();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('should return null if package.json is invalid JSON', async () => {
|
|
167
|
+
await writeFile(join(tempDir, 'package.json'), 'not valid json');
|
|
168
|
+
|
|
169
|
+
const appName = getAppNameFromCwd(tempDir);
|
|
170
|
+
|
|
171
|
+
expect(appName).toBeNull();
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
describe('loadAppConfig', () => {
|
|
176
|
+
let tempDir: string;
|
|
177
|
+
let originalCwd: string;
|
|
178
|
+
|
|
179
|
+
beforeEach(async () => {
|
|
180
|
+
tempDir = await createTempDir();
|
|
181
|
+
originalCwd = process.cwd();
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
afterEach(async () => {
|
|
185
|
+
process.chdir(originalCwd);
|
|
186
|
+
// Clean up GKM_CONFIG_PATH env var
|
|
187
|
+
delete process.env.GKM_CONFIG_PATH;
|
|
188
|
+
await cleanupDir(tempDir);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('should load app config from workspace when in app directory', async () => {
|
|
192
|
+
// Create workspace structure
|
|
193
|
+
const workspaceRoot = tempDir;
|
|
194
|
+
const appDir = join(workspaceRoot, 'apps', 'api');
|
|
195
|
+
await mkdir(appDir, { recursive: true });
|
|
196
|
+
|
|
197
|
+
// Create workspace config (plain JS object with __isWorkspace marker)
|
|
198
|
+
const workspaceConfig = `
|
|
199
|
+
export default {
|
|
200
|
+
__isWorkspace: true,
|
|
201
|
+
name: 'test-workspace',
|
|
202
|
+
apps: {
|
|
203
|
+
api: {
|
|
204
|
+
type: 'backend',
|
|
205
|
+
path: 'apps/api',
|
|
206
|
+
port: 3000,
|
|
207
|
+
routes: './src/endpoints/**/*.ts',
|
|
208
|
+
envParser: './src/config/env',
|
|
209
|
+
logger: './src/config/logger',
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
`;
|
|
214
|
+
await writeFile(join(workspaceRoot, 'gkm.config.ts'), workspaceConfig);
|
|
215
|
+
|
|
216
|
+
// Create app package.json
|
|
217
|
+
const packageJson = { name: '@test-workspace/api', version: '1.0.0' };
|
|
218
|
+
await writeFile(join(appDir, 'package.json'), JSON.stringify(packageJson));
|
|
219
|
+
|
|
220
|
+
// Change to app directory
|
|
221
|
+
process.chdir(appDir);
|
|
222
|
+
|
|
223
|
+
const result = await loadAppConfig();
|
|
224
|
+
|
|
225
|
+
expect(result.appName).toBe('api');
|
|
226
|
+
expect(result.gkmConfig.routes).toBe('./src/endpoints/**/*.ts');
|
|
227
|
+
// Use realpathSync to handle macOS /var -> /private/var symlink
|
|
228
|
+
expect(realpathSync(result.appRoot)).toBe(realpathSync(appDir));
|
|
229
|
+
expect(realpathSync(result.workspaceRoot)).toBe(
|
|
230
|
+
realpathSync(workspaceRoot),
|
|
231
|
+
);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('should throw error if app not found in workspace', async () => {
|
|
235
|
+
// Create workspace structure
|
|
236
|
+
const workspaceRoot = tempDir;
|
|
237
|
+
const appDir = join(workspaceRoot, 'apps', 'unknown');
|
|
238
|
+
await mkdir(appDir, { recursive: true });
|
|
239
|
+
|
|
240
|
+
// Create workspace config without 'unknown' app
|
|
241
|
+
const workspaceConfig = `
|
|
242
|
+
export default {
|
|
243
|
+
__isWorkspace: true,
|
|
244
|
+
name: 'test-workspace',
|
|
245
|
+
apps: {
|
|
246
|
+
api: {
|
|
247
|
+
type: 'backend',
|
|
248
|
+
path: 'apps/api',
|
|
249
|
+
port: 3000,
|
|
250
|
+
routes: './src/endpoints/**/*.ts',
|
|
251
|
+
envParser: './src/config/env',
|
|
252
|
+
logger: './src/config/logger',
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
};
|
|
256
|
+
`;
|
|
257
|
+
await writeFile(join(workspaceRoot, 'gkm.config.ts'), workspaceConfig);
|
|
258
|
+
|
|
259
|
+
// Create app package.json with different name
|
|
260
|
+
const packageJson = { name: '@test-workspace/unknown', version: '1.0.0' };
|
|
261
|
+
await writeFile(join(appDir, 'package.json'), JSON.stringify(packageJson));
|
|
262
|
+
|
|
263
|
+
// Change to app directory
|
|
264
|
+
process.chdir(appDir);
|
|
265
|
+
|
|
266
|
+
await expect(loadAppConfig()).rejects.toThrow(
|
|
267
|
+
'App "unknown" not found in workspace config',
|
|
268
|
+
);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it('should throw error if no package.json exists', async () => {
|
|
272
|
+
// Create workspace structure without package.json in app
|
|
273
|
+
const workspaceRoot = tempDir;
|
|
274
|
+
const appDir = join(workspaceRoot, 'apps', 'api');
|
|
275
|
+
await mkdir(appDir, { recursive: true });
|
|
276
|
+
|
|
277
|
+
const workspaceConfig = `
|
|
278
|
+
export default {
|
|
279
|
+
routes: './src/endpoints/**/*.ts',
|
|
280
|
+
envParser: './src/config/env',
|
|
281
|
+
logger: './src/config/logger',
|
|
282
|
+
};
|
|
283
|
+
`;
|
|
284
|
+
await writeFile(join(workspaceRoot, 'gkm.config.ts'), workspaceConfig);
|
|
285
|
+
|
|
286
|
+
// Change to app directory (no package.json)
|
|
287
|
+
process.chdir(appDir);
|
|
288
|
+
|
|
289
|
+
await expect(loadAppConfig()).rejects.toThrow(
|
|
290
|
+
'Could not determine app name',
|
|
291
|
+
);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it('should use GKM_CONFIG_PATH env var when set', async () => {
|
|
295
|
+
// Create workspace at a different location
|
|
296
|
+
const workspaceRoot = await createTempDir('workspace-');
|
|
297
|
+
const configPath = join(workspaceRoot, 'gkm.config.ts');
|
|
298
|
+
|
|
299
|
+
// Create workspace config
|
|
300
|
+
const workspaceConfig = `
|
|
301
|
+
export default {
|
|
302
|
+
__isWorkspace: true,
|
|
303
|
+
name: 'env-test',
|
|
304
|
+
apps: {
|
|
305
|
+
api: {
|
|
306
|
+
type: 'backend',
|
|
307
|
+
path: 'apps/api',
|
|
308
|
+
port: 3000,
|
|
309
|
+
routes: './src/endpoints/**/*.ts',
|
|
310
|
+
envParser: './src/config/env',
|
|
311
|
+
logger: './src/config/logger',
|
|
312
|
+
},
|
|
313
|
+
},
|
|
314
|
+
};
|
|
315
|
+
`;
|
|
316
|
+
await writeFile(configPath, workspaceConfig);
|
|
317
|
+
|
|
318
|
+
// Create app directory in temp dir (separate from workspace)
|
|
319
|
+
const appDir = join(tempDir, 'apps', 'api');
|
|
320
|
+
await mkdir(appDir, { recursive: true });
|
|
321
|
+
|
|
322
|
+
// Create app package.json
|
|
323
|
+
const packageJson = { name: '@env-test/api', version: '1.0.0' };
|
|
324
|
+
await writeFile(join(appDir, 'package.json'), JSON.stringify(packageJson));
|
|
325
|
+
|
|
326
|
+
// Set GKM_CONFIG_PATH
|
|
327
|
+
process.env.GKM_CONFIG_PATH = configPath;
|
|
328
|
+
|
|
329
|
+
// Change to app directory
|
|
330
|
+
process.chdir(appDir);
|
|
331
|
+
|
|
332
|
+
const result = await loadAppConfig();
|
|
333
|
+
|
|
334
|
+
expect(result.appName).toBe('api');
|
|
335
|
+
// Use realpathSync to handle macOS /var -> /private/var symlink
|
|
336
|
+
expect(realpathSync(result.workspaceRoot)).toBe(
|
|
337
|
+
realpathSync(workspaceRoot),
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
// Cleanup the extra temp dir
|
|
341
|
+
await cleanupDir(workspaceRoot);
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
describe('config discovery', () => {
|
|
346
|
+
let tempDir: string;
|
|
347
|
+
let originalCwd: string;
|
|
348
|
+
|
|
349
|
+
beforeEach(async () => {
|
|
350
|
+
tempDir = await createTempDir();
|
|
351
|
+
originalCwd = process.cwd();
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
afterEach(async () => {
|
|
355
|
+
process.chdir(originalCwd);
|
|
356
|
+
delete process.env.GKM_CONFIG_PATH;
|
|
357
|
+
await cleanupDir(tempDir);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it('should find config by walking up directories', async () => {
|
|
361
|
+
// Create nested directory structure
|
|
362
|
+
const nestedDir = join(tempDir, 'apps', 'api', 'src', 'endpoints');
|
|
363
|
+
await mkdir(nestedDir, { recursive: true });
|
|
364
|
+
|
|
365
|
+
// Create config at root
|
|
366
|
+
const configContent = `
|
|
367
|
+
export default {
|
|
368
|
+
routes: './src/endpoints/**/*.ts',
|
|
369
|
+
envParser: './src/config/env',
|
|
370
|
+
logger: './src/config/logger',
|
|
371
|
+
};
|
|
372
|
+
`;
|
|
373
|
+
await writeFile(join(tempDir, 'gkm.config.ts'), configContent);
|
|
374
|
+
|
|
375
|
+
// Change to deeply nested directory
|
|
376
|
+
process.chdir(nestedDir);
|
|
377
|
+
|
|
378
|
+
const config = await loadConfig();
|
|
379
|
+
|
|
380
|
+
expect(config.routes).toBe('./src/endpoints/**/*.ts');
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
it('should prefer GKM_CONFIG_PATH over walking up directories', async () => {
|
|
384
|
+
// Create two configs at different levels
|
|
385
|
+
const nestedDir = join(tempDir, 'apps', 'api');
|
|
386
|
+
await mkdir(nestedDir, { recursive: true });
|
|
387
|
+
|
|
388
|
+
// Config at root
|
|
389
|
+
const rootConfig = `
|
|
390
|
+
export default {
|
|
391
|
+
routes: './root-routes/**/*.ts',
|
|
392
|
+
envParser: './src/config/env',
|
|
393
|
+
logger: './src/config/logger',
|
|
394
|
+
};
|
|
395
|
+
`;
|
|
396
|
+
await writeFile(join(tempDir, 'gkm.config.ts'), rootConfig);
|
|
397
|
+
|
|
398
|
+
// Config in a different location pointed to by env var
|
|
399
|
+
const envConfigDir = await createTempDir('env-config-');
|
|
400
|
+
const envConfig = `
|
|
401
|
+
export default {
|
|
402
|
+
routes: './env-routes/**/*.ts',
|
|
403
|
+
envParser: './src/config/env',
|
|
404
|
+
logger: './src/config/logger',
|
|
405
|
+
};
|
|
406
|
+
`;
|
|
407
|
+
await writeFile(join(envConfigDir, 'gkm.config.ts'), envConfig);
|
|
408
|
+
|
|
409
|
+
// Set GKM_CONFIG_PATH to the env config
|
|
410
|
+
process.env.GKM_CONFIG_PATH = join(envConfigDir, 'gkm.config.ts');
|
|
411
|
+
|
|
412
|
+
// Change to nested directory
|
|
413
|
+
process.chdir(nestedDir);
|
|
414
|
+
|
|
415
|
+
const config = await loadConfig();
|
|
416
|
+
|
|
417
|
+
expect(config.routes).toBe('./env-routes/**/*.ts');
|
|
418
|
+
|
|
419
|
+
// Cleanup
|
|
420
|
+
await cleanupDir(envConfigDir);
|
|
421
|
+
});
|
|
422
|
+
});
|
package/src/config.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs';
|
|
2
|
-
import { join } from 'node:path';
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join, parse } from 'node:path';
|
|
3
3
|
import type { GkmConfig } from './types.js';
|
|
4
4
|
import {
|
|
5
|
+
getAppGkmConfig,
|
|
5
6
|
isWorkspaceConfig,
|
|
6
7
|
type LoadedConfig,
|
|
8
|
+
type NormalizedAppConfig,
|
|
9
|
+
type NormalizedWorkspace,
|
|
7
10
|
processConfig,
|
|
8
11
|
type WorkspaceConfig,
|
|
9
12
|
} from './workspace/index.js';
|
|
@@ -70,17 +73,45 @@ export function parseModuleConfig(
|
|
|
70
73
|
return { path, importPattern };
|
|
71
74
|
}
|
|
72
75
|
|
|
76
|
+
export interface ConfigDiscoveryResult {
|
|
77
|
+
configPath: string;
|
|
78
|
+
workspaceRoot: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
73
81
|
/**
|
|
74
82
|
* Find and return the path to the config file.
|
|
83
|
+
*
|
|
84
|
+
* Resolution order:
|
|
85
|
+
* 1. GKM_CONFIG_PATH env var (set by workspace dev command)
|
|
86
|
+
* 2. Walk up directory tree from cwd
|
|
75
87
|
*/
|
|
76
|
-
function findConfigPath(cwd: string):
|
|
88
|
+
function findConfigPath(cwd: string): ConfigDiscoveryResult {
|
|
77
89
|
const files = ['gkm.config.json', 'gkm.config.ts', 'gkm.config.js'];
|
|
78
90
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
91
|
+
// Check GKM_CONFIG_PATH env var first (set by workspace dev command)
|
|
92
|
+
const envConfigPath = process.env.GKM_CONFIG_PATH;
|
|
93
|
+
if (envConfigPath && existsSync(envConfigPath)) {
|
|
94
|
+
return {
|
|
95
|
+
configPath: envConfigPath,
|
|
96
|
+
workspaceRoot: dirname(envConfigPath),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Walk up directory tree to find config
|
|
101
|
+
let currentDir = cwd;
|
|
102
|
+
const { root } = parse(currentDir);
|
|
103
|
+
|
|
104
|
+
while (currentDir !== root) {
|
|
105
|
+
for (const file of files) {
|
|
106
|
+
const configPath = join(currentDir, file);
|
|
107
|
+
if (existsSync(configPath)) {
|
|
108
|
+
return {
|
|
109
|
+
configPath,
|
|
110
|
+
workspaceRoot: currentDir,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
83
113
|
}
|
|
114
|
+
currentDir = dirname(currentDir);
|
|
84
115
|
}
|
|
85
116
|
|
|
86
117
|
throw new Error(
|
|
@@ -88,17 +119,58 @@ function findConfigPath(cwd: string): string {
|
|
|
88
119
|
);
|
|
89
120
|
}
|
|
90
121
|
|
|
122
|
+
/**
|
|
123
|
+
* Get app name from package.json in the given directory.
|
|
124
|
+
* Handles scoped packages by extracting the name after the scope.
|
|
125
|
+
*
|
|
126
|
+
* @example
|
|
127
|
+
* getAppNameFromCwd('/path/to/apps/api')
|
|
128
|
+
* // package.json: { "name": "@myorg/api" }
|
|
129
|
+
* // Returns: 'api'
|
|
130
|
+
*/
|
|
131
|
+
export function getAppNameFromCwd(cwd: string = process.cwd()): string | null {
|
|
132
|
+
const packageJsonPath = join(cwd, 'package.json');
|
|
133
|
+
|
|
134
|
+
if (!existsSync(packageJsonPath)) {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
|
|
140
|
+
const name = packageJson.name as string | undefined;
|
|
141
|
+
|
|
142
|
+
if (!name) {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Handle scoped packages: @scope/name -> name
|
|
147
|
+
if (name.startsWith('@') && name.includes('/')) {
|
|
148
|
+
return name.split('/')[1] ?? null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return name;
|
|
152
|
+
} catch {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
interface RawConfigResult {
|
|
158
|
+
config: GkmConfig | WorkspaceConfig;
|
|
159
|
+
workspaceRoot: string;
|
|
160
|
+
}
|
|
161
|
+
|
|
91
162
|
/**
|
|
92
163
|
* Load raw configuration from file.
|
|
93
164
|
*/
|
|
94
|
-
async function loadRawConfig(
|
|
95
|
-
|
|
96
|
-
): Promise<GkmConfig | WorkspaceConfig> {
|
|
97
|
-
const configPath = findConfigPath(cwd);
|
|
165
|
+
async function loadRawConfig(cwd: string): Promise<RawConfigResult> {
|
|
166
|
+
const { configPath, workspaceRoot } = findConfigPath(cwd);
|
|
98
167
|
|
|
99
168
|
try {
|
|
100
169
|
const config = await import(configPath);
|
|
101
|
-
return
|
|
170
|
+
return {
|
|
171
|
+
config: config.default,
|
|
172
|
+
workspaceRoot,
|
|
173
|
+
};
|
|
102
174
|
} catch (error) {
|
|
103
175
|
throw new Error(`Failed to load config: ${(error as Error).message}`);
|
|
104
176
|
}
|
|
@@ -113,7 +185,7 @@ async function loadRawConfig(
|
|
|
113
185
|
export async function loadConfig(
|
|
114
186
|
cwd: string = process.cwd(),
|
|
115
187
|
): Promise<GkmConfig> {
|
|
116
|
-
const config = await loadRawConfig(cwd);
|
|
188
|
+
const { config } = await loadRawConfig(cwd);
|
|
117
189
|
|
|
118
190
|
// If it's a workspace config, throw an error
|
|
119
191
|
if (isWorkspaceConfig(config)) {
|
|
@@ -145,6 +217,70 @@ export async function loadConfig(
|
|
|
145
217
|
export async function loadWorkspaceConfig(
|
|
146
218
|
cwd: string = process.cwd(),
|
|
147
219
|
): Promise<LoadedConfig> {
|
|
148
|
-
const config = await loadRawConfig(cwd);
|
|
149
|
-
return processConfig(config,
|
|
220
|
+
const { config, workspaceRoot } = await loadRawConfig(cwd);
|
|
221
|
+
return processConfig(config, workspaceRoot);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export interface AppConfigResult {
|
|
225
|
+
appName: string;
|
|
226
|
+
app: NormalizedAppConfig;
|
|
227
|
+
gkmConfig: GkmConfig;
|
|
228
|
+
workspace: NormalizedWorkspace;
|
|
229
|
+
workspaceRoot: string;
|
|
230
|
+
appRoot: string;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Load app-specific configuration from workspace.
|
|
235
|
+
* Uses the app name from package.json to find the correct app config.
|
|
236
|
+
*
|
|
237
|
+
* @example
|
|
238
|
+
* ```ts
|
|
239
|
+
* // From apps/api directory with package.json: { "name": "@myorg/api" }
|
|
240
|
+
* const { app, workspace, workspaceRoot } = await loadAppConfig();
|
|
241
|
+
* console.log(app.routes); // './src/endpoints/**\/*.ts'
|
|
242
|
+
* ```
|
|
243
|
+
*/
|
|
244
|
+
export async function loadAppConfig(
|
|
245
|
+
cwd: string = process.cwd(),
|
|
246
|
+
): Promise<AppConfigResult> {
|
|
247
|
+
const appName = getAppNameFromCwd(cwd);
|
|
248
|
+
|
|
249
|
+
if (!appName) {
|
|
250
|
+
throw new Error(
|
|
251
|
+
'Could not determine app name. Ensure package.json exists with a "name" field.',
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const { config, workspaceRoot } = await loadRawConfig(cwd);
|
|
256
|
+
const loadedConfig = processConfig(config, workspaceRoot);
|
|
257
|
+
|
|
258
|
+
// Find the app in workspace (apps is a Record<string, NormalizedAppConfig>)
|
|
259
|
+
const app = loadedConfig.workspace.apps[appName];
|
|
260
|
+
|
|
261
|
+
if (!app) {
|
|
262
|
+
const availableApps = Object.keys(loadedConfig.workspace.apps).join(', ');
|
|
263
|
+
throw new Error(
|
|
264
|
+
`App "${appName}" not found in workspace config. Available apps: ${availableApps}. ` +
|
|
265
|
+
`Ensure the package.json name matches the app key in gkm.config.ts.`,
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Get the app's GKM config using the helper
|
|
270
|
+
const gkmConfig = getAppGkmConfig(loadedConfig.workspace, appName);
|
|
271
|
+
|
|
272
|
+
if (!gkmConfig) {
|
|
273
|
+
throw new Error(
|
|
274
|
+
`App "${appName}" is not a backend app and cannot be run with gkm dev.`,
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return {
|
|
279
|
+
appName,
|
|
280
|
+
app,
|
|
281
|
+
gkmConfig,
|
|
282
|
+
workspace: loadedConfig.workspace,
|
|
283
|
+
workspaceRoot,
|
|
284
|
+
appRoot: join(workspaceRoot, app.path),
|
|
285
|
+
};
|
|
150
286
|
}
|