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.
Files changed (221) hide show
  1. package/dist/bin.d.ts +1 -12
  2. package/dist/bin.js +261 -253
  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 -83
  22. package/dist/core/docker.js +35 -32
  23. package/dist/core/index.d.ts +1 -1
  24. package/dist/core/index.js +123 -118
  25. package/dist/core/network.js +2 -2
  26. package/dist/core/ports.js +1 -1
  27. package/dist/core/process.js +1 -1
  28. package/dist/core/tunnel.d.ts +33 -0
  29. package/dist/core/utils.js +2 -2
  30. package/dist/core/watchdog-runner.js +45 -42
  31. package/dist/core/watchdog.d.ts +1 -0
  32. package/dist/core/watchdog.js +4 -2
  33. package/dist/docker/index.d.ts +1 -0
  34. package/dist/docker/index.js +38 -0
  35. package/dist/docker/runtime.d.ts +87 -0
  36. package/dist/docker/runtime.js +37 -0
  37. package/dist/docker-compose/compose.d.ts +1 -0
  38. package/dist/docker-compose/generated-file.d.ts +7 -0
  39. package/dist/docker-compose/index.d.ts +3 -0
  40. package/dist/docker-compose/index.js +15 -0
  41. package/dist/docker-compose/model.d.ts +6 -0
  42. package/dist/docker-compose/services/clickhouse.d.ts +16 -0
  43. package/dist/docker-compose/services/define-docker-service.d.ts +41 -0
  44. package/dist/docker-compose/services/index.d.ts +23 -0
  45. package/dist/docker-compose/services/index.js +17 -0
  46. package/dist/docker-compose/services/postgres.d.ts +12 -0
  47. package/dist/docker-compose/services/redis.d.ts +12 -0
  48. package/dist/docker-compose/services/shared.d.ts +7 -0
  49. package/dist/docker-compose/yaml.d.ts +2 -0
  50. package/dist/environment/create-dev-environment.d.ts +23 -0
  51. package/dist/environment/index.d.ts +1 -0
  52. package/dist/environment/index.js +15 -0
  53. package/dist/environment/logging.d.ts +17 -0
  54. package/dist/environment/seeding.d.ts +9 -0
  55. package/dist/environment.d.ts +1 -23
  56. package/dist/environment.js +12 -14
  57. package/dist/index-045jksh5.js +147 -0
  58. package/dist/index-08wa79cs.js +125 -117
  59. package/dist/index-0kxnae3z.js +335 -0
  60. package/dist/index-1mdrf7nz.js +51 -43
  61. package/dist/index-1yvbwj4k.js +262 -242
  62. package/dist/index-23ev345g.js +475 -0
  63. package/dist/index-2ckr49sf.js +228 -0
  64. package/dist/index-2f47khe5.js +376 -369
  65. package/dist/index-2fr3g85b.js +220 -183
  66. package/dist/index-38xnzpa6.js +450 -0
  67. package/dist/index-3h3dhtf2.js +51 -43
  68. package/dist/index-42x95209.js +51 -43
  69. package/dist/index-4gp0az1g.js +145 -0
  70. package/dist/index-4xrxh8yv.js +72 -0
  71. package/dist/index-5gmws6ah.js +181 -0
  72. package/dist/index-5hka0tff.js +78 -76
  73. package/dist/index-5rfqps4b.js +3 -0
  74. package/dist/index-5t9jxqm0.js +428 -0
  75. package/dist/index-6c1w1xk5.js +101 -0
  76. package/dist/index-6fm7mvwj.js +118 -97
  77. package/dist/index-6srpc523.js +127 -128
  78. package/dist/index-731rzzfp.js +157 -142
  79. package/dist/index-75y4cg2z.js +51 -43
  80. package/dist/index-7ja4ywyj.js +126 -127
  81. package/dist/index-8bw1cmz4.js +531 -0
  82. package/dist/index-8hbbj1mp.js +120 -121
  83. package/dist/index-8xj2p5n5.js +118 -97
  84. package/dist/index-bj79tw5w.js +0 -0
  85. package/dist/index-bnk6nr0g.js +73 -0
  86. package/dist/index-brbbzyks.js +72 -0
  87. package/dist/index-c0dr6mcv.js +123 -0
  88. package/dist/index-cty0bcry.js +235 -218
  89. package/dist/index-d8tyv5se.js +228 -0
  90. package/dist/index-d9efy0n4.js +176 -150
  91. package/dist/index-etfmqjjf.js +427 -0
  92. package/dist/index-fb29934k.js +172 -0
  93. package/dist/index-g50jw1yf.js +72 -0
  94. package/dist/index-g6eb5wdw.js +118 -117
  95. package/dist/index-ggq3yryx.js +99 -95
  96. package/dist/index-h70tce00.js +177 -0
  97. package/dist/index-hkxtfqtc.js +333 -0
  98. package/dist/index-kf3dhser.js +146 -143
  99. package/dist/index-ma6tgdb2.js +500 -0
  100. package/dist/index-mam0bcyz.js +123 -0
  101. package/dist/index-mm412dkp.js +274 -0
  102. package/dist/index-n8v18aeb.js +0 -0
  103. package/dist/index-ndnmnsej.js +378 -371
  104. package/dist/index-p8wty0e2.js +389 -379
  105. package/dist/index-qfphr2fd.js +78 -76
  106. package/dist/index-qqmms8rs.js +51 -43
  107. package/dist/index-qw4093g2.js +51 -43
  108. package/dist/index-qzwpzjbx.js +121 -122
  109. package/dist/index-segbnm0h.js +146 -143
  110. package/dist/index-t0fj6gg1.js +112 -0
  111. package/dist/index-thdkwnv7.js +122 -0
  112. package/dist/index-tjbx2r2t.js +270 -0
  113. package/dist/index-tjqw9vtj.js +62 -54
  114. package/dist/index-vbpb89jy.js +248 -0
  115. package/dist/index-vhs88xhe.js +99 -95
  116. package/dist/index-w8zxnjka.js +249 -0
  117. package/dist/index-wk2na3t9.js +385 -375
  118. package/dist/index-wz9x8g7z.js +383 -373
  119. package/dist/index-x249gyde.js +388 -378
  120. package/dist/index-xkvd0nsd.js +187 -0
  121. package/dist/index-yedqxm1z.js +80 -0
  122. package/dist/index-zfjzzjkf.js +240 -199
  123. package/dist/index.d.ts +12 -8
  124. package/dist/index.js +56 -35
  125. package/dist/lint.d.ts +1 -46
  126. package/dist/lint.js +3 -7
  127. package/dist/loader/cache.d.ts +4 -0
  128. package/dist/loader/find-config-file.d.ts +2 -0
  129. package/dist/loader/index.d.ts +5 -0
  130. package/dist/loader/index.js +24 -0
  131. package/dist/loader/load-dev-env.d.ts +5 -0
  132. package/dist/loader/loader.d.ts +1 -0
  133. package/dist/loader.d.ts +1 -45
  134. package/dist/loader.js +22 -20
  135. package/dist/prisma/index.d.ts +1 -0
  136. package/dist/prisma/prisma.d.ts +29 -0
  137. package/dist/prisma.d.ts +1 -29
  138. package/dist/prisma.js +6 -10
  139. package/dist/src/bin.js +309 -0
  140. package/dist/src/cli.js +5 -0
  141. package/dist/src/config.js +15 -0
  142. package/dist/src/core/docker.js +38 -0
  143. package/dist/src/core/index.js +130 -0
  144. package/dist/src/core/network.js +9 -0
  145. package/dist/src/core/ports.js +23 -0
  146. package/dist/src/core/process.js +31 -0
  147. package/dist/src/core/utils.js +11 -0
  148. package/dist/src/core/watchdog-runner.js +69 -0
  149. package/dist/src/core/watchdog.js +28 -0
  150. package/dist/src/docker/runtime.js +37 -0
  151. package/dist/src/docker-compose/index.js +16 -0
  152. package/dist/src/docker-compose/services/index.js +17 -0
  153. package/dist/src/environment.js +12 -0
  154. package/dist/src/index.js +122 -0
  155. package/dist/src/lint.js +3 -0
  156. package/dist/src/loader.js +25 -0
  157. package/dist/src/prisma.js +6 -0
  158. package/dist/src/types.js +0 -0
  159. package/dist/typecheck/index.d.ts +1 -0
  160. package/dist/typecheck/index.js +7 -0
  161. package/dist/typecheck/typecheck.d.ts +46 -0
  162. package/dist/types/all-types.d.ts +501 -0
  163. package/dist/types/cli.d.ts +1 -0
  164. package/dist/types/config.d.ts +6 -0
  165. package/dist/types/docker.d.ts +15 -0
  166. package/dist/types/environment.d.ts +8 -0
  167. package/dist/types/hooks.d.ts +9 -0
  168. package/dist/types/index.d.ts +1 -0
  169. package/dist/types/index.js +0 -0
  170. package/dist/types/prisma.d.ts +1 -0
  171. package/dist/types.d.ts +1 -399
  172. package/package.json +145 -140
  173. package/readme.md +349 -109
  174. package/src/cli/bin.ts +77 -0
  175. package/src/cli/commands/help.ts +39 -0
  176. package/src/cli/commands/runtime.ts +72 -0
  177. package/src/cli/commands/version.ts +4 -0
  178. package/src/cli/index.ts +1 -0
  179. package/{cli.ts → src/cli/run-cli.ts} +95 -6
  180. package/src/config/define-config.ts +30 -0
  181. package/src/config/index.ts +3 -0
  182. package/src/config/merge-configs.ts +33 -0
  183. package/src/config/validate-config.ts +136 -0
  184. package/{core → src/core}/index.ts +2 -2
  185. package/{core → src/core}/ports.ts +5 -2
  186. package/{core → src/core}/process.ts +6 -2
  187. package/src/core/tunnel.ts +151 -0
  188. package/{core → src/core}/utils.ts +1 -0
  189. package/{core → src/core}/watchdog.ts +5 -1
  190. package/src/docker/index.ts +1 -0
  191. package/{core/docker.ts → src/docker/runtime.ts} +11 -4
  192. package/src/docker-compose/generated-file.ts +45 -0
  193. package/src/docker-compose/index.ts +7 -0
  194. package/src/docker-compose/model.ts +197 -0
  195. package/src/docker-compose/services/clickhouse.ts +79 -0
  196. package/src/docker-compose/services/define-docker-service.ts +109 -0
  197. package/src/docker-compose/services/index.ts +67 -0
  198. package/src/docker-compose/services/postgres.ts +60 -0
  199. package/src/docker-compose/services/redis.ts +48 -0
  200. package/src/docker-compose/services/shared.ts +79 -0
  201. package/src/docker-compose/yaml.ts +88 -0
  202. package/{environment.ts → src/environment/create-dev-environment.ts} +93 -130
  203. package/src/environment/index.ts +1 -0
  204. package/src/environment/logging.ts +101 -0
  205. package/src/environment/seeding.ts +57 -0
  206. package/{index.ts → src/index.ts} +49 -20
  207. package/src/loader/cache.ts +23 -0
  208. package/src/loader/find-config-file.ts +29 -0
  209. package/src/loader/index.ts +17 -0
  210. package/src/loader/load-dev-env.ts +38 -0
  211. package/src/prisma/index.ts +1 -0
  212. package/{prisma.ts → src/prisma/prisma.ts} +4 -2
  213. package/src/typecheck/index.ts +1 -0
  214. package/{types.ts → src/types/all-types.ts} +130 -5
  215. package/src/types/index.ts +1 -0
  216. package/bin.ts +0 -192
  217. package/config.ts +0 -194
  218. package/loader.ts +0 -126
  219. /package/{core → src/core}/network.ts +0 -0
  220. /package/{core → src/core}/watchdog-runner.ts +0 -0
  221. /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
@@ -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 ? `-f ${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 ? `-f ${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 ? `-f ${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,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
+ });
@@ -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
+ }