go-dev 0.2.1 → 0.3.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/README.md +12 -13
- package/package.json +5 -1
- package/src/config.js +4 -7
- package/src/orchestrator.js +11 -2
- package/src/process-manager.js +32 -20
- package/src/services/base.js +2 -1
- package/src/services/cmd.js +17 -18
- package/src/services/docker.js +45 -11
- package/test.js +0 -136
package/README.md
CHANGED
|
@@ -20,7 +20,9 @@ In complex monorepos, starting your development environment can be a chore. You
|
|
|
20
20
|
* **Preset-Driven Startup:** Define different "presets" (e.g., `api`, `frontend`, `all`) to easily spin up specific combinations of services tailored to your current development focus.
|
|
21
21
|
* **Automatic Dependency Resolution:** `go-dev` builds an intelligent execution graph, starting services in the correct topological order.
|
|
22
22
|
* **Centralized Logging:** Prefixes logs from each service, making it easy to follow activity from multiple concurrent processes.
|
|
23
|
-
* **
|
|
23
|
+
* **Automatic Process Exit:** The `go-dev` process will automatically exit when all primary services (those directly listed in the chosen preset) exit cleanly (with a success code of `0`).
|
|
24
|
+
* **Precise Docker Cleanup:** `go-dev` intelligently tracks which Docker Compose services it **actively started** (i.e., those that were not already running). During cleanup, it will only stop these specific services, leaving any pre-existing containers untouched.
|
|
25
|
+
* **Robust Cleanup:** Handles graceful shutdown of all started processes and Docker services on exit (e.g., via `Ctrl+C` or automatic exit).
|
|
24
26
|
|
|
25
27
|
## 🤔 Why `go-dev`?
|
|
26
28
|
|
|
@@ -60,7 +62,7 @@ npx go-dev frontend # Start the environment for Frontend development
|
|
|
60
62
|
npx go-dev all # Start the full development environment
|
|
61
63
|
```
|
|
62
64
|
|
|
63
|
-
|
|
65
|
+
`go-dev` will automatically exit once all primary services (those directly listed in your chosen preset) have completed their execution cleanly. If you need to stop `go-dev` before all services complete, possibly because you're running a web service, press `Ctrl+C` at any time to gracefully shut down all running services.
|
|
64
66
|
|
|
65
67
|
## ⚙️ Configuration (`go-dev.yml`)
|
|
66
68
|
|
|
@@ -86,15 +88,14 @@ services:
|
|
|
86
88
|
api:
|
|
87
89
|
type: hybrid # This service has multiple modes
|
|
88
90
|
defaultMode: dev # Default mode if not specified by a preset or dependency
|
|
89
|
-
# Note: The name of the modes is totally
|
|
91
|
+
# Note: The name of the modes is totally arbitrary
|
|
90
92
|
modes:
|
|
91
93
|
# API in active development mode (runs directly on host)
|
|
92
94
|
dev:
|
|
93
95
|
type: cmd # This mode runs a command-line process
|
|
94
96
|
commands:
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
directory: ./api # Directory to run the command from
|
|
97
|
+
command: [npx, rollup, -c, -w] # The primary command for development
|
|
98
|
+
directory: ./api # Directory to run the command from
|
|
98
99
|
dependencies: # What this mode depends on
|
|
99
100
|
- postgres # API dev needs PostgreSQL (will use postgres's default docker mode)
|
|
100
101
|
- { service: frontend, mode: serve } # API dev needs frontend running in its 'serve' mode
|
|
@@ -117,9 +118,8 @@ services:
|
|
|
117
118
|
dev:
|
|
118
119
|
type: cmd
|
|
119
120
|
commands:
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
directory: ./frontend
|
|
121
|
+
command: [npx, rollup, -c, -w]
|
|
122
|
+
directory: ./frontend
|
|
123
123
|
dependencies:
|
|
124
124
|
# Frontend dev needs API (will use api's default docker mode for this preset)
|
|
125
125
|
# Note: No direct circular dependency between dev modes.
|
|
@@ -129,12 +129,11 @@ services:
|
|
|
129
129
|
# Frontend serving its built assets (e.g., when API depends on it)
|
|
130
130
|
serve:
|
|
131
131
|
type: cmd
|
|
132
|
-
preCommands: # Commands to run and await completion BEFORE the main
|
|
132
|
+
preCommands: # Commands to run and await completion BEFORE the main command
|
|
133
133
|
- [npm, --prefix, frontend, run, build] # Build frontend assets first
|
|
134
134
|
commands:
|
|
135
|
-
start
|
|
136
|
-
|
|
137
|
-
directory: ./frontend
|
|
135
|
+
command: [node, ./localserver.mjs] # Then start the local server
|
|
136
|
+
directory: ./frontend
|
|
138
137
|
dependencies:
|
|
139
138
|
- api # Frontend serve needs API (will use api's default dev mode for this preset)
|
|
140
139
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "go-dev",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"main": "src/index.js",
|
|
5
5
|
"bin": {
|
|
6
6
|
"go-dev": "bin/go-dev"
|
|
@@ -9,6 +9,10 @@
|
|
|
9
9
|
"test": "echo \"Error: no test specified\" && exit 1"
|
|
10
10
|
},
|
|
11
11
|
"author": "Giuliano Collacchioni",
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "github",
|
|
14
|
+
"url": "https://github.com/Kal-Aster/go-dev"
|
|
15
|
+
},
|
|
12
16
|
"license": "MIT",
|
|
13
17
|
"description": "",
|
|
14
18
|
"dependencies": {
|
package/src/config.js
CHANGED
|
@@ -24,13 +24,10 @@ const commandConfigSchema = Joi.alternatives().try(
|
|
|
24
24
|
const cmdServiceConfigSchema = Joi.object({
|
|
25
25
|
type: Joi.string().valid('cmd').required(),
|
|
26
26
|
preCommands: Joi.array().items(commandConfigSchema).default([]),
|
|
27
|
-
commands: Joi.
|
|
28
|
-
|
|
29
|
-
Joi.
|
|
30
|
-
|
|
31
|
-
Joi.array().items(commandObjectSchema).min(1)
|
|
32
|
-
)
|
|
33
|
-
).min(1).required(),
|
|
27
|
+
commands: Joi.alternatives().try(
|
|
28
|
+
commandConfigSchema,
|
|
29
|
+
Joi.array().items(commandObjectSchema).min(1)
|
|
30
|
+
),
|
|
34
31
|
defaultCommand: Joi.string().default('start'),
|
|
35
32
|
directory: Joi.string(),
|
|
36
33
|
dependencies: Joi.array().items(dependencyEntrySchema).default([]),
|
package/src/orchestrator.js
CHANGED
|
@@ -43,12 +43,13 @@ class Orchestrator {
|
|
|
43
43
|
if (!ServiceClass) {
|
|
44
44
|
throw new Error(`Unknown service type '${config.type}' for service '${name}'.`);
|
|
45
45
|
}
|
|
46
|
-
const serviceInstance = new ServiceClass(name, mode, config);
|
|
46
|
+
const serviceInstance = new ServiceClass(name, mode, config, () => {});
|
|
47
47
|
this.activeServiceInstances.set(name, serviceInstance);
|
|
48
48
|
await serviceInstance.start();
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
console.log('\n--- Starting Primary Services ---');
|
|
52
|
+
const activePrimaryServices = new Map();
|
|
52
53
|
const primaryServicePromises = [];
|
|
53
54
|
for (const { name, mode, config } of primaryServices) {
|
|
54
55
|
if (this.activeServiceInstances.has(name)) {
|
|
@@ -59,8 +60,16 @@ class Orchestrator {
|
|
|
59
60
|
if (!ServiceClass) {
|
|
60
61
|
throw new Error(`Unknown service type '${config.type}' for service '${name}'.`);
|
|
61
62
|
}
|
|
62
|
-
const serviceInstance = new ServiceClass(name, mode, config)
|
|
63
|
+
const serviceInstance = new ServiceClass(name, mode, config, () => {
|
|
64
|
+
activePrimaryServices.delete(name);
|
|
65
|
+
if (activePrimaryServices.size > 0) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
this.cleanup();
|
|
70
|
+
});
|
|
63
71
|
this.activeServiceInstances.set(name, serviceInstance);
|
|
72
|
+
activePrimaryServices.set(name, serviceInstance);
|
|
64
73
|
primaryServicePromises.push(serviceInstance.start());
|
|
65
74
|
}
|
|
66
75
|
await Promise.all(primaryServicePromises);
|
package/src/process-manager.js
CHANGED
|
@@ -93,10 +93,12 @@ class ProcessManager {
|
|
|
93
93
|
* @param {string[]} args - Arguments for the command.
|
|
94
94
|
* @param {object} options - Options for spawn (e.g., cwd).
|
|
95
95
|
* @param {string} prefix - Prefix for stdout/stderr lines (e.g., 'frontend:').
|
|
96
|
-
* @param {boolean}
|
|
96
|
+
* @param {boolean} restartOnError - Whether to restart the process if it exits with non-zero code.
|
|
97
|
+
* @param {Function} onExit
|
|
98
|
+
* @param {ChildProcess[] | undefined} processReference - An array where to save the process to gain a reference to it.
|
|
97
99
|
* @returns {ChildProcess} The spawned child process instance.
|
|
98
100
|
*/
|
|
99
|
-
startManagedProcess(command, args, options, prefix,
|
|
101
|
+
startManagedProcess(command, args, options, prefix, restartOnError, onExit, processReference) {
|
|
100
102
|
if (this.cleanupInProgress) {
|
|
101
103
|
console.warn(`[ProcessManager] Skipping managed process '${command}' during cleanup.`);
|
|
102
104
|
return null;
|
|
@@ -107,13 +109,18 @@ class ProcessManager {
|
|
|
107
109
|
)} (prefix: ${prefix})`,
|
|
108
110
|
);
|
|
109
111
|
|
|
110
|
-
|
|
112
|
+
if (processReference == null) {
|
|
113
|
+
processReference = {}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const startedProcess = spawn(command, args, {
|
|
111
117
|
shell: true,
|
|
112
118
|
stdio: 'pipe',
|
|
113
119
|
...options,
|
|
114
120
|
});
|
|
121
|
+
processReference.process = startedProcess;
|
|
115
122
|
|
|
116
|
-
|
|
123
|
+
startedProcess.stdout.on('data', (data) => {
|
|
117
124
|
let prefixedData = data.toString().split('\n').map(line => `${prefix} ${line}`).join('\n');
|
|
118
125
|
if (prefixedData.endsWith(`${prefix} `)) {
|
|
119
126
|
prefixedData = prefixedData.slice(0, -(`${prefix} `).length);
|
|
@@ -124,7 +131,7 @@ class ProcessManager {
|
|
|
124
131
|
prefixedData = prefixedData.replace(/\x1b\[(?:2J|3J|H)/gi, '');
|
|
125
132
|
process.stdout.write(prefixedData);
|
|
126
133
|
});
|
|
127
|
-
|
|
134
|
+
startedProcess.stderr.on('data', (data) => {
|
|
128
135
|
let prefixedData = data.toString().split('\n').map(line => `${prefix} ${line}`).join('\n');
|
|
129
136
|
if (prefixedData.endsWith(`${prefix} `)) {
|
|
130
137
|
prefixedData = prefixedData.slice(0, -(`${prefix} `).length);
|
|
@@ -136,49 +143,54 @@ class ProcessManager {
|
|
|
136
143
|
process.stderr.write(prefixedData);
|
|
137
144
|
});
|
|
138
145
|
|
|
139
|
-
this.managedProcesses.add(
|
|
146
|
+
this.managedProcesses.add(startedProcess);
|
|
140
147
|
|
|
141
|
-
|
|
148
|
+
startedProcess.on('error', (err) => {
|
|
142
149
|
console.error(
|
|
143
150
|
`[ProcessManager] Error starting managed process '${command}': ${err.message}`,
|
|
144
151
|
);
|
|
145
|
-
this.managedProcesses.delete(
|
|
152
|
+
this.managedProcesses.delete(startedProcess);
|
|
146
153
|
});
|
|
147
154
|
|
|
148
|
-
|
|
149
|
-
if (!this.managedProcesses.has(
|
|
155
|
+
startedProcess.on('exit', (code) => {
|
|
156
|
+
if (!this.managedProcesses.has(startedProcess)) {
|
|
150
157
|
return;
|
|
151
158
|
}
|
|
152
159
|
|
|
153
|
-
this.managedProcesses.delete(
|
|
160
|
+
this.managedProcesses.delete(startedProcess);
|
|
154
161
|
if (this.cleanupInProgress) {
|
|
155
162
|
console.log(
|
|
156
|
-
`[ProcessManager] Managed process '${command}' (PID: ${
|
|
163
|
+
`[ProcessManager] Managed process '${command}' (PID: ${startedProcess.pid}) exited due to cleanup.`,
|
|
157
164
|
);
|
|
158
165
|
return;
|
|
159
166
|
}
|
|
160
167
|
|
|
161
168
|
if (code !== 0) {
|
|
162
169
|
console.error(
|
|
163
|
-
`[ProcessManager] Managed process '${command}' (PID: ${
|
|
170
|
+
`[ProcessManager] Managed process '${command}' (PID: ${startedProcess.pid}) exited with code ${code}.`,
|
|
164
171
|
);
|
|
165
|
-
if (
|
|
172
|
+
if (restartOnError) {
|
|
166
173
|
console.warn(
|
|
167
174
|
`[ProcessManager] Restarting managed process '${command}'...`,
|
|
168
175
|
);
|
|
169
|
-
this.startManagedProcess(command, args, options, prefix,
|
|
176
|
+
this.startManagedProcess(command, args, options, prefix, restartOnError, onExit, processReference);
|
|
170
177
|
}
|
|
171
178
|
} else {
|
|
172
179
|
console.log(
|
|
173
|
-
`[ProcessManager] Managed process '${command}' (PID: ${
|
|
180
|
+
`[ProcessManager] Managed process '${command}' (PID: ${startedProcess.pid}) exited cleanly.`,
|
|
174
181
|
);
|
|
182
|
+
onExit?.();
|
|
175
183
|
}
|
|
176
184
|
});
|
|
177
185
|
|
|
178
|
-
return
|
|
186
|
+
return processReference;
|
|
179
187
|
}
|
|
180
188
|
|
|
181
189
|
killProcess(process) {
|
|
190
|
+
if (process.killed || process.exitCode != null) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
182
194
|
this.managedProcesses.delete(process);
|
|
183
195
|
|
|
184
196
|
return new Promise((resolve, reject) => {
|
|
@@ -229,7 +241,7 @@ class ProcessManager {
|
|
|
229
241
|
}
|
|
230
242
|
for (let index = this.managedProcesses.length - 1; index >= 0; index--) {
|
|
231
243
|
const process = this.managedProcesses[index];
|
|
232
|
-
if (process.killed) {
|
|
244
|
+
if (process.killed || process.exitCode != null) {
|
|
233
245
|
this.managedProcesses.splice(index, 1);
|
|
234
246
|
}
|
|
235
247
|
}
|
|
@@ -239,7 +251,7 @@ class ProcessManager {
|
|
|
239
251
|
console.log('\n[ProcessManager] Initiating cleanup of managed processes...');
|
|
240
252
|
|
|
241
253
|
for (const proc of [...this.managedProcesses]) {
|
|
242
|
-
if (proc.killed) {
|
|
254
|
+
if (proc.killed || proc.exitCode != null) {
|
|
243
255
|
continue;
|
|
244
256
|
}
|
|
245
257
|
|
|
@@ -253,7 +265,7 @@ class ProcessManager {
|
|
|
253
265
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
254
266
|
|
|
255
267
|
for (const proc of [...this.managedProcesses]) {
|
|
256
|
-
if (proc.killed) {
|
|
268
|
+
if (proc.killed || proc.exitCode != null) {
|
|
257
269
|
continue;
|
|
258
270
|
}
|
|
259
271
|
|
package/src/services/base.js
CHANGED
|
@@ -28,10 +28,11 @@ class BaseService {
|
|
|
28
28
|
* @param {string} mode - The resolved mode for this service (e.g., 'dev', 'docker', 'serve').
|
|
29
29
|
* @param {object} config - The concrete configuration object for this service and mode.
|
|
30
30
|
*/
|
|
31
|
-
constructor(name, mode, config) {
|
|
31
|
+
constructor(name, mode, config, onExit) {
|
|
32
32
|
this.name = name;
|
|
33
33
|
this.mode = mode;
|
|
34
34
|
this.config = config;
|
|
35
|
+
this.onExit = onExit;
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
async start() {
|
package/src/services/cmd.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
const { BaseService } = require('./base');
|
|
2
2
|
|
|
3
3
|
class CmdService extends BaseService {
|
|
4
|
-
constructor(name, mode, config) {
|
|
5
|
-
super(name, mode, config);
|
|
4
|
+
constructor(name, mode, config, onExit) {
|
|
5
|
+
super(name, mode, config, onExit);
|
|
6
6
|
this.prefix = `${name}:${mode}:`;
|
|
7
7
|
this.processes = [];
|
|
8
8
|
}
|
|
@@ -10,7 +10,12 @@ class CmdService extends BaseService {
|
|
|
10
10
|
async start() {
|
|
11
11
|
console.log(`[${this.name}:${this.mode}] Starting cmd service...`);
|
|
12
12
|
|
|
13
|
-
const { preCommands, commands
|
|
13
|
+
const { preCommands, commands } = this.config;
|
|
14
|
+
if (!commands) {
|
|
15
|
+
throw new Error(
|
|
16
|
+
`[${this.name}:${this.mode}] Commands not found for service.`,
|
|
17
|
+
);
|
|
18
|
+
}
|
|
14
19
|
|
|
15
20
|
if (preCommands && preCommands.length > 0) {
|
|
16
21
|
console.log(`[${this.name}:${this.mode}] Running pre-commands...`);
|
|
@@ -36,21 +41,14 @@ class CmdService extends BaseService {
|
|
|
36
41
|
console.log(`[${this.name}:${this.mode}] Pre-commands completed.`);
|
|
37
42
|
}
|
|
38
43
|
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
`[${this.name}:${this.mode}] Default command '${defaultCommand}' not found for service.`,
|
|
43
|
-
);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const { cmdArgs, directory } = (Array.isArray(mainCommand) && typeof mainCommand[0] === 'string' ?
|
|
47
|
-
{ cmdArgs: [mainCommand], directory: [undefined] } :
|
|
48
|
-
(Array.isArray(mainCommand) ?
|
|
44
|
+
const { cmdArgs, directory } = (Array.isArray(commands) && typeof commands[0] === 'string' ?
|
|
45
|
+
{ cmdArgs: [commands], directory: [undefined] } :
|
|
46
|
+
(Array.isArray(commands) ?
|
|
49
47
|
{
|
|
50
|
-
cmdArgs:
|
|
51
|
-
directory:
|
|
48
|
+
cmdArgs: commands.map(({ command }) => command),
|
|
49
|
+
directory: commands.map(({ directory }) => directory),
|
|
52
50
|
} :
|
|
53
|
-
{ cmdArgs: [
|
|
51
|
+
{ cmdArgs: [commands.command], directory: [commands.directory] }
|
|
54
52
|
)
|
|
55
53
|
);
|
|
56
54
|
|
|
@@ -68,6 +66,7 @@ class CmdService extends BaseService {
|
|
|
68
66
|
this.prefix
|
|
69
67
|
),
|
|
70
68
|
true,
|
|
69
|
+
() => this.onExit?.()
|
|
71
70
|
);
|
|
72
71
|
|
|
73
72
|
if (!process) {
|
|
@@ -78,13 +77,13 @@ class CmdService extends BaseService {
|
|
|
78
77
|
|
|
79
78
|
this.processes.push(process);
|
|
80
79
|
console.log(
|
|
81
|
-
`[${this.name}:${this.mode}] Process started (PID: ${process.pid}).`,
|
|
80
|
+
`[${this.name}:${this.mode}] Process started (PID: ${process.process.pid}).`,
|
|
82
81
|
);
|
|
83
82
|
}
|
|
84
83
|
}
|
|
85
84
|
|
|
86
85
|
async stop() {
|
|
87
|
-
const promises = this.processes.map(process => {
|
|
86
|
+
const promises = this.processes.map(({ process }) => {
|
|
88
87
|
console.log(`[${this.name}:${this.mode}] Stopping process (PID: ${process.pid}).`);
|
|
89
88
|
return CmdService._processManager.killProcess(process);
|
|
90
89
|
});
|
package/src/services/docker.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
const { BaseService } = require('./base');
|
|
2
2
|
|
|
3
3
|
class DockerService extends BaseService {
|
|
4
|
-
/** @type {
|
|
5
|
-
static
|
|
4
|
+
/** @type {Map<string, string[]>} */
|
|
5
|
+
static _servicesToStop = new Map();
|
|
6
6
|
|
|
7
7
|
static async cleanup() {
|
|
8
8
|
if (!DockerService._processManager) {
|
|
@@ -10,23 +10,23 @@ class DockerService extends BaseService {
|
|
|
10
10
|
return;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
if (DockerService.
|
|
13
|
+
if (DockerService._servicesToStop.size > 0) {
|
|
14
14
|
console.log('[DockerService] Stopping all docker services from used compose files...');
|
|
15
|
-
for (const composeFile of
|
|
15
|
+
for (const [composeFile, services] of this._servicesToStop.entries()) {
|
|
16
16
|
try {
|
|
17
|
-
console.log(`[DockerService]
|
|
17
|
+
console.log(`[DockerService] Stopping services ${services.map(service => `'${service}'`).join(', ')} of '${composeFile}'`);
|
|
18
18
|
DockerService._processManager.runSync(
|
|
19
19
|
'docker',
|
|
20
|
-
['compose', '-f', composeFile, 'stop'],
|
|
20
|
+
['compose', '-f', composeFile, 'stop', ...services],
|
|
21
21
|
{ stdio: 'inherit' },
|
|
22
22
|
);
|
|
23
23
|
} catch (error) {
|
|
24
24
|
console.error(
|
|
25
|
-
`[DockerService] Failed to
|
|
25
|
+
`[DockerService] Failed to stop services of '${composeFile}': ${error.message}`,
|
|
26
26
|
);
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
|
-
DockerService.
|
|
29
|
+
DockerService._servicesToStop.clear();
|
|
30
30
|
} else {
|
|
31
31
|
console.log('[DockerService] No docker services were started by orchestrator, skipping global stop.');
|
|
32
32
|
}
|
|
@@ -47,16 +47,33 @@ class DockerService extends BaseService {
|
|
|
47
47
|
console.log(
|
|
48
48
|
`[${this.name}:${this.mode}] Docker container for '${this.dockerServiceName}' is already running.`,
|
|
49
49
|
);
|
|
50
|
-
DockerService._composeFilesToStop.add(this.dockerComposeFile);
|
|
51
50
|
} else {
|
|
52
51
|
console.log(`[${this.name}:${this.mode}] Bringing up docker service '${this.dockerServiceName}'...`);
|
|
53
52
|
try {
|
|
53
|
+
const servicesBeforeStart = this._getCurrentlyRunningServices();
|
|
54
54
|
await DockerService._processManager.runInherited(
|
|
55
55
|
'docker',
|
|
56
56
|
['compose', '-f', this.dockerComposeFile, 'up', this.dockerServiceName, '-d'],
|
|
57
57
|
);
|
|
58
|
-
|
|
58
|
+
const servicesAfterStart = this._getCurrentlyRunningServices();
|
|
59
|
+
let servicesOfComposeFile = DockerService._servicesToStop.get(this.dockerComposeFile);
|
|
60
|
+
if (servicesOfComposeFile == null) {
|
|
61
|
+
servicesOfComposeFile = [];
|
|
62
|
+
}
|
|
63
|
+
const newServices = servicesAfterStart.filter(service => {
|
|
64
|
+
return !(
|
|
65
|
+
servicesBeforeStart.includes(service) ||
|
|
66
|
+
servicesOfComposeFile.includes(service)
|
|
67
|
+
);
|
|
68
|
+
});
|
|
69
|
+
DockerService._servicesToStop.set(
|
|
70
|
+
this.dockerComposeFile,
|
|
71
|
+
servicesOfComposeFile.concat(newServices)
|
|
72
|
+
);
|
|
59
73
|
console.log(`[${this.name}:${this.mode}] Docker service '${this.dockerServiceName}' brought up.`);
|
|
74
|
+
if (newServices.length > 1) {
|
|
75
|
+
console.log(`[${this.name}:${this.mode}] Dependency service${newServices.length > 2 ? 's' : ''} for '${this.dockerServiceName}': ${newServices.filter(service => service.name !== this.dockerServiceName).join(', ')}`);
|
|
76
|
+
}
|
|
60
77
|
} catch (error) {
|
|
61
78
|
throw new Error(
|
|
62
79
|
`[${this.name}:${this.mode}] Failed to bring up docker service '${this.dockerServiceName}': ${error.message}`,
|
|
@@ -88,6 +105,23 @@ class DockerService extends BaseService {
|
|
|
88
105
|
return await this._checkServiceHealthiness();
|
|
89
106
|
}
|
|
90
107
|
|
|
108
|
+
_getCurrentlyRunningServices() {
|
|
109
|
+
try {
|
|
110
|
+
const services = DockerService._processManager.runSync(
|
|
111
|
+
'docker',
|
|
112
|
+
['compose', '-f', this.dockerComposeFile, 'ps', '--services'],
|
|
113
|
+
);
|
|
114
|
+
return (services
|
|
115
|
+
.split('\n')
|
|
116
|
+
.map(serviceName => serviceName.trim())
|
|
117
|
+
.filter(serviceName => serviceName !== '')
|
|
118
|
+
);
|
|
119
|
+
} catch (error) {
|
|
120
|
+
// console.warn(...);
|
|
121
|
+
}
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
|
|
91
125
|
_getContainerName() {
|
|
92
126
|
if (this.containerName) {
|
|
93
127
|
return this.containerName;
|
|
@@ -143,7 +177,7 @@ class DockerService extends BaseService {
|
|
|
143
177
|
);
|
|
144
178
|
|
|
145
179
|
if (healthStatus === 'healthy' || healthStatus === 'none') {
|
|
146
|
-
console.log(`[${this.name}:${this.mode}] Container '${containerName}'
|
|
180
|
+
console.log(`[${this.name}:${this.mode}] Container '${containerName}' healthy!`);
|
|
147
181
|
return true;
|
|
148
182
|
}
|
|
149
183
|
|
package/test.js
DELETED
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
const { loadConfig } = require('./src/config');
|
|
2
|
-
const { resolveServiceExecutionGraph } = require('./src/dependency-resolver');
|
|
3
|
-
const fs = require('fs');
|
|
4
|
-
|
|
5
|
-
const testConfigContent = `
|
|
6
|
-
composeFile: infrastructure/docker-compose.yml
|
|
7
|
-
|
|
8
|
-
services:
|
|
9
|
-
postgres:
|
|
10
|
-
type: docker
|
|
11
|
-
service: postgres
|
|
12
|
-
healthCheck: true
|
|
13
|
-
|
|
14
|
-
api:
|
|
15
|
-
type: hybrid
|
|
16
|
-
defaultMode: dev
|
|
17
|
-
modes:
|
|
18
|
-
dev:
|
|
19
|
-
type: cmd
|
|
20
|
-
commands:
|
|
21
|
-
start: [npx, rollup, -c, -w]
|
|
22
|
-
directory: ./api
|
|
23
|
-
dependencies:
|
|
24
|
-
- postgres
|
|
25
|
-
- { service: frontend, mode: serve }
|
|
26
|
-
|
|
27
|
-
docker:
|
|
28
|
-
type: docker
|
|
29
|
-
service: api
|
|
30
|
-
healthCheck: true
|
|
31
|
-
dependencies:
|
|
32
|
-
- postgres
|
|
33
|
-
|
|
34
|
-
frontend:
|
|
35
|
-
type: hybrid
|
|
36
|
-
defaultMode: dev
|
|
37
|
-
modes:
|
|
38
|
-
dev:
|
|
39
|
-
type: cmd
|
|
40
|
-
commands:
|
|
41
|
-
start: [npx, rollup, -c, -w]
|
|
42
|
-
directory: ./frontend
|
|
43
|
-
dependencies:
|
|
44
|
-
- { service: api, mode: docker }
|
|
45
|
-
|
|
46
|
-
serve:
|
|
47
|
-
type: cmd
|
|
48
|
-
preCommands:
|
|
49
|
-
- [npm, --prefix, frontend, run, build]
|
|
50
|
-
commands:
|
|
51
|
-
start: [node, ./localserver.mjs]
|
|
52
|
-
directory: ./frontend
|
|
53
|
-
dependencies:
|
|
54
|
-
- api
|
|
55
|
-
|
|
56
|
-
presets:
|
|
57
|
-
api:
|
|
58
|
-
services: [api]
|
|
59
|
-
|
|
60
|
-
frontend:
|
|
61
|
-
services: [frontend]
|
|
62
|
-
|
|
63
|
-
all:
|
|
64
|
-
services: [api, frontend]
|
|
65
|
-
|
|
66
|
-
# Example of a preset that would cause a cycle if dependencies were set up naively
|
|
67
|
-
# (and without the \`dev\` modes being peers)
|
|
68
|
-
# cycle-test:
|
|
69
|
-
# services: [api, frontend]
|
|
70
|
-
# modes:
|
|
71
|
-
# api: dev
|
|
72
|
-
# frontend: cycle-dev # Imagine 'cycle-dev' depends on 'api:dev'
|
|
73
|
-
# # To trigger a cycle, you'd need api:dev -> frontend:dev and frontend:dev -> api:dev
|
|
74
|
-
|
|
75
|
-
`;
|
|
76
|
-
|
|
77
|
-
const testConfigPath = 'temp-dev-orchestrator.yml';
|
|
78
|
-
fs.writeFileSync(testConfigPath, testConfigContent);
|
|
79
|
-
|
|
80
|
-
console.log('--- Testing Dependency Resolver ---');
|
|
81
|
-
|
|
82
|
-
try {
|
|
83
|
-
const config = loadConfig(testConfigPath);
|
|
84
|
-
console.log('✅ Config loaded successfully');
|
|
85
|
-
|
|
86
|
-
console.log('\n--- Resolving "api" preset ---');
|
|
87
|
-
const apiExecutionGraph = resolveServiceExecutionGraph(config, 'api');
|
|
88
|
-
console.log('✅ "api" preset resolved successfully.');
|
|
89
|
-
console.log('Dependencies:');
|
|
90
|
-
apiExecutionGraph.dependencies.forEach((node) => {
|
|
91
|
-
console.log(` - ${node.name} (mode: ${node.mode})`);
|
|
92
|
-
});
|
|
93
|
-
console.log('Services:');
|
|
94
|
-
apiExecutionGraph.services.forEach((node) => {
|
|
95
|
-
console.log(` - ${node.name} (mode: ${node.mode})`);
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
console.log('\n--- Resolving "frontend" preset ---');
|
|
99
|
-
const frontendExecutionGraph = resolveServiceExecutionGraph(config, 'frontend');
|
|
100
|
-
console.log('✅ "frontend" preset resolved successfully.');
|
|
101
|
-
console.log('Dependencies:');
|
|
102
|
-
frontendExecutionGraph.dependencies.forEach((node) => {
|
|
103
|
-
console.log(` - ${node.name} (mode: ${node.mode})`);
|
|
104
|
-
});
|
|
105
|
-
console.log('Services:');
|
|
106
|
-
frontendExecutionGraph.services.forEach((node) => {
|
|
107
|
-
console.log(` - ${node.name} (mode: ${node.mode})`);
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
console.log('\n--- Resolving "all" preset ---');
|
|
111
|
-
const allExecutionGraph = resolveServiceExecutionGraph(config, 'all');
|
|
112
|
-
console.log('✅ "all" preset resolved successfully.');
|
|
113
|
-
console.log('Dependencies:');
|
|
114
|
-
allExecutionGraph.dependencies.forEach((node) => {
|
|
115
|
-
console.log(` - ${node.name} (mode: ${node.mode})`);
|
|
116
|
-
});
|
|
117
|
-
console.log('Services:');
|
|
118
|
-
allExecutionGraph.services.forEach((node) => {
|
|
119
|
-
console.log(` - ${node.name} (mode: ${node.mode})`);
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
console.log('\n--- Testing non-existent preset ---');
|
|
123
|
-
try {
|
|
124
|
-
resolveServiceExecutionGraph(config, 'non-existent');
|
|
125
|
-
} catch (error) {
|
|
126
|
-
console.log(`✅ Caught expected error: ${error.message}`);
|
|
127
|
-
}
|
|
128
|
-
} catch (error) {
|
|
129
|
-
console.error('❌ An unexpected error occurred during testing:', error);
|
|
130
|
-
} finally {
|
|
131
|
-
// Clean up the temporary config file
|
|
132
|
-
if (fs.existsSync(testConfigPath)) {
|
|
133
|
-
fs.unlinkSync(testConfigPath);
|
|
134
|
-
console.log(`\nCleaned up temporary config file: ${testConfigPath}`);
|
|
135
|
-
}
|
|
136
|
-
}
|