buncargo 1.0.29 → 3.0.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/bin.d.ts +1 -12
- package/dist/bin.js +261 -253
- package/dist/cli/bin.d.ts +13 -0
- package/dist/cli/bin.js +315 -0
- package/dist/cli/commands/help.d.ts +1 -0
- package/dist/cli/commands/runtime.d.ts +5 -0
- package/dist/cli/commands/version.d.ts +1 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +14 -0
- package/dist/cli/run-cli.d.ts +22 -0
- package/dist/cli.d.ts +1 -22
- package/dist/cli.js +5 -13
- package/dist/config/config.d.ts +1 -0
- package/dist/config/define-config.d.ts +13 -0
- package/dist/config/index.d.ts +3 -0
- package/dist/config/index.js +15 -0
- package/dist/config/merge-configs.d.ts +3 -0
- package/dist/config/validate-config.d.ts +3 -0
- package/dist/config.d.ts +1 -72
- package/dist/config.js +12 -12
- package/dist/core/docker.d.ts +1 -83
- package/dist/core/docker.js +35 -32
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.js +123 -118
- package/dist/core/network.js +2 -2
- package/dist/core/ports.js +1 -1
- package/dist/core/process.js +1 -1
- package/dist/core/tunnel.d.ts +33 -0
- package/dist/core/utils.js +2 -2
- package/dist/core/watchdog-runner.js +45 -42
- package/dist/core/watchdog.d.ts +1 -0
- package/dist/core/watchdog.js +4 -2
- package/dist/docker/index.d.ts +1 -0
- package/dist/docker/index.js +38 -0
- package/dist/docker/runtime.d.ts +87 -0
- package/dist/docker/runtime.js +37 -0
- package/dist/docker-compose/compose.d.ts +1 -0
- package/dist/docker-compose/generated-file.d.ts +7 -0
- package/dist/docker-compose/index.d.ts +3 -0
- package/dist/docker-compose/index.js +15 -0
- package/dist/docker-compose/model.d.ts +6 -0
- package/dist/docker-compose/services/clickhouse.d.ts +16 -0
- package/dist/docker-compose/services/define-docker-service.d.ts +41 -0
- package/dist/docker-compose/services/index.d.ts +23 -0
- package/dist/docker-compose/services/index.js +17 -0
- package/dist/docker-compose/services/postgres.d.ts +12 -0
- package/dist/docker-compose/services/redis.d.ts +12 -0
- package/dist/docker-compose/services/shared.d.ts +7 -0
- package/dist/docker-compose/yaml.d.ts +2 -0
- package/dist/environment/create-dev-environment.d.ts +23 -0
- package/dist/environment/index.d.ts +1 -0
- package/dist/environment/index.js +15 -0
- package/dist/environment/logging.d.ts +17 -0
- package/dist/environment/seeding.d.ts +9 -0
- package/dist/environment.d.ts +1 -23
- package/dist/environment.js +12 -14
- package/dist/index-045jksh5.js +147 -0
- package/dist/index-08wa79cs.js +125 -117
- package/dist/index-0kxnae3z.js +335 -0
- package/dist/index-1mdrf7nz.js +51 -43
- package/dist/index-1yvbwj4k.js +262 -242
- package/dist/index-23ev345g.js +475 -0
- package/dist/index-2ckr49sf.js +228 -0
- package/dist/index-2f47khe5.js +376 -369
- package/dist/index-2fr3g85b.js +220 -183
- package/dist/index-38xnzpa6.js +450 -0
- package/dist/index-3h3dhtf2.js +51 -43
- package/dist/index-42x95209.js +51 -43
- package/dist/index-4gp0az1g.js +145 -0
- package/dist/index-4xrxh8yv.js +72 -0
- package/dist/index-5gmws6ah.js +181 -0
- package/dist/index-5hka0tff.js +78 -76
- package/dist/index-5rfqps4b.js +3 -0
- package/dist/index-5t9jxqm0.js +428 -0
- package/dist/index-6c1w1xk5.js +101 -0
- package/dist/index-6fm7mvwj.js +118 -97
- package/dist/index-6srpc523.js +127 -128
- package/dist/index-731rzzfp.js +157 -142
- package/dist/index-75y4cg2z.js +51 -43
- package/dist/index-7ja4ywyj.js +126 -127
- package/dist/index-8bw1cmz4.js +531 -0
- package/dist/index-8hbbj1mp.js +120 -121
- package/dist/index-8xj2p5n5.js +118 -97
- package/dist/index-bj79tw5w.js +0 -0
- package/dist/index-bnk6nr0g.js +73 -0
- package/dist/index-brbbzyks.js +72 -0
- package/dist/index-c0dr6mcv.js +123 -0
- package/dist/index-cty0bcry.js +235 -218
- package/dist/index-d8tyv5se.js +228 -0
- package/dist/index-d9efy0n4.js +176 -150
- package/dist/index-etfmqjjf.js +427 -0
- package/dist/index-fb29934k.js +172 -0
- package/dist/index-g50jw1yf.js +72 -0
- package/dist/index-g6eb5wdw.js +118 -117
- package/dist/index-ggq3yryx.js +99 -95
- package/dist/index-h70tce00.js +177 -0
- package/dist/index-hkxtfqtc.js +333 -0
- package/dist/index-kf3dhser.js +146 -143
- package/dist/index-ma6tgdb2.js +500 -0
- package/dist/index-mam0bcyz.js +123 -0
- package/dist/index-mm412dkp.js +274 -0
- package/dist/index-n8v18aeb.js +0 -0
- package/dist/index-ndnmnsej.js +378 -371
- package/dist/index-p8wty0e2.js +389 -379
- package/dist/index-qfphr2fd.js +78 -76
- package/dist/index-qqmms8rs.js +51 -43
- package/dist/index-qw4093g2.js +51 -43
- package/dist/index-qzwpzjbx.js +121 -122
- package/dist/index-segbnm0h.js +146 -143
- package/dist/index-t0fj6gg1.js +112 -0
- package/dist/index-thdkwnv7.js +122 -0
- package/dist/index-tjbx2r2t.js +270 -0
- package/dist/index-tjqw9vtj.js +62 -54
- package/dist/index-vbpb89jy.js +248 -0
- package/dist/index-vhs88xhe.js +99 -95
- package/dist/index-w8zxnjka.js +249 -0
- package/dist/index-wk2na3t9.js +385 -375
- package/dist/index-wz9x8g7z.js +383 -373
- package/dist/index-x249gyde.js +388 -378
- package/dist/index-xkvd0nsd.js +187 -0
- package/dist/index-yedqxm1z.js +80 -0
- package/dist/index-zfjzzjkf.js +240 -199
- package/dist/index.d.ts +12 -8
- package/dist/index.js +56 -35
- package/dist/lint.d.ts +1 -46
- package/dist/lint.js +3 -7
- package/dist/loader/cache.d.ts +4 -0
- package/dist/loader/find-config-file.d.ts +2 -0
- package/dist/loader/index.d.ts +5 -0
- package/dist/loader/index.js +24 -0
- package/dist/loader/load-dev-env.d.ts +5 -0
- package/dist/loader/loader.d.ts +1 -0
- package/dist/loader.d.ts +1 -45
- package/dist/loader.js +22 -20
- package/dist/prisma/index.d.ts +1 -0
- package/dist/prisma/prisma.d.ts +29 -0
- package/dist/prisma.d.ts +1 -29
- package/dist/prisma.js +6 -10
- package/dist/src/bin.js +309 -0
- package/dist/src/cli.js +5 -0
- package/dist/src/config.js +15 -0
- package/dist/src/core/docker.js +38 -0
- package/dist/src/core/index.js +130 -0
- package/dist/src/core/network.js +9 -0
- package/dist/src/core/ports.js +23 -0
- package/dist/src/core/process.js +31 -0
- package/dist/src/core/utils.js +11 -0
- package/dist/src/core/watchdog-runner.js +69 -0
- package/dist/src/core/watchdog.js +28 -0
- package/dist/src/docker/runtime.js +37 -0
- package/dist/src/docker-compose/index.js +16 -0
- package/dist/src/docker-compose/services/index.js +17 -0
- package/dist/src/environment.js +12 -0
- package/dist/src/index.js +122 -0
- package/dist/src/lint.js +3 -0
- package/dist/src/loader.js +25 -0
- package/dist/src/prisma.js +6 -0
- package/dist/src/types.js +0 -0
- package/dist/typecheck/index.d.ts +1 -0
- package/dist/typecheck/index.js +7 -0
- package/dist/typecheck/typecheck.d.ts +46 -0
- package/dist/types/all-types.d.ts +501 -0
- package/dist/types/cli.d.ts +1 -0
- package/dist/types/config.d.ts +6 -0
- package/dist/types/docker.d.ts +15 -0
- package/dist/types/environment.d.ts +8 -0
- package/dist/types/hooks.d.ts +9 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +0 -0
- package/dist/types/prisma.d.ts +1 -0
- package/dist/types.d.ts +1 -399
- package/package.json +145 -140
- package/readme.md +349 -109
- package/src/cli/bin.ts +77 -0
- package/src/cli/commands/help.ts +39 -0
- package/src/cli/commands/runtime.ts +72 -0
- package/src/cli/commands/version.ts +4 -0
- package/src/cli/index.ts +1 -0
- package/{cli.ts → src/cli/run-cli.ts} +95 -6
- package/src/config/define-config.ts +30 -0
- package/src/config/index.ts +3 -0
- package/src/config/merge-configs.ts +33 -0
- package/src/config/validate-config.ts +136 -0
- package/{core → src/core}/index.ts +2 -2
- package/{core → src/core}/ports.ts +5 -2
- package/{core → src/core}/process.ts +6 -2
- package/src/core/tunnel.ts +151 -0
- package/{core → src/core}/utils.ts +1 -0
- package/{core → src/core}/watchdog.ts +5 -1
- package/src/docker/index.ts +1 -0
- package/{core/docker.ts → src/docker/runtime.ts} +11 -4
- package/src/docker-compose/generated-file.ts +45 -0
- package/src/docker-compose/index.ts +7 -0
- package/src/docker-compose/model.ts +197 -0
- package/src/docker-compose/services/clickhouse.ts +79 -0
- package/src/docker-compose/services/define-docker-service.ts +109 -0
- package/src/docker-compose/services/index.ts +67 -0
- package/src/docker-compose/services/postgres.ts +60 -0
- package/src/docker-compose/services/redis.ts +48 -0
- package/src/docker-compose/services/shared.ts +79 -0
- package/src/docker-compose/yaml.ts +88 -0
- package/{environment.ts → src/environment/create-dev-environment.ts} +93 -130
- package/src/environment/index.ts +1 -0
- package/src/environment/logging.ts +101 -0
- package/src/environment/seeding.ts +57 -0
- package/{index.ts → src/index.ts} +49 -20
- package/src/loader/cache.ts +23 -0
- package/src/loader/find-config-file.ts +29 -0
- package/src/loader/index.ts +17 -0
- package/src/loader/load-dev-env.ts +38 -0
- package/src/prisma/index.ts +1 -0
- package/{prisma.ts → src/prisma/prisma.ts} +4 -2
- package/src/typecheck/index.ts +1 -0
- package/{types.ts → src/types/all-types.ts} +130 -5
- package/src/types/index.ts +1 -0
- package/bin.ts +0 -192
- package/config.ts +0 -194
- package/loader.ts +0 -126
- /package/{core → src/core}/network.ts +0 -0
- /package/{core → src/core}/watchdog-runner.ts +0 -0
- /package/{lint.ts → src/typecheck/typecheck.ts} +0 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { startTunnel } from "untun";
|
|
2
|
+
import type { AppConfig, DevEnvironment, ServiceConfig } from "../types";
|
|
3
|
+
|
|
4
|
+
export interface PublicExposeTarget {
|
|
5
|
+
kind: "service" | "app";
|
|
6
|
+
name: string;
|
|
7
|
+
port: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface PublicTunnel {
|
|
11
|
+
kind: "service" | "app";
|
|
12
|
+
name: string;
|
|
13
|
+
localUrl: string;
|
|
14
|
+
publicUrl: string;
|
|
15
|
+
close: () => Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface UntunTunnelLike {
|
|
19
|
+
url?: string;
|
|
20
|
+
publicUrl?: string;
|
|
21
|
+
tunnelUrl?: string;
|
|
22
|
+
close?: () => void | Promise<void>;
|
|
23
|
+
stop?: () => void | Promise<void>;
|
|
24
|
+
destroy?: () => void | Promise<void>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function parseExposeNames(exposeValue?: string): Set<string> | null {
|
|
28
|
+
if (exposeValue === undefined) return null;
|
|
29
|
+
const names = exposeValue
|
|
30
|
+
.split(",")
|
|
31
|
+
.map((name) => name.trim())
|
|
32
|
+
.filter(Boolean);
|
|
33
|
+
return new Set(names);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function asPublicUrl(tunnel: UntunTunnelLike): string | null {
|
|
37
|
+
return tunnel.url ?? tunnel.publicUrl ?? tunnel.tunnelUrl ?? null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function toCloseFn(tunnel: UntunTunnelLike): () => Promise<void> {
|
|
41
|
+
const close = tunnel.close ?? tunnel.stop ?? tunnel.destroy;
|
|
42
|
+
if (!close) return async () => {};
|
|
43
|
+
return async () => {
|
|
44
|
+
await close();
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function resolveExposeTargets<
|
|
49
|
+
TServices extends Record<string, ServiceConfig>,
|
|
50
|
+
TApps extends Record<string, AppConfig>,
|
|
51
|
+
>(
|
|
52
|
+
env: DevEnvironment<TServices, TApps>,
|
|
53
|
+
exposeValue?: string,
|
|
54
|
+
): {
|
|
55
|
+
targets: PublicExposeTarget[];
|
|
56
|
+
unknownNames: string[];
|
|
57
|
+
notEnabledNames: string[];
|
|
58
|
+
} {
|
|
59
|
+
const requestedNames = parseExposeNames(exposeValue);
|
|
60
|
+
const knownTargets = new Map<string, PublicExposeTarget>();
|
|
61
|
+
const enabledTargets = new Map<string, PublicExposeTarget>();
|
|
62
|
+
|
|
63
|
+
for (const [name, config] of Object.entries(env.services)) {
|
|
64
|
+
const port = env.ports[name];
|
|
65
|
+
if (port === undefined) continue;
|
|
66
|
+
const target: PublicExposeTarget = { kind: "service", name, port };
|
|
67
|
+
knownTargets.set(name, target);
|
|
68
|
+
if (config.expose === true) {
|
|
69
|
+
enabledTargets.set(name, target);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
for (const [name, config] of Object.entries(env.apps)) {
|
|
74
|
+
const port = env.ports[name];
|
|
75
|
+
if (port === undefined) continue;
|
|
76
|
+
const target: PublicExposeTarget = { kind: "app", name, port };
|
|
77
|
+
knownTargets.set(name, target);
|
|
78
|
+
if (config.expose === true) {
|
|
79
|
+
enabledTargets.set(name, target);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (requestedNames === null) {
|
|
84
|
+
return {
|
|
85
|
+
targets: Array.from(enabledTargets.values()),
|
|
86
|
+
unknownNames: [],
|
|
87
|
+
notEnabledNames: [],
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const unknownNames: string[] = [];
|
|
92
|
+
const notEnabledNames: string[] = [];
|
|
93
|
+
const targets: PublicExposeTarget[] = [];
|
|
94
|
+
|
|
95
|
+
for (const name of requestedNames) {
|
|
96
|
+
if (!knownTargets.has(name)) {
|
|
97
|
+
unknownNames.push(name);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
const enabledTarget = enabledTargets.get(name);
|
|
101
|
+
if (!enabledTarget) {
|
|
102
|
+
notEnabledNames.push(name);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
targets.push(enabledTarget);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return { targets, unknownNames, notEnabledNames };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export async function startPublicTunnels(
|
|
112
|
+
targets: PublicExposeTarget[],
|
|
113
|
+
options: {
|
|
114
|
+
start?: (input: { url: string }) => Promise<UntunTunnelLike>;
|
|
115
|
+
} = {},
|
|
116
|
+
): Promise<PublicTunnel[]> {
|
|
117
|
+
const start = options.start ?? ((input) => startTunnel(input));
|
|
118
|
+
const tunnels: PublicTunnel[] = [];
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
for (const target of targets) {
|
|
122
|
+
const localUrl = `http://localhost:${target.port}`;
|
|
123
|
+
const tunnel = (await start({
|
|
124
|
+
url: localUrl,
|
|
125
|
+
})) as UntunTunnelLike;
|
|
126
|
+
const publicUrl = asPublicUrl(tunnel);
|
|
127
|
+
if (!publicUrl) {
|
|
128
|
+
throw new Error(
|
|
129
|
+
`Tunnel for "${target.name}" did not provide a public URL`,
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
tunnels.push({
|
|
133
|
+
kind: target.kind,
|
|
134
|
+
name: target.name,
|
|
135
|
+
localUrl,
|
|
136
|
+
publicUrl,
|
|
137
|
+
close: toCloseFn(tunnel),
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
return tunnels;
|
|
141
|
+
} catch (error) {
|
|
142
|
+
await stopPublicTunnels(tunnels);
|
|
143
|
+
throw error;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export async function stopPublicTunnels(
|
|
148
|
+
tunnels: PublicTunnel[],
|
|
149
|
+
): Promise<void> {
|
|
150
|
+
await Promise.allSettled(tunnels.map((tunnel) => tunnel.close()));
|
|
151
|
+
}
|
|
@@ -120,6 +120,10 @@ export function getWatchdogPid(projectName: string): number | null {
|
|
|
120
120
|
* Spawn watchdog as a detached process.
|
|
121
121
|
* The watchdog monitors the heartbeat file and shuts down containers after idle timeout.
|
|
122
122
|
*/
|
|
123
|
+
export function getWatchdogComposeArg(composeFile?: string): string {
|
|
124
|
+
return composeFile ? `-f "${composeFile}"` : "";
|
|
125
|
+
}
|
|
126
|
+
|
|
123
127
|
export async function spawnWatchdog(
|
|
124
128
|
projectName: string,
|
|
125
129
|
root: string,
|
|
@@ -162,7 +166,7 @@ export async function spawnWatchdog(
|
|
|
162
166
|
WATCHDOG_HEARTBEAT_FILE: getHeartbeatFile(projectName),
|
|
163
167
|
WATCHDOG_PID_FILE: pidFile,
|
|
164
168
|
WATCHDOG_TIMEOUT_MS: String(timeoutMinutes * 60 * 1000),
|
|
165
|
-
WATCHDOG_COMPOSE_ARG: composeFile
|
|
169
|
+
WATCHDOG_COMPOSE_ARG: getWatchdogComposeArg(composeFile),
|
|
166
170
|
},
|
|
167
171
|
});
|
|
168
172
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./runtime";
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { execSync } from "node:child_process";
|
|
2
|
+
import { sleep } from "../core/utils";
|
|
2
3
|
import type {
|
|
3
4
|
BuiltInHealthCheck,
|
|
4
5
|
HealthCheckFn,
|
|
5
6
|
ServiceConfig,
|
|
6
7
|
} from "../types";
|
|
7
|
-
import { sleep } from "./utils";
|
|
8
8
|
|
|
9
9
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
10
10
|
// Constants
|
|
@@ -91,6 +91,13 @@ export interface StartContainersOptions {
|
|
|
91
91
|
composeFile?: string;
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
/**
|
|
95
|
+
* Build `-f` argument for docker compose.
|
|
96
|
+
*/
|
|
97
|
+
export function getComposeArg(composeFile?: string): string {
|
|
98
|
+
return composeFile ? `-f "${composeFile}"` : "";
|
|
99
|
+
}
|
|
100
|
+
|
|
94
101
|
/**
|
|
95
102
|
* Start Docker Compose containers.
|
|
96
103
|
*/
|
|
@@ -105,7 +112,7 @@ export function startContainers(
|
|
|
105
112
|
|
|
106
113
|
if (verbose) console.log("🐳 Starting Docker containers...");
|
|
107
114
|
|
|
108
|
-
const composeArg = composeFile
|
|
115
|
+
const composeArg = getComposeArg(composeFile);
|
|
109
116
|
const waitFlag = wait ? "--wait" : "";
|
|
110
117
|
const cmd = `docker compose ${composeArg} up -d ${waitFlag}`.trim();
|
|
111
118
|
|
|
@@ -143,7 +150,7 @@ export function stopContainers(
|
|
|
143
150
|
);
|
|
144
151
|
}
|
|
145
152
|
|
|
146
|
-
const composeArg = composeFile
|
|
153
|
+
const composeArg = getComposeArg(composeFile);
|
|
147
154
|
const volumeFlag = removeVolumes ? "-v" : "";
|
|
148
155
|
const cmd = `docker compose ${composeArg} down ${volumeFlag}`.trim();
|
|
149
156
|
|
|
@@ -171,7 +178,7 @@ export function startService(
|
|
|
171
178
|
|
|
172
179
|
if (verbose) console.log(`🐳 Starting ${serviceName}...`);
|
|
173
180
|
|
|
174
|
-
const composeArg = composeFile
|
|
181
|
+
const composeArg = getComposeArg(composeFile);
|
|
175
182
|
const cmd = `docker compose ${composeArg} up -d ${serviceName}`.trim();
|
|
176
183
|
|
|
177
184
|
execSync(cmd, {
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, isAbsolute, relative, resolve } from "node:path";
|
|
3
|
+
import type { DockerComposeGenerationOptions, ServiceConfig } from "../types";
|
|
4
|
+
import { buildComposeModel } from "./model";
|
|
5
|
+
import { composeToYaml } from "./yaml";
|
|
6
|
+
|
|
7
|
+
export const DEFAULT_GENERATED_COMPOSE_FILE =
|
|
8
|
+
".buncargo/docker-compose.generated.yml";
|
|
9
|
+
|
|
10
|
+
export function getGeneratedComposePath(
|
|
11
|
+
root: string,
|
|
12
|
+
docker?: DockerComposeGenerationOptions,
|
|
13
|
+
): { absolutePath: string; composeFileArg: string } {
|
|
14
|
+
const generatedFile = docker?.generatedFile ?? DEFAULT_GENERATED_COMPOSE_FILE;
|
|
15
|
+
const absolutePath = isAbsolute(generatedFile)
|
|
16
|
+
? generatedFile
|
|
17
|
+
: resolve(root, generatedFile);
|
|
18
|
+
const relativePath = relative(root, absolutePath);
|
|
19
|
+
const composeFileArg =
|
|
20
|
+
relativePath && !relativePath.startsWith("..")
|
|
21
|
+
? relativePath
|
|
22
|
+
: absolutePath;
|
|
23
|
+
return { absolutePath, composeFileArg };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function writeGeneratedComposeFile(
|
|
27
|
+
root: string,
|
|
28
|
+
services: Record<string, ServiceConfig>,
|
|
29
|
+
docker?: DockerComposeGenerationOptions,
|
|
30
|
+
): string {
|
|
31
|
+
const { absolutePath, composeFileArg } = getGeneratedComposePath(
|
|
32
|
+
root,
|
|
33
|
+
docker,
|
|
34
|
+
);
|
|
35
|
+
const writeStrategy = docker?.writeStrategy ?? "always";
|
|
36
|
+
const shouldWrite = writeStrategy === "always" || !existsSync(absolutePath);
|
|
37
|
+
if (shouldWrite) {
|
|
38
|
+
const composeModel = buildComposeModel(services, docker);
|
|
39
|
+
const yaml = composeToYaml(composeModel);
|
|
40
|
+
mkdirSync(dirname(absolutePath), { recursive: true });
|
|
41
|
+
writeFileSync(absolutePath, yaml, "utf-8");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return composeFileArg;
|
|
45
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DockerComposeGenerationOptions,
|
|
3
|
+
DockerComposeNode,
|
|
4
|
+
DockerComposeServiceRaw,
|
|
5
|
+
DockerComposeVolumeRaw,
|
|
6
|
+
DockerPresetName,
|
|
7
|
+
DockerPresetServiceDefinition,
|
|
8
|
+
DockerServiceDefinition,
|
|
9
|
+
ServiceConfig,
|
|
10
|
+
} from "../types";
|
|
11
|
+
import { buildPresetDockerService, inferDockerPreset } from "./services";
|
|
12
|
+
import { getDefaultPortBindings } from "./services/shared";
|
|
13
|
+
|
|
14
|
+
export type ComposeDocument = {
|
|
15
|
+
services: Record<string, DockerComposeServiceRaw>;
|
|
16
|
+
volumes?: Record<string, DockerComposeVolumeRaw>;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function isObject(
|
|
20
|
+
value: DockerComposeNode,
|
|
21
|
+
): value is Record<string, DockerComposeNode | undefined> {
|
|
22
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function deepMergeNode(
|
|
26
|
+
base: DockerComposeNode,
|
|
27
|
+
override: DockerComposeNode,
|
|
28
|
+
): DockerComposeNode {
|
|
29
|
+
if (Array.isArray(base) || Array.isArray(override)) {
|
|
30
|
+
return override;
|
|
31
|
+
}
|
|
32
|
+
if (!isObject(base) || !isObject(override)) {
|
|
33
|
+
return override;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const merged: Record<string, DockerComposeNode | undefined> = { ...base };
|
|
37
|
+
for (const key of Object.keys(override)) {
|
|
38
|
+
const baseValue = merged[key];
|
|
39
|
+
const overrideValue = override[key];
|
|
40
|
+
if (baseValue === undefined || overrideValue === undefined) {
|
|
41
|
+
merged[key] = overrideValue;
|
|
42
|
+
} else {
|
|
43
|
+
merged[key] = deepMergeNode(baseValue, overrideValue);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return merged;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function isPresetDefinition(
|
|
50
|
+
value: DockerServiceDefinition | undefined,
|
|
51
|
+
): value is DockerPresetServiceDefinition {
|
|
52
|
+
return Boolean(
|
|
53
|
+
value &&
|
|
54
|
+
typeof value === "object" &&
|
|
55
|
+
"kind" in value &&
|
|
56
|
+
(value as { kind?: string }).kind === "preset",
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function normalizeRawService(
|
|
61
|
+
name: string,
|
|
62
|
+
config: ServiceConfig,
|
|
63
|
+
service: DockerComposeServiceRaw,
|
|
64
|
+
): DockerComposeServiceRaw {
|
|
65
|
+
const normalized = { ...service };
|
|
66
|
+
if (!normalized.ports || normalized.ports.length === 0) {
|
|
67
|
+
normalized.ports = getDefaultPortBindings(name, config);
|
|
68
|
+
}
|
|
69
|
+
if (config.healthCheck === false) {
|
|
70
|
+
delete normalized.healthcheck;
|
|
71
|
+
}
|
|
72
|
+
return normalized;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
type NormalizedServiceConfig =
|
|
76
|
+
| {
|
|
77
|
+
kind: "preset";
|
|
78
|
+
serviceName: string;
|
|
79
|
+
preset: DockerPresetName;
|
|
80
|
+
serviceOverride?: DockerComposeServiceRaw;
|
|
81
|
+
}
|
|
82
|
+
| {
|
|
83
|
+
kind: "raw";
|
|
84
|
+
serviceName: string;
|
|
85
|
+
service: DockerComposeServiceRaw;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
function normalizeServiceConfig(
|
|
89
|
+
name: string,
|
|
90
|
+
config: ServiceConfig,
|
|
91
|
+
): NormalizedServiceConfig {
|
|
92
|
+
const serviceName = config.serviceName ?? name;
|
|
93
|
+
const rawDefinition = config.docker;
|
|
94
|
+
|
|
95
|
+
if (isPresetDefinition(rawDefinition)) {
|
|
96
|
+
return {
|
|
97
|
+
kind: "preset",
|
|
98
|
+
serviceName,
|
|
99
|
+
preset: rawDefinition.preset,
|
|
100
|
+
serviceOverride: rawDefinition.service,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (rawDefinition) {
|
|
105
|
+
const inferredPreset = inferDockerPreset(name);
|
|
106
|
+
if (inferredPreset) {
|
|
107
|
+
return {
|
|
108
|
+
kind: "preset",
|
|
109
|
+
serviceName,
|
|
110
|
+
preset: inferredPreset,
|
|
111
|
+
serviceOverride: rawDefinition,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
kind: "raw",
|
|
116
|
+
serviceName,
|
|
117
|
+
service: normalizeRawService(name, config, rawDefinition),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const preset = inferDockerPreset(name);
|
|
122
|
+
if (!preset) {
|
|
123
|
+
throw new Error(
|
|
124
|
+
`Service "${name}" has no docker preset and no docker definition. Add service.docker using helper or raw mode.`,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
kind: "preset",
|
|
130
|
+
serviceName,
|
|
131
|
+
preset,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function resolveServiceDefinition(
|
|
136
|
+
name: string,
|
|
137
|
+
config: ServiceConfig,
|
|
138
|
+
): {
|
|
139
|
+
serviceName: string;
|
|
140
|
+
service: DockerComposeServiceRaw;
|
|
141
|
+
volume?: string;
|
|
142
|
+
} {
|
|
143
|
+
const normalized = normalizeServiceConfig(name, config);
|
|
144
|
+
if (normalized.kind === "raw") {
|
|
145
|
+
return {
|
|
146
|
+
serviceName: normalized.serviceName,
|
|
147
|
+
service: normalized.service,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const { service, volume } = buildPresetDockerService(normalized.preset, {
|
|
152
|
+
serviceKey: name,
|
|
153
|
+
config,
|
|
154
|
+
});
|
|
155
|
+
const mergedService = normalized.serviceOverride
|
|
156
|
+
? (deepMergeNode(
|
|
157
|
+
service as DockerComposeNode,
|
|
158
|
+
normalized.serviceOverride as DockerComposeNode,
|
|
159
|
+
) as DockerComposeServiceRaw)
|
|
160
|
+
: service;
|
|
161
|
+
return {
|
|
162
|
+
serviceName: normalized.serviceName,
|
|
163
|
+
service: mergedService,
|
|
164
|
+
volume,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function buildComposeModel(
|
|
169
|
+
services: Record<string, ServiceConfig>,
|
|
170
|
+
docker?: DockerComposeGenerationOptions,
|
|
171
|
+
): ComposeDocument {
|
|
172
|
+
const composeServices: Record<string, DockerComposeServiceRaw> = {};
|
|
173
|
+
const composeVolumes: Record<string, DockerComposeVolumeRaw> = {};
|
|
174
|
+
|
|
175
|
+
for (const [name, serviceConfig] of Object.entries(services)) {
|
|
176
|
+
const { serviceName, service, volume } = resolveServiceDefinition(
|
|
177
|
+
name,
|
|
178
|
+
serviceConfig,
|
|
179
|
+
);
|
|
180
|
+
composeServices[serviceName] = service;
|
|
181
|
+
if (volume) {
|
|
182
|
+
composeVolumes[volume] = {};
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
for (const [volumeName, volume] of Object.entries(docker?.volumes ?? {})) {
|
|
187
|
+
composeVolumes[volumeName] = volume;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const document: ComposeDocument = {
|
|
191
|
+
services: composeServices,
|
|
192
|
+
};
|
|
193
|
+
if (Object.keys(composeVolumes).length > 0) {
|
|
194
|
+
document.volumes = composeVolumes;
|
|
195
|
+
}
|
|
196
|
+
return document;
|
|
197
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
BuiltInHealthCheck,
|
|
3
|
+
DockerComposeHealthcheckRaw,
|
|
4
|
+
DockerComposeServiceRaw,
|
|
5
|
+
ServiceConfig,
|
|
6
|
+
} from "../../types";
|
|
7
|
+
import { defineDockerService } from "./define-docker-service";
|
|
8
|
+
import { getDefaultPortBindings, resolveHealthcheck } from "./shared";
|
|
9
|
+
|
|
10
|
+
export type ClickhouseServiceOptions = {
|
|
11
|
+
port?: number;
|
|
12
|
+
secondaryPort?: number;
|
|
13
|
+
expose?: boolean;
|
|
14
|
+
healthCheck?: BuiltInHealthCheck | false;
|
|
15
|
+
serviceName?: string;
|
|
16
|
+
database?: string;
|
|
17
|
+
user?: string;
|
|
18
|
+
password?: string;
|
|
19
|
+
docker?: DockerComposeServiceRaw;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type ClickhouseServiceConfig = ServiceConfig & {
|
|
23
|
+
secondaryPort: number;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const clickhouseDockerService = defineDockerService<
|
|
27
|
+
ClickhouseServiceOptions,
|
|
28
|
+
ClickhouseServiceConfig
|
|
29
|
+
>({
|
|
30
|
+
preset: "clickhouse",
|
|
31
|
+
defaults: {
|
|
32
|
+
port: 8123,
|
|
33
|
+
secondaryPort: 9000,
|
|
34
|
+
healthCheck: "http",
|
|
35
|
+
},
|
|
36
|
+
enhanceServiceConfig: (base, options): ClickhouseServiceConfig => ({
|
|
37
|
+
...base,
|
|
38
|
+
secondaryPort: options.secondaryPort ?? 9000,
|
|
39
|
+
}),
|
|
40
|
+
build: ({ serviceKey, config }) => {
|
|
41
|
+
const user = config.user ?? "default";
|
|
42
|
+
const password = config.password ?? "clickhouse";
|
|
43
|
+
const database = config.database ?? "default";
|
|
44
|
+
const defaultHealthcheck: DockerComposeHealthcheckRaw = {
|
|
45
|
+
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:8123/ping || exit 1"],
|
|
46
|
+
interval: "250ms",
|
|
47
|
+
timeout: "5s",
|
|
48
|
+
retries: 20,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
service: {
|
|
53
|
+
image: "clickhouse/clickhouse-server:24-alpine",
|
|
54
|
+
ports: getDefaultPortBindings(serviceKey, config, "clickhouse"),
|
|
55
|
+
volumes: [`${serviceKey}_data:/var/lib/clickhouse`],
|
|
56
|
+
environment: {
|
|
57
|
+
CLICKHOUSE_USER: user,
|
|
58
|
+
CLICKHOUSE_PASSWORD: password,
|
|
59
|
+
CLICKHOUSE_DB: database,
|
|
60
|
+
},
|
|
61
|
+
ulimits: {
|
|
62
|
+
nofile: {
|
|
63
|
+
soft: 262144,
|
|
64
|
+
hard: 262144,
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
healthcheck: resolveHealthcheck(
|
|
68
|
+
config.healthCheck,
|
|
69
|
+
defaultHealthcheck,
|
|
70
|
+
{
|
|
71
|
+
internalPort: 8123,
|
|
72
|
+
user,
|
|
73
|
+
},
|
|
74
|
+
),
|
|
75
|
+
},
|
|
76
|
+
volume: `${serviceKey}_data`,
|
|
77
|
+
};
|
|
78
|
+
},
|
|
79
|
+
});
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
BuiltInHealthCheck,
|
|
3
|
+
DockerComposeServiceRaw,
|
|
4
|
+
DockerPresetName,
|
|
5
|
+
DockerPresetServiceDefinition,
|
|
6
|
+
ServiceConfig,
|
|
7
|
+
} from "../../types";
|
|
8
|
+
|
|
9
|
+
export interface DockerServiceFactoryInput {
|
|
10
|
+
serviceKey: string;
|
|
11
|
+
config: ServiceConfig;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface DockerServiceFactoryOutput {
|
|
15
|
+
service: DockerComposeServiceRaw;
|
|
16
|
+
volume?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type DockerServiceFactory = (
|
|
20
|
+
input: DockerServiceFactoryInput,
|
|
21
|
+
) => DockerServiceFactoryOutput;
|
|
22
|
+
|
|
23
|
+
export type PresetServiceSharedOptions = Pick<
|
|
24
|
+
ServiceConfig,
|
|
25
|
+
"serviceName" | "database" | "user" | "password" | "expose"
|
|
26
|
+
> & {
|
|
27
|
+
port?: number;
|
|
28
|
+
healthCheck?: BuiltInHealthCheck | false;
|
|
29
|
+
docker?: DockerComposeServiceRaw;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export interface DockerServicePresetDefaults {
|
|
33
|
+
port: number;
|
|
34
|
+
healthCheck: BuiltInHealthCheck;
|
|
35
|
+
secondaryPort?: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface DockerServicePreset<
|
|
39
|
+
TOptions extends PresetServiceSharedOptions = PresetServiceSharedOptions,
|
|
40
|
+
TServiceConfig extends ServiceConfig = ServiceConfig,
|
|
41
|
+
> {
|
|
42
|
+
preset: DockerPresetName;
|
|
43
|
+
defaults: DockerServicePresetDefaults;
|
|
44
|
+
build: DockerServiceFactory;
|
|
45
|
+
createPresetDefinition(
|
|
46
|
+
service?: DockerComposeServiceRaw,
|
|
47
|
+
): DockerPresetServiceDefinition;
|
|
48
|
+
toServiceConfig(options?: TOptions): TServiceConfig;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface DefineDockerServiceInput<
|
|
52
|
+
TOptions extends PresetServiceSharedOptions = PresetServiceSharedOptions,
|
|
53
|
+
TServiceConfig extends ServiceConfig = ServiceConfig,
|
|
54
|
+
> {
|
|
55
|
+
preset: DockerPresetName;
|
|
56
|
+
defaults: DockerServicePresetDefaults;
|
|
57
|
+
build: DockerServiceFactory;
|
|
58
|
+
enhanceServiceConfig?: (
|
|
59
|
+
base: ServiceConfig,
|
|
60
|
+
options: TOptions,
|
|
61
|
+
) => TServiceConfig;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Define a docker service preset as single source of truth.
|
|
66
|
+
* The same definition powers:
|
|
67
|
+
* - compose generation (`build`)
|
|
68
|
+
* - typed config helper defaults (`toServiceConfig`)
|
|
69
|
+
*/
|
|
70
|
+
export function defineDockerService<
|
|
71
|
+
TOptions extends PresetServiceSharedOptions = PresetServiceSharedOptions,
|
|
72
|
+
TServiceConfig extends ServiceConfig = ServiceConfig,
|
|
73
|
+
>(
|
|
74
|
+
input: DefineDockerServiceInput<TOptions, TServiceConfig>,
|
|
75
|
+
): DockerServicePreset<TOptions, TServiceConfig> {
|
|
76
|
+
function createPresetDefinition(
|
|
77
|
+
service?: DockerComposeServiceRaw,
|
|
78
|
+
): DockerPresetServiceDefinition {
|
|
79
|
+
return {
|
|
80
|
+
kind: "preset",
|
|
81
|
+
preset: input.preset,
|
|
82
|
+
service,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function toServiceConfig(options = {} as TOptions): TServiceConfig {
|
|
87
|
+
const base: ServiceConfig = {
|
|
88
|
+
port: options.port ?? input.defaults.port,
|
|
89
|
+
expose: options.expose,
|
|
90
|
+
healthCheck: options.healthCheck ?? input.defaults.healthCheck,
|
|
91
|
+
database: options.database,
|
|
92
|
+
user: options.user,
|
|
93
|
+
password: options.password,
|
|
94
|
+
serviceName: options.serviceName,
|
|
95
|
+
docker: createPresetDefinition(options.docker),
|
|
96
|
+
};
|
|
97
|
+
return input.enhanceServiceConfig
|
|
98
|
+
? input.enhanceServiceConfig(base, options)
|
|
99
|
+
: (base as TServiceConfig);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
preset: input.preset,
|
|
104
|
+
defaults: input.defaults,
|
|
105
|
+
build: input.build,
|
|
106
|
+
createPresetDefinition,
|
|
107
|
+
toServiceConfig,
|
|
108
|
+
};
|
|
109
|
+
}
|