Skip to content

Commit e0a0582

Browse files
authored
Enable Hot-reload for CLI (#659)
* add vite-node for hot reload add precheck script to check all the ports are running are not * skip the service start when service is already started * introduce a cli-dev mode in dev * refactor: streamline port conflict checking in precheck
1 parent c42476c commit e0a0582

File tree

5 files changed

+485
-29
lines changed

5 files changed

+485
-29
lines changed

cli/package.json

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
"type": "module",
1010
"scripts": {
1111
"build": "tsc",
12-
"dev": "tsc --watch",
12+
"predev": "[ -e ../.env ] && set -a && . ../.env; vite-node source/precheck.ts",
13+
"dev": "vite-node --watch source/cli.tsx",
1314
"test": "yarn link-fix && ava",
1415
"lint": "eslint source/**/*.{js,ts,tsx}",
1516
"lint-fix": "eslint --fix --ignore-pattern 'node_modules/*' 'source/**/*.{js,ts,tsx}'",
@@ -49,7 +50,8 @@
4950
"ink-testing-library": "^3.0.0",
5051
"prettier": "^3.0.3",
5152
"ts-node": "^10.9.1",
52-
"typescript": "5.3.3"
53+
"typescript": "5.3.3",
54+
"vite-node": "^3.1.3"
5355
},
5456
"ava": {
5557
"extensions": {
@@ -59,5 +61,6 @@
5961
"nodeArguments": [
6062
"--loader=ts-node/esm"
6163
]
62-
}
64+
},
65+
"packageManager": "[email protected]"
6366
}

cli/source/app.tsx

Lines changed: 41 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ const CliUi = () => {
5555
const db_pass = process.env['DB_PASS'];
5656
const redis_port = process.env['REDIS_PORT'];
5757
const redis_host = process.env['REDIS_HOST'];
58+
const services_started = process.env['SERVICES_STARTED'];
5859

5960
const preCheck = usePreCheck({
6061
db: Number(db_port),
@@ -106,21 +107,22 @@ const CliUi = () => {
106107
exit();
107108
}
108109
await dispatch(appSlice.actions.setAppState(AppStates.TEARDOWN));
109-
setTimeout(() => {
110-
if (!processRef.current) return;
111-
110+
if (processRef.current) {
112111
processRef.current.kill();
113112
processRef.current.stdout.destroy();
114113
processRef.current.stderr.destroy();
114+
await new Promise((r) => setTimeout(r, 50));
115+
}
116+
// dont depend on processRef.current here, as it might be null
117+
// so always do a docker compose down irrespective of processRef.current
118+
try {
119+
await runCommand('docker', ['compose', 'down'], runCommandOpts).promise;
120+
} catch {
121+
await runCommand('docker-compose', ['down'], runCommandOpts).promise;
122+
}
115123

116-
runCommand('docker', ['compose', 'down'], runCommandOpts)
117-
.promise.catch(async (err: any) => {
118-
await runCommand('docker-compose', ['down']).promise;
119-
})
120-
.finally(async () => {
121-
await dispatch(appSlice.actions.setAppState(AppStates.TERMINATED));
122-
});
123-
}, 200);
124+
await dispatch(appSlice.actions.setAppState(AppStates.TERMINATED));
125+
exit();
124126
}, [appState, dispatch, runCommandOpts]);
125127

126128
const handleVersionUpdates = useCallback(async () => {
@@ -169,11 +171,32 @@ const CliUi = () => {
169171
useEffect(() => {
170172
if (Object.values(preCheck).includes(PreCheckStates.RUNNING)) {
171173
return;
172-
} else {
173-
if (Object.values(preCheck).includes(PreCheckStates.FAILED)) {
174-
handleExit();
175-
return;
176-
}
174+
}
175+
/*
176+
just a reminder that we make the ports check for optimizing hot reloading
177+
so a FAILED port check shall imply that some ports are already in use
178+
if the ports are already in use, we will not start the services
179+
FAILING the ports check will not stop the app from starting, because the assumption is that
180+
the user has already started the services and they are running
181+
*/
182+
183+
if (
184+
preCheck.ports === PreCheckStates.FAILED &&
185+
services_started === 'true'
186+
) {
187+
dispatch(appSlice.actions.setAppState(AppStates.DOCKER_READY));
188+
dispatch(appSlice.actions.updateReadyServices(LogSource.WebServer));
189+
dispatch(appSlice.actions.updateReadyServices(LogSource.ApiServer));
190+
dispatch(appSlice.actions.updateReadyServices(LogSource.SyncServer));
191+
dispatch(appSlice.actions.updateReadyServices(LogSource.Postgres));
192+
dispatch(appSlice.actions.updateReadyServices(LogSource.InitDb));
193+
dispatch(appSlice.actions.updateReadyServices(LogSource.Redis));
194+
return;
195+
}
196+
197+
if (Object.values(preCheck).includes(PreCheckStates.FAILED)) {
198+
handleExit();
199+
return;
177200
}
178201

179202
dispatch(appSlice.actions.setAppState(AppStates.INIT));
@@ -197,6 +220,8 @@ const CliUi = () => {
197220
})
198221
);
199222
});
223+
// set the env variable APP_STARTED to true
224+
globalThis.process.env['SERVICES_STARTED'] = 'true';
200225
})
201226
.catch(async (err: any) => {
202227
await runCommand('docker-compose', ['down']).promise;
@@ -406,13 +431,6 @@ const CliUi = () => {
406431
'Docker daemon is not running, please ensure it is running. To check if your docker daemon is running, use the command: \ndocker info'
407432
}
408433
/>
409-
<PreCheckDisplayElement
410-
value={preCheck.ports}
411-
property={PreCheckProperties.PORTS}
412-
errHelp={
413-
'One of the ports is not free. Please ensure that all the specified ports in dockerfile.dev are free.'
414-
}
415-
/>
416434
<PreCheckDisplayElement
417435
value={preCheck.composeFile}
418436
property={PreCheckProperties.COMPOSE_FILE}

cli/source/precheck.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { isFreePort } from 'find-free-ports';
2+
3+
async function run(): Promise<void> {
4+
const { DB_PORT, REDIS_PORT, PORT, SYNC_SERVER_PORT, ANALYTICS_SERVER_PORT } =
5+
process.env;
6+
7+
const rawPorts = [
8+
DB_PORT,
9+
REDIS_PORT,
10+
PORT,
11+
SYNC_SERVER_PORT,
12+
ANALYTICS_SERVER_PORT
13+
];
14+
const ports: number[] = rawPorts.map((p, i) => {
15+
const num = Number(p);
16+
if (typeof p !== 'string' || Number.isNaN(num)) {
17+
console.error(`❌ Invalid or missing port value at index ${i}:`, p);
18+
process.exit(1);
19+
}
20+
return num;
21+
});
22+
23+
try {
24+
const results = await Promise.allSettled(ports.map(isFreePort));
25+
results.forEach((result, i) => {
26+
const port = ports[i];
27+
if (result.status === 'rejected') {
28+
console.error(`❌ Failed to check port ${port}:`, result.reason);
29+
process.exit(1);
30+
} else if (!result.value) {
31+
console.error(`❌ Port ${port} is already in use.`);
32+
process.exit(1);
33+
}
34+
});
35+
36+
console.log('✅ All ports are free; starting app...');
37+
} catch (err) {
38+
console.error('❌ Error checking ports:', err);
39+
process.exit(1);
40+
}
41+
}
42+
43+
run();

0 commit comments

Comments
 (0)