buncargo 1.0.26 → 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.
Files changed (222) hide show
  1. package/dist/bin.d.ts +1 -12
  2. package/dist/bin.js +261 -252
  3. package/dist/cli/bin.d.ts +13 -0
  4. package/dist/cli/bin.js +315 -0
  5. package/dist/cli/commands/help.d.ts +1 -0
  6. package/dist/cli/commands/runtime.d.ts +5 -0
  7. package/dist/cli/commands/version.d.ts +1 -0
  8. package/dist/cli/index.d.ts +1 -0
  9. package/dist/cli/index.js +14 -0
  10. package/dist/cli/run-cli.d.ts +22 -0
  11. package/dist/cli.d.ts +1 -22
  12. package/dist/cli.js +5 -13
  13. package/dist/config/config.d.ts +1 -0
  14. package/dist/config/define-config.d.ts +13 -0
  15. package/dist/config/index.d.ts +3 -0
  16. package/dist/config/index.js +15 -0
  17. package/dist/config/merge-configs.d.ts +3 -0
  18. package/dist/config/validate-config.d.ts +3 -0
  19. package/dist/config.d.ts +1 -72
  20. package/dist/config.js +12 -12
  21. package/dist/core/docker.d.ts +1 -74
  22. package/dist/core/docker.js +35 -26
  23. package/dist/core/index.d.ts +1 -1
  24. package/dist/core/index.js +123 -108
  25. package/dist/core/network.js +2 -2
  26. package/dist/core/ports.d.ts +22 -0
  27. package/dist/core/ports.js +5 -1
  28. package/dist/core/process.js +1 -1
  29. package/dist/core/tunnel.d.ts +33 -0
  30. package/dist/core/utils.js +2 -2
  31. package/dist/core/watchdog-runner.js +45 -42
  32. package/dist/core/watchdog.d.ts +1 -0
  33. package/dist/core/watchdog.js +4 -2
  34. package/dist/docker/index.d.ts +1 -0
  35. package/dist/docker/index.js +38 -0
  36. package/dist/docker/runtime.d.ts +87 -0
  37. package/dist/docker/runtime.js +37 -0
  38. package/dist/docker-compose/compose.d.ts +1 -0
  39. package/dist/docker-compose/generated-file.d.ts +7 -0
  40. package/dist/docker-compose/index.d.ts +3 -0
  41. package/dist/docker-compose/index.js +15 -0
  42. package/dist/docker-compose/model.d.ts +6 -0
  43. package/dist/docker-compose/services/clickhouse.d.ts +16 -0
  44. package/dist/docker-compose/services/define-docker-service.d.ts +41 -0
  45. package/dist/docker-compose/services/index.d.ts +23 -0
  46. package/dist/docker-compose/services/index.js +17 -0
  47. package/dist/docker-compose/services/postgres.d.ts +12 -0
  48. package/dist/docker-compose/services/redis.d.ts +12 -0
  49. package/dist/docker-compose/services/shared.d.ts +7 -0
  50. package/dist/docker-compose/yaml.d.ts +2 -0
  51. package/dist/environment/create-dev-environment.d.ts +23 -0
  52. package/dist/environment/index.d.ts +1 -0
  53. package/dist/environment/index.js +15 -0
  54. package/dist/environment/logging.d.ts +17 -0
  55. package/dist/environment/seeding.d.ts +9 -0
  56. package/dist/environment.d.ts +1 -23
  57. package/dist/environment.js +12 -14
  58. package/dist/index-045jksh5.js +147 -0
  59. package/dist/index-08wa79cs.js +125 -117
  60. package/dist/index-0kxnae3z.js +335 -0
  61. package/dist/index-1mdrf7nz.js +66 -0
  62. package/dist/index-1yvbwj4k.js +262 -242
  63. package/dist/index-23ev345g.js +475 -0
  64. package/dist/index-2ckr49sf.js +228 -0
  65. package/dist/index-2f47khe5.js +376 -369
  66. package/dist/index-2fr3g85b.js +220 -183
  67. package/dist/index-38xnzpa6.js +450 -0
  68. package/dist/index-3h3dhtf2.js +51 -43
  69. package/dist/index-42x95209.js +51 -43
  70. package/dist/index-4gp0az1g.js +145 -0
  71. package/dist/index-4xrxh8yv.js +72 -0
  72. package/dist/index-5gmws6ah.js +181 -0
  73. package/dist/index-5hka0tff.js +78 -76
  74. package/dist/index-5rfqps4b.js +3 -0
  75. package/dist/index-5t9jxqm0.js +428 -0
  76. package/dist/index-6c1w1xk5.js +101 -0
  77. package/dist/index-6fm7mvwj.js +118 -97
  78. package/dist/index-6srpc523.js +127 -128
  79. package/dist/index-731rzzfp.js +187 -0
  80. package/dist/index-75y4cg2z.js +51 -43
  81. package/dist/index-7ja4ywyj.js +126 -127
  82. package/dist/index-8bw1cmz4.js +531 -0
  83. package/dist/index-8hbbj1mp.js +120 -121
  84. package/dist/index-8xj2p5n5.js +145 -0
  85. package/dist/index-bj79tw5w.js +0 -0
  86. package/dist/index-bnk6nr0g.js +73 -0
  87. package/dist/index-brbbzyks.js +72 -0
  88. package/dist/index-c0dr6mcv.js +123 -0
  89. package/dist/index-cty0bcry.js +235 -218
  90. package/dist/index-d8tyv5se.js +228 -0
  91. package/dist/index-d9efy0n4.js +176 -150
  92. package/dist/index-etfmqjjf.js +427 -0
  93. package/dist/index-fb29934k.js +172 -0
  94. package/dist/index-g50jw1yf.js +72 -0
  95. package/dist/index-g6eb5wdw.js +118 -117
  96. package/dist/index-ggq3yryx.js +99 -95
  97. package/dist/index-h70tce00.js +177 -0
  98. package/dist/index-hkxtfqtc.js +333 -0
  99. package/dist/index-kf3dhser.js +146 -143
  100. package/dist/index-ma6tgdb2.js +500 -0
  101. package/dist/index-mam0bcyz.js +123 -0
  102. package/dist/index-mm412dkp.js +274 -0
  103. package/dist/index-n8v18aeb.js +0 -0
  104. package/dist/index-ndnmnsej.js +378 -371
  105. package/dist/index-p8wty0e2.js +389 -379
  106. package/dist/index-qfphr2fd.js +100 -0
  107. package/dist/index-qqmms8rs.js +51 -43
  108. package/dist/index-qw4093g2.js +51 -43
  109. package/dist/index-qzwpzjbx.js +121 -122
  110. package/dist/index-segbnm0h.js +146 -143
  111. package/dist/index-t0fj6gg1.js +112 -0
  112. package/dist/index-thdkwnv7.js +122 -0
  113. package/dist/index-tjbx2r2t.js +270 -0
  114. package/dist/index-tjqw9vtj.js +62 -54
  115. package/dist/index-vbpb89jy.js +248 -0
  116. package/dist/index-vhs88xhe.js +99 -95
  117. package/dist/index-w8zxnjka.js +249 -0
  118. package/dist/index-wk2na3t9.js +404 -0
  119. package/dist/index-wz9x8g7z.js +383 -373
  120. package/dist/index-x249gyde.js +388 -378
  121. package/dist/index-xkvd0nsd.js +187 -0
  122. package/dist/index-yedqxm1z.js +80 -0
  123. package/dist/index-zfjzzjkf.js +266 -0
  124. package/dist/index.d.ts +12 -8
  125. package/dist/index.js +66 -35
  126. package/dist/lint.d.ts +1 -46
  127. package/dist/lint.js +3 -7
  128. package/dist/loader/cache.d.ts +4 -0
  129. package/dist/loader/find-config-file.d.ts +2 -0
  130. package/dist/loader/index.d.ts +5 -0
  131. package/dist/loader/index.js +24 -0
  132. package/dist/loader/load-dev-env.d.ts +5 -0
  133. package/dist/loader/loader.d.ts +1 -0
  134. package/dist/loader.d.ts +1 -45
  135. package/dist/loader.js +22 -20
  136. package/dist/prisma/index.d.ts +1 -0
  137. package/dist/prisma/prisma.d.ts +29 -0
  138. package/dist/prisma.d.ts +1 -29
  139. package/dist/prisma.js +6 -10
  140. package/dist/src/bin.js +309 -0
  141. package/dist/src/cli.js +5 -0
  142. package/dist/src/config.js +15 -0
  143. package/dist/src/core/docker.js +38 -0
  144. package/dist/src/core/index.js +130 -0
  145. package/dist/src/core/network.js +9 -0
  146. package/dist/src/core/ports.js +23 -0
  147. package/dist/src/core/process.js +31 -0
  148. package/dist/src/core/utils.js +11 -0
  149. package/dist/src/core/watchdog-runner.js +69 -0
  150. package/dist/src/core/watchdog.js +28 -0
  151. package/dist/src/docker/runtime.js +37 -0
  152. package/dist/src/docker-compose/index.js +16 -0
  153. package/dist/src/docker-compose/services/index.js +17 -0
  154. package/dist/src/environment.js +12 -0
  155. package/dist/src/index.js +122 -0
  156. package/dist/src/lint.js +3 -0
  157. package/dist/src/loader.js +25 -0
  158. package/dist/src/prisma.js +6 -0
  159. package/dist/src/types.js +0 -0
  160. package/dist/typecheck/index.d.ts +1 -0
  161. package/dist/typecheck/index.js +7 -0
  162. package/dist/typecheck/typecheck.d.ts +46 -0
  163. package/dist/types/all-types.d.ts +501 -0
  164. package/dist/types/cli.d.ts +1 -0
  165. package/dist/types/config.d.ts +6 -0
  166. package/dist/types/docker.d.ts +15 -0
  167. package/dist/types/environment.d.ts +8 -0
  168. package/dist/types/hooks.d.ts +9 -0
  169. package/dist/types/index.d.ts +1 -0
  170. package/dist/types/index.js +0 -0
  171. package/dist/types/prisma.d.ts +1 -0
  172. package/dist/types.d.ts +1 -393
  173. package/package.json +145 -140
  174. package/readme.md +358 -105
  175. package/src/cli/bin.ts +77 -0
  176. package/src/cli/commands/help.ts +39 -0
  177. package/src/cli/commands/runtime.ts +72 -0
  178. package/src/cli/commands/version.ts +4 -0
  179. package/src/cli/index.ts +1 -0
  180. package/{cli.ts → src/cli/run-cli.ts} +95 -6
  181. package/src/config/define-config.ts +30 -0
  182. package/src/config/index.ts +3 -0
  183. package/src/config/merge-configs.ts +33 -0
  184. package/src/config/validate-config.ts +136 -0
  185. package/{core → src/core}/index.ts +2 -2
  186. package/{core → src/core}/ports.ts +68 -1
  187. package/{core → src/core}/process.ts +6 -2
  188. package/src/core/tunnel.ts +151 -0
  189. package/{core → src/core}/utils.ts +1 -0
  190. package/{core → src/core}/watchdog.ts +5 -1
  191. package/src/docker/index.ts +1 -0
  192. package/{core/docker.ts → src/docker/runtime.ts} +40 -4
  193. package/src/docker-compose/generated-file.ts +45 -0
  194. package/src/docker-compose/index.ts +7 -0
  195. package/src/docker-compose/model.ts +197 -0
  196. package/src/docker-compose/services/clickhouse.ts +79 -0
  197. package/src/docker-compose/services/define-docker-service.ts +109 -0
  198. package/src/docker-compose/services/index.ts +67 -0
  199. package/src/docker-compose/services/postgres.ts +60 -0
  200. package/src/docker-compose/services/redis.ts +48 -0
  201. package/src/docker-compose/services/shared.ts +79 -0
  202. package/src/docker-compose/yaml.ts +88 -0
  203. package/{environment.ts → src/environment/create-dev-environment.ts} +101 -146
  204. package/src/environment/index.ts +1 -0
  205. package/src/environment/logging.ts +101 -0
  206. package/src/environment/seeding.ts +57 -0
  207. package/{index.ts → src/index.ts} +49 -15
  208. package/src/loader/cache.ts +23 -0
  209. package/src/loader/find-config-file.ts +29 -0
  210. package/src/loader/index.ts +17 -0
  211. package/src/loader/load-dev-env.ts +38 -0
  212. package/src/prisma/index.ts +1 -0
  213. package/{prisma.ts → src/prisma/prisma.ts} +4 -2
  214. package/src/typecheck/index.ts +1 -0
  215. package/{types.ts → src/types/all-types.ts} +137 -6
  216. package/src/types/index.ts +1 -0
  217. package/bin.ts +0 -191
  218. package/config.ts +0 -194
  219. package/loader.ts +0 -126
  220. /package/{core → src/core}/network.ts +0 -0
  221. /package/{core → src/core}/watchdog-runner.ts +0 -0
  222. /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
