@geekmidas/cli 0.7.0 → 0.8.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.d.cts +1 -1
- package/dist/config.d.mts +1 -1
- package/dist/index.cjs +49 -27
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +49 -27
- package/dist/index.mjs.map +1 -1
- package/dist/{openapi-Mwy2_R4W.mjs → openapi--vOy9mo4.mjs} +23 -2
- package/dist/openapi--vOy9mo4.mjs.map +1 -0
- package/dist/{openapi-tAIbJJU_.cjs → openapi-CHhTPief.cjs} +23 -2
- package/dist/openapi-CHhTPief.cjs.map +1 -0
- package/dist/openapi.cjs +1 -1
- package/dist/openapi.d.cts +1 -1
- package/dist/openapi.d.mts +1 -1
- package/dist/openapi.mjs +1 -1
- package/dist/{types-B3TXoj7v.d.mts → types-DXgiA1sF.d.mts} +38 -1
- package/dist/{types-C0hwnSjm.d.cts → types-b-vwGpqc.d.cts} +38 -1
- package/package.json +4 -4
- package/src/__tests__/EndpointGenerator.hooks.spec.ts +204 -0
- package/src/__tests__/normalizeHooksConfig.spec.ts +63 -0
- package/src/build/index.ts +8 -1
- package/src/build/types.ts +6 -0
- package/src/dev/index.ts +82 -29
- package/src/generators/EndpointGenerator.ts +27 -1
- package/src/types.ts +38 -0
- package/dist/openapi-Mwy2_R4W.mjs.map +0 -1
- package/dist/openapi-tAIbJJU_.cjs.map +0 -1
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { mkdir, readFile, rm } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { e } from '@geekmidas/constructs/endpoints';
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
5
|
+
import type { BuildContext } from '../build/types';
|
|
6
|
+
import { EndpointGenerator } from '../generators/EndpointGenerator';
|
|
7
|
+
import type { GeneratedConstruct } from '../generators/Generator';
|
|
8
|
+
|
|
9
|
+
// Create a minimal mock endpoint for testing
|
|
10
|
+
const mockEndpoint = e.get('/test').handle(async () => ({ ok: true }));
|
|
11
|
+
|
|
12
|
+
describe('EndpointGenerator hooks generation', () => {
|
|
13
|
+
const testOutputDir = join(process.cwd(), '.test-output');
|
|
14
|
+
let generator: EndpointGenerator;
|
|
15
|
+
|
|
16
|
+
// Mock endpoint construct for testing
|
|
17
|
+
const mockConstruct: GeneratedConstruct<typeof mockEndpoint> = {
|
|
18
|
+
key: 'testEndpoint',
|
|
19
|
+
construct: mockEndpoint,
|
|
20
|
+
path: {
|
|
21
|
+
relative: '/project/src/endpoints/test.ts',
|
|
22
|
+
absolute: '/project/src/endpoints/test.ts',
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
beforeEach(async () => {
|
|
27
|
+
generator = new EndpointGenerator();
|
|
28
|
+
await mkdir(testOutputDir, { recursive: true });
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
afterEach(async () => {
|
|
32
|
+
await rm(testOutputDir, { recursive: true, force: true });
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const baseContext: BuildContext = {
|
|
36
|
+
envParserPath: '/project/src/config/env.ts',
|
|
37
|
+
envParserImportPattern: '{ envParser }',
|
|
38
|
+
loggerPath: '/project/src/config/logger.ts',
|
|
39
|
+
loggerImportPattern: 'logger',
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
describe('generateAppFile', () => {
|
|
43
|
+
it('should not include hooks when hooks config is undefined', async () => {
|
|
44
|
+
await generator.build(baseContext, [mockConstruct], testOutputDir, {
|
|
45
|
+
provider: 'server',
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const appContent = await readFile(join(testOutputDir, 'app.ts'), 'utf-8');
|
|
49
|
+
|
|
50
|
+
expect(appContent).not.toContain('serverHooks');
|
|
51
|
+
expect(appContent).not.toContain('beforeSetup');
|
|
52
|
+
expect(appContent).not.toContain('afterSetup');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should include hooks imports when hooks config is provided', async () => {
|
|
56
|
+
const contextWithHooks: BuildContext = {
|
|
57
|
+
...baseContext,
|
|
58
|
+
hooks: {
|
|
59
|
+
serverHooksPath: '/project/src/config/hooks.ts',
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
await generator.build(contextWithHooks, [mockConstruct], testOutputDir, {
|
|
64
|
+
provider: 'server',
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const appContent = await readFile(join(testOutputDir, 'app.ts'), 'utf-8');
|
|
68
|
+
|
|
69
|
+
expect(appContent).toContain('import * as serverHooks from');
|
|
70
|
+
expect(appContent).toContain('hooks.ts');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('should include beforeSetup hook call', async () => {
|
|
74
|
+
const contextWithHooks: BuildContext = {
|
|
75
|
+
...baseContext,
|
|
76
|
+
hooks: {
|
|
77
|
+
serverHooksPath: '/project/src/config/hooks.ts',
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
await generator.build(contextWithHooks, [mockConstruct], testOutputDir, {
|
|
82
|
+
provider: 'server',
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const appContent = await readFile(join(testOutputDir, 'app.ts'), 'utf-8');
|
|
86
|
+
|
|
87
|
+
expect(appContent).toContain(
|
|
88
|
+
"if (typeof serverHooks.beforeSetup === 'function')",
|
|
89
|
+
);
|
|
90
|
+
expect(appContent).toContain(
|
|
91
|
+
'await serverHooks.beforeSetup(honoApp, { envParser, logger })',
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should include afterSetup hook call', async () => {
|
|
96
|
+
const contextWithHooks: BuildContext = {
|
|
97
|
+
...baseContext,
|
|
98
|
+
hooks: {
|
|
99
|
+
serverHooksPath: '/project/src/config/hooks.ts',
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
await generator.build(contextWithHooks, [mockConstruct], testOutputDir, {
|
|
104
|
+
provider: 'server',
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const appContent = await readFile(join(testOutputDir, 'app.ts'), 'utf-8');
|
|
108
|
+
|
|
109
|
+
expect(appContent).toContain(
|
|
110
|
+
"if (typeof serverHooks.afterSetup === 'function')",
|
|
111
|
+
);
|
|
112
|
+
expect(appContent).toContain(
|
|
113
|
+
'await serverHooks.afterSetup(honoApp, { envParser, logger })',
|
|
114
|
+
);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('should place telescope before beforeSetup, and beforeSetup before endpoints', async () => {
|
|
118
|
+
const contextWithHooks: BuildContext = {
|
|
119
|
+
...baseContext,
|
|
120
|
+
hooks: {
|
|
121
|
+
serverHooksPath: '/project/src/config/hooks.ts',
|
|
122
|
+
},
|
|
123
|
+
telescope: {
|
|
124
|
+
enabled: true,
|
|
125
|
+
path: '/__telescope',
|
|
126
|
+
maxEntries: 100,
|
|
127
|
+
recordBody: true,
|
|
128
|
+
ignore: [],
|
|
129
|
+
websocket: false,
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
await generator.build(contextWithHooks, [mockConstruct], testOutputDir, {
|
|
134
|
+
provider: 'server',
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const appContent = await readFile(join(testOutputDir, 'app.ts'), 'utf-8');
|
|
138
|
+
|
|
139
|
+
// Use specific patterns to find the actual calls (not imports/comments)
|
|
140
|
+
const telescopeIndex = appContent.indexOf('createMiddleware(telescope)');
|
|
141
|
+
const beforeSetupCallIndex = appContent.indexOf(
|
|
142
|
+
'serverHooks.beforeSetup(honoApp',
|
|
143
|
+
);
|
|
144
|
+
const setupEndpointsIndex = appContent.indexOf(
|
|
145
|
+
'await setupEndpoints(honoApp',
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
expect(telescopeIndex).toBeGreaterThan(0);
|
|
149
|
+
expect(beforeSetupCallIndex).toBeGreaterThan(0);
|
|
150
|
+
expect(setupEndpointsIndex).toBeGreaterThan(0);
|
|
151
|
+
// Telescope middleware first (to capture all requests)
|
|
152
|
+
expect(telescopeIndex).toBeLessThan(beforeSetupCallIndex);
|
|
153
|
+
// Then beforeSetup hook
|
|
154
|
+
expect(beforeSetupCallIndex).toBeLessThan(setupEndpointsIndex);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('should place afterSetup after setupEndpoints', async () => {
|
|
158
|
+
const contextWithHooks: BuildContext = {
|
|
159
|
+
...baseContext,
|
|
160
|
+
hooks: {
|
|
161
|
+
serverHooksPath: '/project/src/config/hooks.ts',
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
await generator.build(contextWithHooks, [mockConstruct], testOutputDir, {
|
|
166
|
+
provider: 'server',
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const appContent = await readFile(join(testOutputDir, 'app.ts'), 'utf-8');
|
|
170
|
+
|
|
171
|
+
// Use specific patterns to find the actual calls (not imports/comments)
|
|
172
|
+
const setupEndpointsIndex = appContent.indexOf(
|
|
173
|
+
'await setupEndpoints(honoApp',
|
|
174
|
+
);
|
|
175
|
+
const afterSetupCallIndex = appContent.indexOf(
|
|
176
|
+
'serverHooks.afterSetup(honoApp',
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
expect(setupEndpointsIndex).toBeGreaterThan(0);
|
|
180
|
+
expect(afterSetupCallIndex).toBeGreaterThan(0);
|
|
181
|
+
expect(afterSetupCallIndex).toBeGreaterThan(setupEndpointsIndex);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('should generate correct relative import path for hooks', async () => {
|
|
185
|
+
const contextWithHooks: BuildContext = {
|
|
186
|
+
...baseContext,
|
|
187
|
+
hooks: {
|
|
188
|
+
serverHooksPath: join(testOutputDir, '../../src/config/hooks.ts'),
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
await generator.build(contextWithHooks, [mockConstruct], testOutputDir, {
|
|
193
|
+
provider: 'server',
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const appContent = await readFile(join(testOutputDir, 'app.ts'), 'utf-8');
|
|
197
|
+
|
|
198
|
+
// Should have a relative import path
|
|
199
|
+
expect(appContent).toMatch(
|
|
200
|
+
/import \* as serverHooks from '\.\.\/.*hooks\.ts'/,
|
|
201
|
+
);
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { normalizeHooksConfig } from '../dev';
|
|
3
|
+
|
|
4
|
+
describe('normalizeHooksConfig', () => {
|
|
5
|
+
const originalCwd = process.cwd();
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
vi.spyOn(process, 'cwd').mockReturnValue('/test/project');
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
vi.restoreAllMocks();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('should return undefined when hooks config is undefined', () => {
|
|
16
|
+
const result = normalizeHooksConfig(undefined);
|
|
17
|
+
expect(result).toBeUndefined();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('should return undefined when hooks config is empty object', () => {
|
|
21
|
+
const result = normalizeHooksConfig({});
|
|
22
|
+
expect(result).toBeUndefined();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should return undefined when server hooks path is not provided', () => {
|
|
26
|
+
const result = normalizeHooksConfig({ server: undefined });
|
|
27
|
+
expect(result).toBeUndefined();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should normalize path with .ts extension', () => {
|
|
31
|
+
const result = normalizeHooksConfig({ server: './src/hooks.ts' });
|
|
32
|
+
|
|
33
|
+
expect(result).toBeDefined();
|
|
34
|
+
expect(result!.serverHooksPath).toBe('/test/project/src/hooks.ts');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should add .ts extension when missing', () => {
|
|
38
|
+
const result = normalizeHooksConfig({ server: './src/hooks' });
|
|
39
|
+
|
|
40
|
+
expect(result).toBeDefined();
|
|
41
|
+
expect(result!.serverHooksPath).toBe('/test/project/src/hooks.ts');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should resolve relative paths from cwd', () => {
|
|
45
|
+
const result = normalizeHooksConfig({ server: 'config/server-hooks' });
|
|
46
|
+
|
|
47
|
+
expect(result).toBeDefined();
|
|
48
|
+
expect(result!.serverHooksPath).toBe(
|
|
49
|
+
'/test/project/config/server-hooks.ts',
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should handle nested directory paths', () => {
|
|
54
|
+
const result = normalizeHooksConfig({
|
|
55
|
+
server: './src/config/hooks/server',
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
expect(result).toBeDefined();
|
|
59
|
+
expect(result!.serverHooksPath).toBe(
|
|
60
|
+
'/test/project/src/config/hooks/server.ts',
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
});
|
package/src/build/index.ts
CHANGED
|
@@ -5,7 +5,7 @@ import type { Endpoint } from '@geekmidas/constructs/endpoints';
|
|
|
5
5
|
import type { Function } from '@geekmidas/constructs/functions';
|
|
6
6
|
import type { Subscriber } from '@geekmidas/constructs/subscribers';
|
|
7
7
|
import { loadConfig, parseModuleConfig } from '../config';
|
|
8
|
-
import { normalizeTelescopeConfig } from '../dev';
|
|
8
|
+
import { normalizeHooksConfig, normalizeTelescopeConfig } from '../dev';
|
|
9
9
|
import {
|
|
10
10
|
CronGenerator,
|
|
11
11
|
EndpointGenerator,
|
|
@@ -55,12 +55,19 @@ export async function buildCommand(options: BuildOptions): Promise<void> {
|
|
|
55
55
|
logger.log(`🔭 Telescope enabled at ${telescope.path}`);
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
// Normalize hooks configuration
|
|
59
|
+
const hooks = normalizeHooksConfig(config.hooks);
|
|
60
|
+
if (hooks) {
|
|
61
|
+
logger.log(`🪝 Server hooks enabled`);
|
|
62
|
+
}
|
|
63
|
+
|
|
58
64
|
const buildContext: BuildContext = {
|
|
59
65
|
envParserPath,
|
|
60
66
|
envParserImportPattern,
|
|
61
67
|
loggerPath,
|
|
62
68
|
loggerImportPattern,
|
|
63
69
|
telescope,
|
|
70
|
+
hooks,
|
|
64
71
|
};
|
|
65
72
|
|
|
66
73
|
// Initialize generators
|
package/src/build/types.ts
CHANGED
|
@@ -50,6 +50,11 @@ export interface NormalizedStudioConfig {
|
|
|
50
50
|
schema: string;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
export interface NormalizedHooksConfig {
|
|
54
|
+
/** Path to server hooks module */
|
|
55
|
+
serverHooksPath: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
53
58
|
export interface BuildContext {
|
|
54
59
|
envParserPath: string;
|
|
55
60
|
envParserImportPattern: string;
|
|
@@ -57,6 +62,7 @@ export interface BuildContext {
|
|
|
57
62
|
loggerImportPattern: string;
|
|
58
63
|
telescope?: NormalizedTelescopeConfig;
|
|
59
64
|
studio?: NormalizedStudioConfig;
|
|
65
|
+
hooks?: NormalizedHooksConfig;
|
|
60
66
|
}
|
|
61
67
|
|
|
62
68
|
export interface ProviderBuildResult {
|
package/src/dev/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ChildProcess, spawn } from 'node:child_process';
|
|
1
|
+
import { type ChildProcess, execSync, spawn } from 'node:child_process';
|
|
2
2
|
import { existsSync } from 'node:fs';
|
|
3
3
|
import { mkdir } from 'node:fs/promises';
|
|
4
4
|
import { createServer } from 'node:net';
|
|
@@ -9,6 +9,7 @@ import fg from 'fast-glob';
|
|
|
9
9
|
import { resolveProviders } from '../build/providerResolver';
|
|
10
10
|
import type {
|
|
11
11
|
BuildContext,
|
|
12
|
+
NormalizedHooksConfig,
|
|
12
13
|
NormalizedStudioConfig,
|
|
13
14
|
NormalizedTelescopeConfig,
|
|
14
15
|
} from '../build/types';
|
|
@@ -19,7 +20,11 @@ import {
|
|
|
19
20
|
FunctionGenerator,
|
|
20
21
|
SubscriberGenerator,
|
|
21
22
|
} from '../generators';
|
|
22
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
OPENAPI_OUTPUT_PATH,
|
|
25
|
+
generateOpenApi,
|
|
26
|
+
resolveOpenApiConfig,
|
|
27
|
+
} from '../openapi';
|
|
23
28
|
import type {
|
|
24
29
|
GkmConfig,
|
|
25
30
|
LegacyProvider,
|
|
@@ -200,6 +205,29 @@ export function normalizeStudioConfig(
|
|
|
200
205
|
};
|
|
201
206
|
}
|
|
202
207
|
|
|
208
|
+
/**
|
|
209
|
+
* Normalize hooks configuration
|
|
210
|
+
* @internal Exported for testing
|
|
211
|
+
*/
|
|
212
|
+
export function normalizeHooksConfig(
|
|
213
|
+
config: GkmConfig['hooks'],
|
|
214
|
+
): NormalizedHooksConfig | undefined {
|
|
215
|
+
if (!config?.server) {
|
|
216
|
+
return undefined;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Resolve the path (handle .ts extension)
|
|
220
|
+
const serverPath = config.server.endsWith('.ts')
|
|
221
|
+
? config.server
|
|
222
|
+
: `${config.server}.ts`;
|
|
223
|
+
|
|
224
|
+
const resolvedPath = resolve(process.cwd(), serverPath);
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
serverHooksPath: resolvedPath,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
203
231
|
export interface DevOptions {
|
|
204
232
|
port?: number;
|
|
205
233
|
enableOpenApi?: boolean;
|
|
@@ -260,12 +288,18 @@ export async function devCommand(options: DevOptions): Promise<void> {
|
|
|
260
288
|
logger.log(`🗄️ Studio enabled at ${studio.path}`);
|
|
261
289
|
}
|
|
262
290
|
|
|
291
|
+
// Normalize hooks configuration
|
|
292
|
+
const hooks = normalizeHooksConfig(config.hooks);
|
|
293
|
+
if (hooks) {
|
|
294
|
+
logger.log(`🪝 Server hooks enabled from ${config.hooks?.server}`);
|
|
295
|
+
}
|
|
296
|
+
|
|
263
297
|
// Resolve OpenAPI configuration
|
|
264
298
|
const openApiConfig = resolveOpenApiConfig(config);
|
|
265
299
|
// Enable OpenAPI docs endpoint if either root config or provider config enables it
|
|
266
300
|
const enableOpenApi = openApiConfig.enabled || resolved.enableOpenApi;
|
|
267
301
|
if (enableOpenApi) {
|
|
268
|
-
logger.log(`📄 OpenAPI output: ${
|
|
302
|
+
logger.log(`📄 OpenAPI output: ${OPENAPI_OUTPUT_PATH}`);
|
|
269
303
|
}
|
|
270
304
|
|
|
271
305
|
const buildContext: BuildContext = {
|
|
@@ -275,6 +309,7 @@ export async function devCommand(options: DevOptions): Promise<void> {
|
|
|
275
309
|
loggerImportPattern,
|
|
276
310
|
telescope,
|
|
277
311
|
studio,
|
|
312
|
+
hooks,
|
|
278
313
|
};
|
|
279
314
|
|
|
280
315
|
// Build initial version
|
|
@@ -309,6 +344,9 @@ export async function devCommand(options: DevOptions): Promise<void> {
|
|
|
309
344
|
const envParserFile = config.envParser.split('#')[0];
|
|
310
345
|
const loggerFile = config.logger.split('#')[0];
|
|
311
346
|
|
|
347
|
+
// Get hooks file path for watching
|
|
348
|
+
const hooksFile = config.hooks?.server?.split('#')[0];
|
|
349
|
+
|
|
312
350
|
const watchPatterns = [
|
|
313
351
|
config.routes,
|
|
314
352
|
...(config.functions ? [config.functions] : []),
|
|
@@ -317,6 +355,10 @@ export async function devCommand(options: DevOptions): Promise<void> {
|
|
|
317
355
|
// Add .ts extension if not present for config files
|
|
318
356
|
envParserFile.endsWith('.ts') ? envParserFile : `${envParserFile}.ts`,
|
|
319
357
|
loggerFile.endsWith('.ts') ? loggerFile : `${loggerFile}.ts`,
|
|
358
|
+
// Add hooks file to watch list
|
|
359
|
+
...(hooksFile
|
|
360
|
+
? [hooksFile.endsWith('.ts') ? hooksFile : `${hooksFile}.ts`]
|
|
361
|
+
: []),
|
|
320
362
|
].flat();
|
|
321
363
|
|
|
322
364
|
// Normalize patterns - remove leading ./ when using cwd option
|
|
@@ -391,11 +433,21 @@ export async function devCommand(options: DevOptions): Promise<void> {
|
|
|
391
433
|
});
|
|
392
434
|
|
|
393
435
|
// Handle graceful shutdown
|
|
394
|
-
|
|
436
|
+
let isShuttingDown = false;
|
|
437
|
+
const shutdown = () => {
|
|
438
|
+
if (isShuttingDown) return;
|
|
439
|
+
isShuttingDown = true;
|
|
440
|
+
|
|
395
441
|
logger.log('\n🛑 Shutting down...');
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
442
|
+
|
|
443
|
+
// Use sync-style shutdown to ensure it completes before exit
|
|
444
|
+
Promise.all([watcher.close(), devServer.stop()])
|
|
445
|
+
.catch((err) => {
|
|
446
|
+
logger.error('Error during shutdown:', err);
|
|
447
|
+
})
|
|
448
|
+
.finally(() => {
|
|
449
|
+
process.exit(0);
|
|
450
|
+
});
|
|
399
451
|
};
|
|
400
452
|
|
|
401
453
|
process.on('SIGINT', shutdown);
|
|
@@ -530,40 +582,41 @@ class DevServer {
|
|
|
530
582
|
}
|
|
531
583
|
|
|
532
584
|
async stop(): Promise<void> {
|
|
585
|
+
const port = this.actualPort;
|
|
586
|
+
|
|
533
587
|
if (this.serverProcess && this.isRunning) {
|
|
534
588
|
const pid = this.serverProcess.pid;
|
|
535
589
|
|
|
536
|
-
//
|
|
590
|
+
// Use SIGKILL directly since the server ignores SIGTERM
|
|
537
591
|
if (pid) {
|
|
538
592
|
try {
|
|
539
|
-
process.kill(-pid, '
|
|
593
|
+
process.kill(-pid, 'SIGKILL');
|
|
540
594
|
} catch {
|
|
541
|
-
|
|
595
|
+
try {
|
|
596
|
+
process.kill(pid, 'SIGKILL');
|
|
597
|
+
} catch {
|
|
598
|
+
// Process might already be dead
|
|
599
|
+
}
|
|
542
600
|
}
|
|
543
601
|
}
|
|
544
602
|
|
|
545
|
-
// Wait for process to exit
|
|
546
|
-
await new Promise<void>((resolve) => {
|
|
547
|
-
const timeout = setTimeout(() => {
|
|
548
|
-
if (pid) {
|
|
549
|
-
try {
|
|
550
|
-
process.kill(-pid, 'SIGKILL');
|
|
551
|
-
} catch {
|
|
552
|
-
// Process might already be dead
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
resolve();
|
|
556
|
-
}, 3000);
|
|
557
|
-
|
|
558
|
-
this.serverProcess?.on('exit', () => {
|
|
559
|
-
clearTimeout(timeout);
|
|
560
|
-
resolve();
|
|
561
|
-
});
|
|
562
|
-
});
|
|
563
|
-
|
|
564
603
|
this.serverProcess = null;
|
|
565
604
|
this.isRunning = false;
|
|
566
605
|
}
|
|
606
|
+
|
|
607
|
+
// Also kill any processes still holding the port
|
|
608
|
+
this.killProcessesOnPort(port);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
private killProcessesOnPort(port: number): void {
|
|
612
|
+
try {
|
|
613
|
+
// Use lsof to find PIDs on the port and kill them with -9
|
|
614
|
+
execSync(`lsof -ti tcp:${port} | xargs kill -9 2>/dev/null || true`, {
|
|
615
|
+
stdio: 'ignore',
|
|
616
|
+
});
|
|
617
|
+
} catch {
|
|
618
|
+
// Ignore errors - port may already be free
|
|
619
|
+
}
|
|
567
620
|
}
|
|
568
621
|
|
|
569
622
|
async restart(): Promise<void> {
|
|
@@ -384,6 +384,30 @@ import { createStudioApp } from '@geekmidas/studio/server/hono';`;
|
|
|
384
384
|
}
|
|
385
385
|
}
|
|
386
386
|
|
|
387
|
+
// Generate imports for server hooks
|
|
388
|
+
let hooksImports = '';
|
|
389
|
+
let beforeSetupCall = '';
|
|
390
|
+
let afterSetupCall = '';
|
|
391
|
+
if (context.hooks?.serverHooksPath) {
|
|
392
|
+
const relativeHooksPath = relative(
|
|
393
|
+
dirname(appPath),
|
|
394
|
+
context.hooks.serverHooksPath,
|
|
395
|
+
);
|
|
396
|
+
hooksImports = `import * as serverHooks from '${relativeHooksPath}';`;
|
|
397
|
+
beforeSetupCall = `
|
|
398
|
+
// Call beforeSetup hook if defined
|
|
399
|
+
if (typeof serverHooks.beforeSetup === 'function') {
|
|
400
|
+
await serverHooks.beforeSetup(honoApp, { envParser, logger });
|
|
401
|
+
}
|
|
402
|
+
`;
|
|
403
|
+
afterSetupCall = `
|
|
404
|
+
// Call afterSetup hook if defined
|
|
405
|
+
if (typeof serverHooks.afterSetup === 'function') {
|
|
406
|
+
await serverHooks.afterSetup(honoApp, { envParser, logger });
|
|
407
|
+
}
|
|
408
|
+
`;
|
|
409
|
+
}
|
|
410
|
+
|
|
387
411
|
const telescopeWebSocketSetupCode = telescopeWebSocketEnabled
|
|
388
412
|
? `
|
|
389
413
|
// Setup WebSocket for real-time telescope updates
|
|
@@ -480,6 +504,7 @@ import ${context.envParserImportPattern} from '${relativeEnvParserPath}';
|
|
|
480
504
|
import ${context.loggerImportPattern} from '${relativeLoggerPath}';
|
|
481
505
|
${telescopeImports}
|
|
482
506
|
${studioImports}
|
|
507
|
+
${hooksImports}
|
|
483
508
|
|
|
484
509
|
export interface ServerApp {
|
|
485
510
|
app: HonoType;
|
|
@@ -525,9 +550,10 @@ export interface ServerApp {
|
|
|
525
550
|
*/
|
|
526
551
|
export async function createApp(app?: HonoType, enableOpenApi: boolean = true): Promise<ServerApp> {
|
|
527
552
|
const honoApp = app || new Hono();
|
|
528
|
-
${telescopeSetup}${studioSetup}
|
|
553
|
+
${telescopeSetup}${beforeSetupCall}${studioSetup}
|
|
529
554
|
// Setup HTTP endpoints
|
|
530
555
|
await setupEndpoints(honoApp, envParser, logger, enableOpenApi);
|
|
556
|
+
${afterSetupCall}
|
|
531
557
|
|
|
532
558
|
return {
|
|
533
559
|
app: honoApp,
|
package/src/types.ts
CHANGED
|
@@ -64,6 +64,34 @@ export interface OpenApiConfig {
|
|
|
64
64
|
description?: string;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
export interface HooksConfig {
|
|
68
|
+
/**
|
|
69
|
+
* Path to a module exporting server lifecycle hooks.
|
|
70
|
+
* The module should export `beforeSetup` and/or `afterSetup` functions.
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* ```typescript
|
|
74
|
+
* // src/config/hooks.ts
|
|
75
|
+
* import type { Hono } from 'hono';
|
|
76
|
+
* import type { Logger } from '@geekmidas/logger';
|
|
77
|
+
* import type { EnvironmentParser } from '@geekmidas/envkit';
|
|
78
|
+
*
|
|
79
|
+
* // Called BEFORE gkm endpoints are registered
|
|
80
|
+
* export function beforeSetup(app: Hono, ctx: { envParser: EnvironmentParser; logger: Logger }) {
|
|
81
|
+
* app.use('*', cors());
|
|
82
|
+
* app.get('/custom/health', (c) => c.json({ status: 'ok' }));
|
|
83
|
+
* }
|
|
84
|
+
*
|
|
85
|
+
* // Called AFTER gkm endpoints are registered
|
|
86
|
+
* export function afterSetup(app: Hono, ctx: { envParser: EnvironmentParser; logger: Logger }) {
|
|
87
|
+
* app.notFound((c) => c.json({ error: 'Not found' }, 404));
|
|
88
|
+
* app.onError((err, c) => c.json({ error: err.message }, 500));
|
|
89
|
+
* }
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
server?: string;
|
|
93
|
+
}
|
|
94
|
+
|
|
67
95
|
export interface ProvidersConfig {
|
|
68
96
|
aws?: {
|
|
69
97
|
apiGateway?: {
|
|
@@ -86,6 +114,16 @@ export interface GkmConfig {
|
|
|
86
114
|
envParser: string;
|
|
87
115
|
logger: string;
|
|
88
116
|
providers?: ProvidersConfig;
|
|
117
|
+
/**
|
|
118
|
+
* Server lifecycle hooks for customizing the Hono app.
|
|
119
|
+
* Allows adding custom routes, middleware, error handlers, etc.
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* hooks: {
|
|
123
|
+
* server: './src/config/hooks'
|
|
124
|
+
* }
|
|
125
|
+
*/
|
|
126
|
+
hooks?: HooksConfig;
|
|
89
127
|
/**
|
|
90
128
|
* Telescope configuration for debugging/monitoring.
|
|
91
129
|
* Can be:
|