go-dev 0.2.0 → 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 -22
- 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);
|
|
@@ -123,9 +130,8 @@ class ProcessManager {
|
|
|
123
130
|
}
|
|
124
131
|
prefixedData = prefixedData.replace(/\x1b\[(?:2J|3J|H)/gi, '');
|
|
125
132
|
process.stdout.write(prefixedData);
|
|
126
|
-
appendFileSync('./banana.log', prefixedData);
|
|
127
133
|
});
|
|
128
|
-
|
|
134
|
+
startedProcess.stderr.on('data', (data) => {
|
|
129
135
|
let prefixedData = data.toString().split('\n').map(line => `${prefix} ${line}`).join('\n');
|
|
130
136
|
if (prefixedData.endsWith(`${prefix} `)) {
|
|
131
137
|
prefixedData = prefixedData.slice(0, -(`${prefix} `).length);
|
|
@@ -135,52 +141,56 @@ class ProcessManager {
|
|
|
135
141
|
}
|
|
136
142
|
prefixedData = prefixedData.replace(/\x1b\[(?:2J|H)/gi, '');
|
|
137
143
|
process.stderr.write(prefixedData);
|
|
138
|
-
appendFileSync('./banana.log', prefixedData);
|
|
139
144
|
});
|
|
140
145
|
|
|
141
|
-
this.managedProcesses.add(
|
|
146
|
+
this.managedProcesses.add(startedProcess);
|
|
142
147
|
|
|
143
|
-
|
|
148
|
+
startedProcess.on('error', (err) => {
|
|
144
149
|
console.error(
|
|
145
150
|
`[ProcessManager] Error starting managed process '${command}': ${err.message}`,
|
|
146
151
|
);
|
|
147
|
-
this.managedProcesses.delete(
|
|
152
|
+
this.managedProcesses.delete(startedProcess);
|
|
148
153
|
});
|
|
149
154
|
|
|
150
|
-
|
|
151
|
-
if (!this.managedProcesses.has(
|
|
155
|
+
startedProcess.on('exit', (code) => {
|
|
156
|
+
if (!this.managedProcesses.has(startedProcess)) {
|
|
152
157
|
return;
|
|
153
158
|
}
|
|
154
159
|
|
|
155
|
-
this.managedProcesses.delete(
|
|
160
|
+
this.managedProcesses.delete(startedProcess);
|
|
156
161
|
if (this.cleanupInProgress) {
|
|
157
162
|
console.log(
|
|
158
|
-
`[ProcessManager] Managed process '${command}' (PID: ${
|
|
163
|
+
`[ProcessManager] Managed process '${command}' (PID: ${startedProcess.pid}) exited due to cleanup.`,
|
|
159
164
|
);
|
|
160
165
|
return;
|
|
161
166
|
}
|
|
162
167
|
|
|
163
168
|
if (code !== 0) {
|
|
164
169
|
console.error(
|
|
165
|
-
`[ProcessManager] Managed process '${command}' (PID: ${
|
|
170
|
+
`[ProcessManager] Managed process '${command}' (PID: ${startedProcess.pid}) exited with code ${code}.`,
|
|
166
171
|
);
|
|
167
|
-
if (
|
|
172
|
+
if (restartOnError) {
|
|
168
173
|
console.warn(
|
|
169
174
|
`[ProcessManager] Restarting managed process '${command}'...`,
|
|
170
175
|
);
|
|
171
|
-
this.startManagedProcess(command, args, options, prefix,
|
|
176
|
+
this.startManagedProcess(command, args, options, prefix, restartOnError, onExit, processReference);
|
|
172
177
|
}
|
|
173
178
|
} else {
|
|
174
179
|
console.log(
|
|
175
|
-
`[ProcessManager] Managed process '${command}' (PID: ${
|
|
180
|
+
`[ProcessManager] Managed process '${command}' (PID: ${startedProcess.pid}) exited cleanly.`,
|
|
176
181
|
);
|
|
182
|
+
onExit?.();
|
|
177
183
|
}
|
|
178
184
|
});
|
|
179
185
|
|
|
180
|
-
return
|
|
186
|
+
return processReference;
|
|
181
187
|
}
|
|
182
188
|
|
|
183
189
|
killProcess(process) {
|
|
190
|
+
if (process.killed || process.exitCode != null) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
184
194
|
this.managedProcesses.delete(process);
|
|
185
195
|
|
|
186
196
|
return new Promise((resolve, reject) => {
|
|
@@ -231,7 +241,7 @@ class ProcessManager {
|
|
|
231
241
|
}
|
|
232
242
|
for (let index = this.managedProcesses.length - 1; index >= 0; index--) {
|
|
233
243
|
const process = this.managedProcesses[index];
|
|
234
|
-
if (process.killed) {
|
|
244
|
+
if (process.killed || process.exitCode != null) {
|
|
235
245
|
this.managedProcesses.splice(index, 1);
|
|
236
246
|
}
|
|
237
247
|
}
|
|
@@ -241,7 +251,7 @@ class ProcessManager {
|
|
|
241
251
|
console.log('\n[ProcessManager] Initiating cleanup of managed processes...');
|
|
242
252
|
|
|
243
253
|
for (const proc of [...this.managedProcesses]) {
|
|
244
|
-
if (proc.killed) {
|
|
254
|
+
if (proc.killed || proc.exitCode != null) {
|
|
245
255
|
continue;
|
|
246
256
|
}
|
|
247
257
|
|
|
@@ -255,7 +265,7 @@ class ProcessManager {
|
|
|
255
265
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
256
266
|
|
|
257
267
|
for (const proc of [...this.managedProcesses]) {
|
|
258
|
-
if (proc.killed) {
|
|
268
|
+
if (proc.killed || proc.exitCode != null) {
|
|
259
269
|
continue;
|
|
260
270
|
}
|
|
261
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
|
-
}
|