@geekmidas/cli 0.2.3 → 0.2.4
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/{CronGenerator-DXRfHQcV.mjs → CronGenerator-Bh26MaNA.mjs} +2 -1
- package/dist/CronGenerator-Bh26MaNA.mjs.map +1 -0
- package/dist/{CronGenerator-1PflEYe2.cjs → CronGenerator-C6MF8rlG.cjs} +2 -1
- package/dist/CronGenerator-C6MF8rlG.cjs.map +1 -0
- package/dist/{EndpointGenerator-BbGrDiCP.cjs → EndpointGenerator-C73wNoih.cjs} +12 -5
- package/dist/EndpointGenerator-C73wNoih.cjs.map +1 -0
- package/dist/{EndpointGenerator-BmZ9BxbO.mjs → EndpointGenerator-CWh18d92.mjs} +12 -5
- package/dist/EndpointGenerator-CWh18d92.mjs.map +1 -0
- package/dist/{FunctionGenerator-DOEB_yPh.mjs → FunctionGenerator-BNE_GC7N.mjs} +2 -1
- package/dist/FunctionGenerator-BNE_GC7N.mjs.map +1 -0
- package/dist/{FunctionGenerator-Clw64SwQ.cjs → FunctionGenerator-FgZUTd8L.cjs} +2 -1
- package/dist/FunctionGenerator-FgZUTd8L.cjs.map +1 -0
- package/dist/{SubscriberGenerator-CB-NHtZW.cjs → SubscriberGenerator-Bd-a7aiw.cjs} +2 -1
- package/dist/SubscriberGenerator-Bd-a7aiw.cjs.map +1 -0
- package/dist/{SubscriberGenerator-Cuu4co3-.mjs → SubscriberGenerator-Dnlj_1FK.mjs} +2 -1
- package/dist/SubscriberGenerator-Dnlj_1FK.mjs.map +1 -0
- package/dist/build/index.cjs +5 -5
- package/dist/build/index.mjs +5 -5
- package/dist/{build-Ajg356_5.cjs → build-BVng9MQX.cjs} +5 -5
- package/dist/{build-Ajg356_5.cjs.map → build-BVng9MQX.cjs.map} +1 -1
- package/dist/{build-zpABVsc0.mjs → build-BqexeI-W.mjs} +5 -5
- package/dist/{build-zpABVsc0.mjs.map → build-BqexeI-W.mjs.map} +1 -1
- package/dist/dev/index.cjs +13 -0
- package/dist/dev/index.mjs +11 -0
- package/dist/dev-DbtyToc7.cjs +259 -0
- package/dist/dev-DbtyToc7.cjs.map +1 -0
- package/dist/dev-DnGYXuMn.mjs +241 -0
- package/dist/dev-DnGYXuMn.mjs.map +1 -0
- package/dist/generators/CronGenerator.cjs +1 -1
- package/dist/generators/CronGenerator.mjs +1 -1
- package/dist/generators/EndpointGenerator.cjs +1 -1
- package/dist/generators/EndpointGenerator.mjs +1 -1
- package/dist/generators/FunctionGenerator.cjs +1 -1
- package/dist/generators/FunctionGenerator.mjs +1 -1
- package/dist/generators/SubscriberGenerator.cjs +1 -1
- package/dist/generators/SubscriberGenerator.mjs +1 -1
- package/dist/generators/index.cjs +4 -4
- package/dist/generators/index.mjs +4 -4
- package/dist/index.cjs +33 -12
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +33 -12
- package/dist/index.mjs.map +1 -1
- package/dist/{openapi-BQx3_JbM.mjs → openapi-BTHbPrxS.mjs} +2 -2
- package/dist/{openapi-BQx3_JbM.mjs.map → openapi-BTHbPrxS.mjs.map} +1 -1
- package/dist/{openapi-CMLr04cz.cjs → openapi-CewcfoRH.cjs} +2 -2
- package/dist/{openapi-CMLr04cz.cjs.map → openapi-CewcfoRH.cjs.map} +1 -1
- package/dist/{openapi-react-query-Dvjqx_Eo.cjs → openapi-react-query-D9Z7lh0p.cjs} +1 -1
- package/dist/{openapi-react-query-Dvjqx_Eo.cjs.map → openapi-react-query-D9Z7lh0p.cjs.map} +1 -1
- package/dist/{openapi-react-query-DbrWwQzb.mjs → openapi-react-query-MEBlYIM1.mjs} +1 -1
- package/dist/{openapi-react-query-DbrWwQzb.mjs.map → openapi-react-query-MEBlYIM1.mjs.map} +1 -1
- package/dist/openapi-react-query.cjs +1 -1
- package/dist/openapi-react-query.mjs +1 -1
- package/dist/openapi.cjs +2 -2
- package/dist/openapi.mjs +2 -2
- package/package.json +12 -6
- package/src/dev/__tests__/index.spec.ts +208 -0
- package/src/dev/index.ts +377 -0
- package/src/generators/CronGenerator.ts +8 -3
- package/src/generators/EndpointGenerator.ts +94 -6
- package/src/generators/FunctionGenerator.ts +21 -3
- package/src/generators/SubscriberGenerator.ts +1 -0
- package/src/generators/__tests__/SubscriberGenerator.spec.ts +12 -9
- package/src/index.ts +27 -0
- package/src/types.ts +3 -0
- package/dist/CronGenerator-1PflEYe2.cjs.map +0 -1
- package/dist/CronGenerator-DXRfHQcV.mjs.map +0 -1
- package/dist/EndpointGenerator-BbGrDiCP.cjs.map +0 -1
- package/dist/EndpointGenerator-BmZ9BxbO.mjs.map +0 -1
- package/dist/FunctionGenerator-Clw64SwQ.cjs.map +0 -1
- package/dist/FunctionGenerator-DOEB_yPh.mjs.map +0 -1
- package/dist/SubscriberGenerator-CB-NHtZW.cjs.map +0 -1
- package/dist/SubscriberGenerator-Cuu4co3-.mjs.map +0 -1
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { createServer } from 'node:net';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { findAvailablePort, isPortAvailable } from '../index';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Helper to occupy a port for testing
|
|
7
|
+
*/
|
|
8
|
+
function occupyPort(port: number): Promise<ReturnType<typeof createServer>> {
|
|
9
|
+
return new Promise((resolve, reject) => {
|
|
10
|
+
const server = createServer();
|
|
11
|
+
|
|
12
|
+
server.once('error', (err) => {
|
|
13
|
+
reject(err);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
server.once('listening', () => {
|
|
17
|
+
resolve(server);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
server.listen(port);
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe('Port Availability Functions', () => {
|
|
25
|
+
describe('isPortAvailable', () => {
|
|
26
|
+
it('should return true for an available port', async () => {
|
|
27
|
+
// Use a high port number to avoid conflicts
|
|
28
|
+
const port = 45000;
|
|
29
|
+
const available = await isPortAvailable(port);
|
|
30
|
+
expect(available).toBe(true);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should return false for a port in use', async () => {
|
|
34
|
+
const port = 45001;
|
|
35
|
+
const server = await occupyPort(port);
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const available = await isPortAvailable(port);
|
|
39
|
+
expect(available).toBe(false);
|
|
40
|
+
} finally {
|
|
41
|
+
server.close();
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should handle multiple sequential checks correctly', async () => {
|
|
46
|
+
const port = 45002;
|
|
47
|
+
|
|
48
|
+
// First check - port should be available
|
|
49
|
+
const firstCheck = await isPortAvailable(port);
|
|
50
|
+
expect(firstCheck).toBe(true);
|
|
51
|
+
|
|
52
|
+
// Occupy the port
|
|
53
|
+
const server = await occupyPort(port);
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
// Second check - port should be unavailable
|
|
57
|
+
const secondCheck = await isPortAvailable(port);
|
|
58
|
+
expect(secondCheck).toBe(false);
|
|
59
|
+
} finally {
|
|
60
|
+
server.close();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Give a moment for the port to be released
|
|
64
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
65
|
+
|
|
66
|
+
// Third check - port should be available again
|
|
67
|
+
const thirdCheck = await isPortAvailable(port);
|
|
68
|
+
expect(thirdCheck).toBe(true);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe('findAvailablePort', () => {
|
|
73
|
+
it('should return the preferred port if available', async () => {
|
|
74
|
+
const preferredPort = 45100;
|
|
75
|
+
const foundPort = await findAvailablePort(preferredPort);
|
|
76
|
+
expect(foundPort).toBe(preferredPort);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should return the next available port if preferred is in use', async () => {
|
|
80
|
+
const preferredPort = 45101;
|
|
81
|
+
const server = await occupyPort(preferredPort);
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
const foundPort = await findAvailablePort(preferredPort);
|
|
85
|
+
expect(foundPort).toBe(preferredPort + 1);
|
|
86
|
+
} finally {
|
|
87
|
+
server.close();
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('should skip multiple occupied ports', async () => {
|
|
92
|
+
const preferredPort = 45102;
|
|
93
|
+
const server1 = await occupyPort(preferredPort);
|
|
94
|
+
const server2 = await occupyPort(preferredPort + 1);
|
|
95
|
+
const server3 = await occupyPort(preferredPort + 2);
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
const foundPort = await findAvailablePort(preferredPort);
|
|
99
|
+
expect(foundPort).toBe(preferredPort + 3);
|
|
100
|
+
} finally {
|
|
101
|
+
server1.close();
|
|
102
|
+
server2.close();
|
|
103
|
+
server3.close();
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('should throw error if no available port found within max attempts', async () => {
|
|
108
|
+
const preferredPort = 45103;
|
|
109
|
+
const maxAttempts = 3;
|
|
110
|
+
|
|
111
|
+
// Occupy ports 45103, 45104, 45105
|
|
112
|
+
const servers = await Promise.all([
|
|
113
|
+
occupyPort(preferredPort),
|
|
114
|
+
occupyPort(preferredPort + 1),
|
|
115
|
+
occupyPort(preferredPort + 2),
|
|
116
|
+
]);
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
await expect(
|
|
120
|
+
findAvailablePort(preferredPort, maxAttempts),
|
|
121
|
+
).rejects.toThrow(
|
|
122
|
+
`Could not find an available port after trying ${maxAttempts} ports starting from ${preferredPort}`,
|
|
123
|
+
);
|
|
124
|
+
} finally {
|
|
125
|
+
servers.forEach((server) => server.close());
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('should respect custom maxAttempts parameter', async () => {
|
|
130
|
+
const preferredPort = 45104;
|
|
131
|
+
const maxAttempts = 5;
|
|
132
|
+
|
|
133
|
+
// Occupy first 4 ports
|
|
134
|
+
const servers = await Promise.all([
|
|
135
|
+
occupyPort(preferredPort),
|
|
136
|
+
occupyPort(preferredPort + 1),
|
|
137
|
+
occupyPort(preferredPort + 2),
|
|
138
|
+
occupyPort(preferredPort + 3),
|
|
139
|
+
]);
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
const foundPort = await findAvailablePort(preferredPort, maxAttempts);
|
|
143
|
+
// Should find port at preferredPort + 4 (within 5 attempts)
|
|
144
|
+
expect(foundPort).toBe(preferredPort + 4);
|
|
145
|
+
} finally {
|
|
146
|
+
servers.forEach((server) => server.close());
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
describe('DevServer', () => {
|
|
153
|
+
describe('port selection', () => {
|
|
154
|
+
it('should use requested port when available', async () => {
|
|
155
|
+
// This is more of an integration test that would need the actual DevServer
|
|
156
|
+
// For now, we test the underlying logic
|
|
157
|
+
const requestedPort = 45200;
|
|
158
|
+
const actualPort = await findAvailablePort(requestedPort);
|
|
159
|
+
expect(actualPort).toBe(requestedPort);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('should select alternative port when requested is in use', async () => {
|
|
163
|
+
const requestedPort = 45201;
|
|
164
|
+
const server = await occupyPort(requestedPort);
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
const actualPort = await findAvailablePort(requestedPort);
|
|
168
|
+
expect(actualPort).not.toBe(requestedPort);
|
|
169
|
+
expect(actualPort).toBeGreaterThan(requestedPort);
|
|
170
|
+
expect(actualPort).toBeLessThanOrEqual(requestedPort + 10);
|
|
171
|
+
} finally {
|
|
172
|
+
server.close();
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
describe('devCommand edge cases', () => {
|
|
179
|
+
it('should handle port conflicts gracefully', async () => {
|
|
180
|
+
const port = 45300;
|
|
181
|
+
const server = await occupyPort(port);
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
// The dev command should find an alternative port
|
|
185
|
+
const alternativePort = await findAvailablePort(port);
|
|
186
|
+
expect(alternativePort).toBeGreaterThan(port);
|
|
187
|
+
} finally {
|
|
188
|
+
server.close();
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('should handle concurrent port checks', async () => {
|
|
193
|
+
const basePort = 45400;
|
|
194
|
+
|
|
195
|
+
// Run multiple port checks concurrently
|
|
196
|
+
const results = await Promise.all([
|
|
197
|
+
findAvailablePort(basePort),
|
|
198
|
+
findAvailablePort(basePort + 5),
|
|
199
|
+
findAvailablePort(basePort + 10),
|
|
200
|
+
]);
|
|
201
|
+
|
|
202
|
+
// All should succeed and return valid ports
|
|
203
|
+
expect(results).toHaveLength(3);
|
|
204
|
+
results.forEach((port) => {
|
|
205
|
+
expect(port).toBeGreaterThanOrEqual(basePort);
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
});
|
package/src/dev/index.ts
ADDED
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
import { type ChildProcess, spawn } from 'node:child_process';
|
|
2
|
+
import { mkdir } from 'node:fs/promises';
|
|
3
|
+
import { createServer } from 'node:net';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import chokidar from 'chokidar';
|
|
6
|
+
import { resolveProviders } from '../build/providerResolver';
|
|
7
|
+
import type { BuildContext } from '../build/types';
|
|
8
|
+
import { loadConfig } from '../config';
|
|
9
|
+
import {
|
|
10
|
+
CronGenerator,
|
|
11
|
+
EndpointGenerator,
|
|
12
|
+
FunctionGenerator,
|
|
13
|
+
SubscriberGenerator,
|
|
14
|
+
} from '../generators';
|
|
15
|
+
import type { LegacyProvider } from '../types';
|
|
16
|
+
|
|
17
|
+
const logger = console;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Check if a port is available
|
|
21
|
+
* @internal Exported for testing
|
|
22
|
+
*/
|
|
23
|
+
export async function isPortAvailable(port: number): Promise<boolean> {
|
|
24
|
+
return new Promise((resolve) => {
|
|
25
|
+
const server = createServer();
|
|
26
|
+
|
|
27
|
+
server.once('error', (err: NodeJS.ErrnoException) => {
|
|
28
|
+
if (err.code === 'EADDRINUSE') {
|
|
29
|
+
resolve(false);
|
|
30
|
+
} else {
|
|
31
|
+
resolve(false);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
server.once('listening', () => {
|
|
36
|
+
server.close();
|
|
37
|
+
resolve(true);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
server.listen(port);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Find an available port starting from the preferred port
|
|
46
|
+
* @internal Exported for testing
|
|
47
|
+
*/
|
|
48
|
+
export async function findAvailablePort(
|
|
49
|
+
preferredPort: number,
|
|
50
|
+
maxAttempts = 10,
|
|
51
|
+
): Promise<number> {
|
|
52
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
53
|
+
const port = preferredPort + i;
|
|
54
|
+
if (await isPortAvailable(port)) {
|
|
55
|
+
return port;
|
|
56
|
+
}
|
|
57
|
+
logger.log(`⚠️ Port ${port} is in use, trying ${port + 1}...`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
throw new Error(
|
|
61
|
+
`Could not find an available port after trying ${maxAttempts} ports starting from ${preferredPort}`,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface DevOptions {
|
|
66
|
+
port?: number;
|
|
67
|
+
enableOpenApi?: boolean;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function devCommand(options: DevOptions): Promise<void> {
|
|
71
|
+
const config = await loadConfig();
|
|
72
|
+
|
|
73
|
+
// Force server provider for dev mode
|
|
74
|
+
const resolved = resolveProviders(config, { provider: 'server' });
|
|
75
|
+
|
|
76
|
+
logger.log('🚀 Starting development server...');
|
|
77
|
+
logger.log(`Loading routes from: ${config.routes}`);
|
|
78
|
+
if (config.functions) {
|
|
79
|
+
logger.log(`Loading functions from: ${config.functions}`);
|
|
80
|
+
}
|
|
81
|
+
if (config.crons) {
|
|
82
|
+
logger.log(`Loading crons from: ${config.crons}`);
|
|
83
|
+
}
|
|
84
|
+
if (config.subscribers) {
|
|
85
|
+
logger.log(`Loading subscribers from: ${config.subscribers}`);
|
|
86
|
+
}
|
|
87
|
+
logger.log(`Using envParser: ${config.envParser}`);
|
|
88
|
+
|
|
89
|
+
// Parse envParser configuration
|
|
90
|
+
const [envParserPath, envParserName] = config.envParser.split('#');
|
|
91
|
+
const envParserImportPattern = !envParserName
|
|
92
|
+
? 'envParser'
|
|
93
|
+
: envParserName === 'envParser'
|
|
94
|
+
? '{ envParser }'
|
|
95
|
+
: `{ ${envParserName} as envParser }`;
|
|
96
|
+
|
|
97
|
+
// Parse logger configuration
|
|
98
|
+
const [loggerPath, loggerName] = config.logger.split('#');
|
|
99
|
+
const loggerImportPattern = !loggerName
|
|
100
|
+
? 'logger'
|
|
101
|
+
: loggerName === 'logger'
|
|
102
|
+
? '{ logger }'
|
|
103
|
+
: `{ ${loggerName} as logger }`;
|
|
104
|
+
|
|
105
|
+
const buildContext: BuildContext = {
|
|
106
|
+
envParserPath,
|
|
107
|
+
envParserImportPattern,
|
|
108
|
+
loggerPath,
|
|
109
|
+
loggerImportPattern,
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
// Build initial version
|
|
113
|
+
await buildServer(
|
|
114
|
+
config,
|
|
115
|
+
buildContext,
|
|
116
|
+
resolved.providers[0] as LegacyProvider,
|
|
117
|
+
resolved.enableOpenApi,
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
// Start the dev server
|
|
121
|
+
const devServer = new DevServer(
|
|
122
|
+
resolved.providers[0] as LegacyProvider,
|
|
123
|
+
options.port || 3000,
|
|
124
|
+
resolved.enableOpenApi,
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
await devServer.start();
|
|
128
|
+
|
|
129
|
+
// Watch for file changes
|
|
130
|
+
const watchPatterns = [
|
|
131
|
+
config.routes,
|
|
132
|
+
...(config.functions ? [config.functions] : []),
|
|
133
|
+
...(config.crons ? [config.crons] : []),
|
|
134
|
+
...(config.subscribers ? [config.subscribers] : []),
|
|
135
|
+
config.envParser.split('#')[0],
|
|
136
|
+
config.logger.split('#')[0],
|
|
137
|
+
].flat();
|
|
138
|
+
|
|
139
|
+
logger.log(`👀 Watching for changes in: ${watchPatterns.join(', ')}`);
|
|
140
|
+
|
|
141
|
+
const watcher = chokidar.watch(watchPatterns, {
|
|
142
|
+
ignored: /(^|[\/\\])\../, // ignore dotfiles
|
|
143
|
+
persistent: true,
|
|
144
|
+
ignoreInitial: true,
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
let rebuildTimeout: NodeJS.Timeout | null = null;
|
|
148
|
+
|
|
149
|
+
watcher.on('change', async (path) => {
|
|
150
|
+
logger.log(`📝 File changed: ${path}`);
|
|
151
|
+
|
|
152
|
+
// Debounce rebuilds
|
|
153
|
+
if (rebuildTimeout) {
|
|
154
|
+
clearTimeout(rebuildTimeout);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
rebuildTimeout = setTimeout(async () => {
|
|
158
|
+
try {
|
|
159
|
+
logger.log('🔄 Rebuilding...');
|
|
160
|
+
await buildServer(
|
|
161
|
+
config,
|
|
162
|
+
buildContext,
|
|
163
|
+
resolved.providers[0] as LegacyProvider,
|
|
164
|
+
resolved.enableOpenApi,
|
|
165
|
+
);
|
|
166
|
+
logger.log('✅ Rebuild complete, restarting server...');
|
|
167
|
+
await devServer.restart();
|
|
168
|
+
} catch (error) {
|
|
169
|
+
logger.error('❌ Rebuild failed:', (error as Error).message);
|
|
170
|
+
}
|
|
171
|
+
}, 300);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// Handle graceful shutdown
|
|
175
|
+
const shutdown = async () => {
|
|
176
|
+
logger.log('\n🛑 Shutting down...');
|
|
177
|
+
await watcher.close();
|
|
178
|
+
await devServer.stop();
|
|
179
|
+
process.exit(0);
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
process.on('SIGINT', shutdown);
|
|
183
|
+
process.on('SIGTERM', shutdown);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function buildServer(
|
|
187
|
+
config: any,
|
|
188
|
+
context: BuildContext,
|
|
189
|
+
provider: LegacyProvider,
|
|
190
|
+
enableOpenApi: boolean,
|
|
191
|
+
): Promise<void> {
|
|
192
|
+
// Initialize generators
|
|
193
|
+
const endpointGenerator = new EndpointGenerator();
|
|
194
|
+
const functionGenerator = new FunctionGenerator();
|
|
195
|
+
const cronGenerator = new CronGenerator();
|
|
196
|
+
const subscriberGenerator = new SubscriberGenerator();
|
|
197
|
+
|
|
198
|
+
// Load all constructs
|
|
199
|
+
const [allEndpoints, allFunctions, allCrons, allSubscribers] =
|
|
200
|
+
await Promise.all([
|
|
201
|
+
endpointGenerator.load(config.routes),
|
|
202
|
+
config.functions ? functionGenerator.load(config.functions) : [],
|
|
203
|
+
config.crons ? cronGenerator.load(config.crons) : [],
|
|
204
|
+
config.subscribers ? subscriberGenerator.load(config.subscribers) : [],
|
|
205
|
+
]);
|
|
206
|
+
|
|
207
|
+
// Ensure .gkm directory exists
|
|
208
|
+
const outputDir = join(process.cwd(), '.gkm', provider);
|
|
209
|
+
await mkdir(outputDir, { recursive: true });
|
|
210
|
+
|
|
211
|
+
// Build for server provider
|
|
212
|
+
await Promise.all([
|
|
213
|
+
endpointGenerator.build(context, allEndpoints, outputDir, {
|
|
214
|
+
provider,
|
|
215
|
+
enableOpenApi,
|
|
216
|
+
}),
|
|
217
|
+
functionGenerator.build(context, allFunctions, outputDir, { provider }),
|
|
218
|
+
cronGenerator.build(context, allCrons, outputDir, { provider }),
|
|
219
|
+
subscriberGenerator.build(context, allSubscribers, outputDir, { provider }),
|
|
220
|
+
]);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
class DevServer {
|
|
224
|
+
private serverProcess: ChildProcess | null = null;
|
|
225
|
+
private isRunning = false;
|
|
226
|
+
private actualPort: number;
|
|
227
|
+
|
|
228
|
+
constructor(
|
|
229
|
+
private provider: LegacyProvider,
|
|
230
|
+
private requestedPort: number,
|
|
231
|
+
private enableOpenApi: boolean,
|
|
232
|
+
) {
|
|
233
|
+
this.actualPort = requestedPort;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async start(): Promise<void> {
|
|
237
|
+
if (this.isRunning) {
|
|
238
|
+
await this.stop();
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Find an available port
|
|
242
|
+
this.actualPort = await findAvailablePort(this.requestedPort);
|
|
243
|
+
|
|
244
|
+
if (this.actualPort !== this.requestedPort) {
|
|
245
|
+
logger.log(
|
|
246
|
+
`ℹ️ Port ${this.requestedPort} was in use, using port ${this.actualPort} instead`,
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const serverEntryPath = join(
|
|
251
|
+
process.cwd(),
|
|
252
|
+
'.gkm',
|
|
253
|
+
this.provider,
|
|
254
|
+
'server.ts',
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
// Create server entry file
|
|
258
|
+
await this.createServerEntry();
|
|
259
|
+
|
|
260
|
+
logger.log(`\n✨ Starting server on port ${this.actualPort}...`);
|
|
261
|
+
|
|
262
|
+
// Start the server using tsx (TypeScript execution)
|
|
263
|
+
this.serverProcess = spawn(
|
|
264
|
+
'npx',
|
|
265
|
+
['tsx', serverEntryPath, '--port', this.actualPort.toString()],
|
|
266
|
+
{
|
|
267
|
+
stdio: 'inherit',
|
|
268
|
+
env: { ...process.env, NODE_ENV: 'development' },
|
|
269
|
+
},
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
this.isRunning = true;
|
|
273
|
+
|
|
274
|
+
this.serverProcess.on('error', (error) => {
|
|
275
|
+
logger.error('❌ Server error:', error);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
this.serverProcess.on('exit', (code, signal) => {
|
|
279
|
+
if (code !== null && code !== 0 && signal !== 'SIGTERM') {
|
|
280
|
+
logger.error(`❌ Server exited with code ${code}`);
|
|
281
|
+
}
|
|
282
|
+
this.isRunning = false;
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
// Give the server a moment to start
|
|
286
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
287
|
+
|
|
288
|
+
if (this.isRunning) {
|
|
289
|
+
logger.log(`\n🎉 Server running at http://localhost:${this.actualPort}`);
|
|
290
|
+
if (this.enableOpenApi) {
|
|
291
|
+
logger.log(
|
|
292
|
+
`📚 API Docs available at http://localhost:${this.actualPort}/docs`,
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async stop(): Promise<void> {
|
|
299
|
+
if (this.serverProcess && this.isRunning) {
|
|
300
|
+
this.serverProcess.kill('SIGTERM');
|
|
301
|
+
|
|
302
|
+
// Wait for process to exit
|
|
303
|
+
await new Promise<void>((resolve) => {
|
|
304
|
+
const timeout = setTimeout(() => {
|
|
305
|
+
this.serverProcess?.kill('SIGKILL');
|
|
306
|
+
resolve();
|
|
307
|
+
}, 5000);
|
|
308
|
+
|
|
309
|
+
this.serverProcess?.on('exit', () => {
|
|
310
|
+
clearTimeout(timeout);
|
|
311
|
+
resolve();
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
this.serverProcess = null;
|
|
316
|
+
this.isRunning = false;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
async restart(): Promise<void> {
|
|
321
|
+
await this.stop();
|
|
322
|
+
await this.start();
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
private async createServerEntry(): Promise<void> {
|
|
326
|
+
const { writeFile } = await import('node:fs/promises');
|
|
327
|
+
const { relative, dirname } = await import('node:path');
|
|
328
|
+
|
|
329
|
+
const serverPath = join(process.cwd(), '.gkm', this.provider, 'server.ts');
|
|
330
|
+
|
|
331
|
+
const relativeAppPath = relative(
|
|
332
|
+
dirname(serverPath),
|
|
333
|
+
join(dirname(serverPath), 'app.js'),
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
const content = `#!/usr/bin/env node
|
|
337
|
+
/**
|
|
338
|
+
* Development server entry point
|
|
339
|
+
* This file is auto-generated by 'gkm dev'
|
|
340
|
+
*/
|
|
341
|
+
import { createApp } from './${relativeAppPath.startsWith('.') ? relativeAppPath : './' + relativeAppPath}';
|
|
342
|
+
|
|
343
|
+
const port = process.argv.includes('--port')
|
|
344
|
+
? Number.parseInt(process.argv[process.argv.indexOf('--port') + 1])
|
|
345
|
+
: 3000;
|
|
346
|
+
|
|
347
|
+
const { app, start } = createApp(undefined, ${this.enableOpenApi});
|
|
348
|
+
|
|
349
|
+
// Start the server
|
|
350
|
+
start({
|
|
351
|
+
port,
|
|
352
|
+
serve: async (app, port) => {
|
|
353
|
+
// Detect runtime and use appropriate server
|
|
354
|
+
if (typeof Bun !== 'undefined') {
|
|
355
|
+
// Bun runtime
|
|
356
|
+
Bun.serve({
|
|
357
|
+
port,
|
|
358
|
+
fetch: app.fetch,
|
|
359
|
+
});
|
|
360
|
+
} else {
|
|
361
|
+
// Node.js runtime with @hono/node-server
|
|
362
|
+
const { serve } = await import('@hono/node-server');
|
|
363
|
+
serve({
|
|
364
|
+
fetch: app.fetch,
|
|
365
|
+
port,
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
},
|
|
369
|
+
}).catch((error) => {
|
|
370
|
+
console.error('Failed to start server:', error);
|
|
371
|
+
process.exit(1);
|
|
372
|
+
});
|
|
373
|
+
`;
|
|
374
|
+
|
|
375
|
+
await writeFile(serverPath, content);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
@@ -10,12 +10,14 @@ import {
|
|
|
10
10
|
} from './Generator';
|
|
11
11
|
|
|
12
12
|
export class CronGenerator extends ConstructGenerator<
|
|
13
|
-
Cron<any, any, any, any>,
|
|
13
|
+
Cron<any, any, any, any, any, any, any, any>,
|
|
14
14
|
CronInfo[]
|
|
15
15
|
> {
|
|
16
16
|
async build(
|
|
17
17
|
context: BuildContext,
|
|
18
|
-
constructs: GeneratedConstruct<
|
|
18
|
+
constructs: GeneratedConstruct<
|
|
19
|
+
Cron<any, any, any, any, any, any, any, any>
|
|
20
|
+
>[],
|
|
19
21
|
outputDir: string,
|
|
20
22
|
options?: GeneratorOptions,
|
|
21
23
|
): Promise<CronInfo[]> {
|
|
@@ -48,6 +50,7 @@ export class CronGenerator extends ConstructGenerator<
|
|
|
48
50
|
),
|
|
49
51
|
schedule: construct.schedule || 'rate(1 hour)',
|
|
50
52
|
timeout: construct.timeout,
|
|
53
|
+
memorySize: construct.memorySize,
|
|
51
54
|
environment: await construct.getEnvironment(),
|
|
52
55
|
});
|
|
53
56
|
|
|
@@ -57,7 +60,9 @@ export class CronGenerator extends ConstructGenerator<
|
|
|
57
60
|
return cronInfos;
|
|
58
61
|
}
|
|
59
62
|
|
|
60
|
-
isConstruct(
|
|
63
|
+
isConstruct(
|
|
64
|
+
value: any,
|
|
65
|
+
): value is Cron<any, any, any, any, any, any, any, any> {
|
|
61
66
|
return Cron.isCron(value);
|
|
62
67
|
}
|
|
63
68
|
|