@geekmidas/cli 0.7.0 → 0.9.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 +64 -34
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +64 -34
- 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 +7 -7
- 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 +104 -35
- package/src/generators/EndpointGenerator.ts +27 -1
- package/src/index.ts +5 -12
- 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,8 +205,32 @@ 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;
|
|
233
|
+
portExplicit?: boolean;
|
|
205
234
|
enableOpenApi?: boolean;
|
|
206
235
|
}
|
|
207
236
|
|
|
@@ -260,12 +289,18 @@ export async function devCommand(options: DevOptions): Promise<void> {
|
|
|
260
289
|
logger.log(`🗄️ Studio enabled at ${studio.path}`);
|
|
261
290
|
}
|
|
262
291
|
|
|
292
|
+
// Normalize hooks configuration
|
|
293
|
+
const hooks = normalizeHooksConfig(config.hooks);
|
|
294
|
+
if (hooks) {
|
|
295
|
+
logger.log(`🪝 Server hooks enabled from ${config.hooks?.server}`);
|
|
296
|
+
}
|
|
297
|
+
|
|
263
298
|
// Resolve OpenAPI configuration
|
|
264
299
|
const openApiConfig = resolveOpenApiConfig(config);
|
|
265
300
|
// Enable OpenAPI docs endpoint if either root config or provider config enables it
|
|
266
301
|
const enableOpenApi = openApiConfig.enabled || resolved.enableOpenApi;
|
|
267
302
|
if (enableOpenApi) {
|
|
268
|
-
logger.log(`📄 OpenAPI output: ${
|
|
303
|
+
logger.log(`📄 OpenAPI output: ${OPENAPI_OUTPUT_PATH}`);
|
|
269
304
|
}
|
|
270
305
|
|
|
271
306
|
const buildContext: BuildContext = {
|
|
@@ -275,6 +310,7 @@ export async function devCommand(options: DevOptions): Promise<void> {
|
|
|
275
310
|
loggerImportPattern,
|
|
276
311
|
telescope,
|
|
277
312
|
studio,
|
|
313
|
+
hooks,
|
|
278
314
|
};
|
|
279
315
|
|
|
280
316
|
// Build initial version
|
|
@@ -297,6 +333,7 @@ export async function devCommand(options: DevOptions): Promise<void> {
|
|
|
297
333
|
const devServer = new DevServer(
|
|
298
334
|
resolved.providers[0] as LegacyProvider,
|
|
299
335
|
options.port || 3000,
|
|
336
|
+
options.portExplicit ?? false,
|
|
300
337
|
enableOpenApi,
|
|
301
338
|
telescope,
|
|
302
339
|
studio,
|
|
@@ -309,6 +346,9 @@ export async function devCommand(options: DevOptions): Promise<void> {
|
|
|
309
346
|
const envParserFile = config.envParser.split('#')[0];
|
|
310
347
|
const loggerFile = config.logger.split('#')[0];
|
|
311
348
|
|
|
349
|
+
// Get hooks file path for watching
|
|
350
|
+
const hooksFile = config.hooks?.server?.split('#')[0];
|
|
351
|
+
|
|
312
352
|
const watchPatterns = [
|
|
313
353
|
config.routes,
|
|
314
354
|
...(config.functions ? [config.functions] : []),
|
|
@@ -317,6 +357,10 @@ export async function devCommand(options: DevOptions): Promise<void> {
|
|
|
317
357
|
// Add .ts extension if not present for config files
|
|
318
358
|
envParserFile.endsWith('.ts') ? envParserFile : `${envParserFile}.ts`,
|
|
319
359
|
loggerFile.endsWith('.ts') ? loggerFile : `${loggerFile}.ts`,
|
|
360
|
+
// Add hooks file to watch list
|
|
361
|
+
...(hooksFile
|
|
362
|
+
? [hooksFile.endsWith('.ts') ? hooksFile : `${hooksFile}.ts`]
|
|
363
|
+
: []),
|
|
320
364
|
].flat();
|
|
321
365
|
|
|
322
366
|
// Normalize patterns - remove leading ./ when using cwd option
|
|
@@ -391,11 +435,21 @@ export async function devCommand(options: DevOptions): Promise<void> {
|
|
|
391
435
|
});
|
|
392
436
|
|
|
393
437
|
// Handle graceful shutdown
|
|
394
|
-
|
|
438
|
+
let isShuttingDown = false;
|
|
439
|
+
const shutdown = () => {
|
|
440
|
+
if (isShuttingDown) return;
|
|
441
|
+
isShuttingDown = true;
|
|
442
|
+
|
|
395
443
|
logger.log('\n🛑 Shutting down...');
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
444
|
+
|
|
445
|
+
// Use sync-style shutdown to ensure it completes before exit
|
|
446
|
+
Promise.all([watcher.close(), devServer.stop()])
|
|
447
|
+
.catch((err) => {
|
|
448
|
+
logger.error('Error during shutdown:', err);
|
|
449
|
+
})
|
|
450
|
+
.finally(() => {
|
|
451
|
+
process.exit(0);
|
|
452
|
+
});
|
|
399
453
|
};
|
|
400
454
|
|
|
401
455
|
process.on('SIGINT', shutdown);
|
|
@@ -447,6 +501,7 @@ class DevServer {
|
|
|
447
501
|
constructor(
|
|
448
502
|
private provider: LegacyProvider,
|
|
449
503
|
private requestedPort: number,
|
|
504
|
+
private portExplicit: boolean,
|
|
450
505
|
private enableOpenApi: boolean,
|
|
451
506
|
private telescope?: NormalizedTelescopeConfig,
|
|
452
507
|
private studio?: NormalizedStudioConfig,
|
|
@@ -460,13 +515,26 @@ class DevServer {
|
|
|
460
515
|
await this.stop();
|
|
461
516
|
}
|
|
462
517
|
|
|
463
|
-
//
|
|
464
|
-
|
|
518
|
+
// Check port availability
|
|
519
|
+
if (this.portExplicit) {
|
|
520
|
+
// Port was explicitly specified - throw if unavailable
|
|
521
|
+
const available = await isPortAvailable(this.requestedPort);
|
|
522
|
+
if (!available) {
|
|
523
|
+
throw new Error(
|
|
524
|
+
`Port ${this.requestedPort} is already in use. ` +
|
|
525
|
+
`Either stop the process using that port or omit -p/--port to auto-select an available port.`,
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
this.actualPort = this.requestedPort;
|
|
529
|
+
} else {
|
|
530
|
+
// Find an available port starting from the default
|
|
531
|
+
this.actualPort = await findAvailablePort(this.requestedPort);
|
|
465
532
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
533
|
+
if (this.actualPort !== this.requestedPort) {
|
|
534
|
+
logger.log(
|
|
535
|
+
`ℹ️ Port ${this.requestedPort} was in use, using port ${this.actualPort} instead`,
|
|
536
|
+
);
|
|
537
|
+
}
|
|
470
538
|
}
|
|
471
539
|
|
|
472
540
|
const serverEntryPath = join(
|
|
@@ -530,40 +598,41 @@ class DevServer {
|
|
|
530
598
|
}
|
|
531
599
|
|
|
532
600
|
async stop(): Promise<void> {
|
|
601
|
+
const port = this.actualPort;
|
|
602
|
+
|
|
533
603
|
if (this.serverProcess && this.isRunning) {
|
|
534
604
|
const pid = this.serverProcess.pid;
|
|
535
605
|
|
|
536
|
-
//
|
|
606
|
+
// Use SIGKILL directly since the server ignores SIGTERM
|
|
537
607
|
if (pid) {
|
|
538
608
|
try {
|
|
539
|
-
process.kill(-pid, '
|
|
609
|
+
process.kill(-pid, 'SIGKILL');
|
|
540
610
|
} catch {
|
|
541
|
-
|
|
611
|
+
try {
|
|
612
|
+
process.kill(pid, 'SIGKILL');
|
|
613
|
+
} catch {
|
|
614
|
+
// Process might already be dead
|
|
615
|
+
}
|
|
542
616
|
}
|
|
543
617
|
}
|
|
544
618
|
|
|
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
619
|
this.serverProcess = null;
|
|
565
620
|
this.isRunning = false;
|
|
566
621
|
}
|
|
622
|
+
|
|
623
|
+
// Also kill any processes still holding the port
|
|
624
|
+
this.killProcessesOnPort(port);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
private killProcessesOnPort(port: number): void {
|
|
628
|
+
try {
|
|
629
|
+
// Use lsof to find PIDs on the port and kill them with -9
|
|
630
|
+
execSync(`lsof -ti tcp:${port} | xargs kill -9 2>/dev/null || true`, {
|
|
631
|
+
stdio: 'ignore',
|
|
632
|
+
});
|
|
633
|
+
} catch {
|
|
634
|
+
// Ignore errors - port may already be free
|
|
635
|
+
}
|
|
567
636
|
}
|
|
568
637
|
|
|
569
638
|
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/index.ts
CHANGED
|
@@ -111,7 +111,7 @@ program
|
|
|
111
111
|
program
|
|
112
112
|
.command('dev')
|
|
113
113
|
.description('Start development server with automatic reload')
|
|
114
|
-
.option('--port <port>', 'Port to run the development server on'
|
|
114
|
+
.option('-p, --port <port>', 'Port to run the development server on')
|
|
115
115
|
.option(
|
|
116
116
|
'--enable-openapi',
|
|
117
117
|
'Enable OpenAPI documentation for development server',
|
|
@@ -126,6 +126,7 @@ program
|
|
|
126
126
|
|
|
127
127
|
await devCommand({
|
|
128
128
|
port: options.port ? Number.parseInt(options.port) : 3000,
|
|
129
|
+
portExplicit: !!options.port,
|
|
129
130
|
enableOpenApi: options.enableOpenapi ?? true,
|
|
130
131
|
});
|
|
131
132
|
} catch (error) {
|
|
@@ -169,22 +170,14 @@ program
|
|
|
169
170
|
|
|
170
171
|
program
|
|
171
172
|
.command('openapi')
|
|
172
|
-
.description(
|
|
173
|
-
|
|
174
|
-
)
|
|
175
|
-
.option(
|
|
176
|
-
'--output <path>',
|
|
177
|
-
'Output file path for the OpenAPI spec',
|
|
178
|
-
'openapi.ts',
|
|
179
|
-
)
|
|
180
|
-
.option('--json', 'Generate JSON instead of TypeScript (legacy)', false)
|
|
181
|
-
.action(async (options: { output?: string; json?: boolean }) => {
|
|
173
|
+
.description('Generate OpenAPI specification from endpoints')
|
|
174
|
+
.action(async () => {
|
|
182
175
|
try {
|
|
183
176
|
const globalOptions = program.opts();
|
|
184
177
|
if (globalOptions.cwd) {
|
|
185
178
|
process.chdir(globalOptions.cwd);
|
|
186
179
|
}
|
|
187
|
-
await openapiCommand(
|
|
180
|
+
await openapiCommand({});
|
|
188
181
|
} catch (error) {
|
|
189
182
|
console.error('OpenAPI generation failed:', (error as Error).message);
|
|
190
183
|
process.exit(1);
|