+ }
@@ -93,6 +93,7 @@ export function getEnvVar<
93
93
  projectName: config.projectPrefix,
94
94
  localIp,
95
95
  portOffset: offset,
96
+ publicUrls: {},
96
97
  },
97
98
  );
98
99
 
@@ -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 ? `-f ${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
@@ -12,6 +12,8 @@ import { sleep } from "./utils";
12
12
 
13
13
  export const POLL_INTERVAL = 250; // Fast polling for quicker startup
14
14
  export const MAX_ATTEMPTS = 120; // 30 seconds total (120 * 250ms)
15
+ export const DOCKER_NOT_RUNNING_MESSAGE =
16
+ "Docker is not running. Please start Docker and try again.";
15
17
 
16
18
  // ═══════════════════════════════════════════════════════════════════════════
17
19
  // Container Status Checks
@@ -35,6 +37,30 @@ export async function isContainerRunning(
35
37
  }
36
38
  }
37
39
 
40
+ /**
41
+ * Check if Docker daemon is running and reachable.
42
+ */
43
+ export function isDockerRunning(): boolean {
44
+ try {
45
+ execSync('docker info --format "{{.ServerVersion}}"', {
46
+ encoding: "utf-8",
47
+ stdio: ["pipe", "pipe", "pipe"],
48
+ });
49
+ return true;
50
+ } catch {
51
+ return false;
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Ensure Docker is running before attempting compose operations.
57
+ */
58
+ export function assertDockerRunning(): void {
59
+ if (!isDockerRunning()) {
60
+ throw new Error(DOCKER_NOT_RUNNING_MESSAGE);
61
+ }
62
+ }
63
+
38
64
  /**
39
65
  * Check if all expected containers are running.
40
66
  */
@@ -65,6 +91,13 @@ export interface StartContainersOptions {
65
91
  composeFile?: string;
66
92
  }
67
93
 
94
+ /**
95
+ * Build `-f` argument for docker compose.
96
+ */
97
+ export function getComposeArg(composeFile?: string): string {
98
+ return composeFile ? `-f "${composeFile}"` : "";
99
+ }
100
+
68
101
  /**
69
102
  * Start Docker Compose containers.
70
103
  */
@@ -75,10 +108,11 @@ export function startContainers(
75
108
  options: StartContainersOptions = {},
76
109
  ): void {
77
110
  const { verbose = true, wait = true, composeFile } = options;
111
+ assertDockerRunning();
78
112
 
79
113
  if (verbose) console.log("🐳 Starting Docker containers...");
80
114
 
81
- const composeArg = composeFile ? `-f ${composeFile}` : "";
115
+ const composeArg = getComposeArg(composeFile);
82
116
  const waitFlag = wait ? "--wait" : "";
83
117
  const cmd = `docker compose ${composeArg} up -d ${waitFlag}`.trim();
84
118
 
@@ -106,6 +140,7 @@ export function stopContainers(
106
140
  options: StopContainersOptions = {},
107
141
  ): void {
108
142
  const { verbose = true, removeVolumes = false, composeFile } = options;
143
+ assertDockerRunning();
109
144
 
110
145
  if (verbose) {
111
146
  console.log(
@@ -115,7 +150,7 @@ export function stopContainers(
115
150
  );
116
151
  }
117
152
 
118
- const composeArg = composeFile ? `-f ${composeFile}` : "";
153
+ const composeArg = getComposeArg(composeFile);
119
154
  const volumeFlag = removeVolumes ? "-v" : "";
120
155
  const cmd = `docker compose ${composeArg} down ${volumeFlag}`.trim();
121
156
 
@@ -139,10 +174,11 @@ export function startService(
139
174
  options: { verbose?: boolean; composeFile?: string } = {},
140
175
  ): void {
141
176
  const { verbose = true, composeFile } = options;
177
+ assertDockerRunning();
142
178
 
143
179
  if (verbose) console.log(`🐳 Starting ${serviceName}...`);
144
180
 
145
- const composeArg = composeFile ? `-f ${composeFile}` : "";
181
+ const composeArg = getComposeArg(composeFile);
146
182
  const cmd = `docker compose ${composeArg} up -d ${serviceName}`.trim();
147
183
 
148
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,7 @@
1
+ export {
2
+ DEFAULT_GENERATED_COMPOSE_FILE,
3
+ getGeneratedComposePath,
4
+ writeGeneratedComposeFile,
5
+ } from "./generated-file";
6
+ export { buildComposeModel } from "./model";
7
+ export { composeToYaml } from "./yaml";
@@ -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
+ });