@flisk-sh/flisk-export 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -45,8 +45,9 @@ flisk-export compose --check # exit 1 when that file is out of date (for CI)
45
45
  ```
46
46
 
47
47
  Writes a **standalone** `docker-compose.yml` from the bring-your-own `service.json` files (`image` or `dockerfile`, `env`,
48
- `volumes`, `extraPorts`, `dependsOn`, `sidecarOf`, `container`). It runs with plain `docker compose up` and no Flisk: no external
49
- networks, no `FLISK_*` variables, no generated secrets. `${VAR}` references in `env` pass through for Compose to read from `.env`.
48
+ `secrets.required`, `volumes`, `extraPorts`, `dependsOn`, `healthCheck`, `sidecarOf`, `container`). It runs with plain `docker compose up` and no Flisk: no external
49
+ networks, no `FLISK_*` variables, no generated secrets. `${VAR}` references in `env`, and every name in `secrets.required`, pass through as `${NAME}` for Compose to read from `.env`.
50
+ `healthCheck.command` becomes a Compose `healthcheck`; a service that lists a checked service in `dependsOn` waits for `service_healthy`, and one that lists a `restart: "no"` job waits for `service_completed_successfully`.
50
51
  Other resource types and `command` (native) services are not exported. The file is generated: change `service.json` and run it
51
52
  again with `--force`. It never starts a container or calls an API.
52
53
 
package/dist/compose.js CHANGED
@@ -40,7 +40,8 @@ function hostPath(outDir, absolute) {
40
40
  function isNamedVolume(source) {
41
41
  return !/[\\/]/.test(source) && !source.startsWith(".") && !source.startsWith("~");
42
42
  }
43
- function serviceLines(resource, outDir, known) {
43
+ function serviceLines(resource, outDir, services) {
44
+ const known = services;
44
45
  const c = resource.config;
45
46
  const name = resource.name;
46
47
  if (!SERVICE_NAME.test(name)) {
@@ -81,13 +82,17 @@ function serviceLines(resource, outDir, known) {
81
82
  lines.push(` network_mode: ${q(`service:${sidecarOf}`)}`);
82
83
  }
83
84
  const env = record(c.env);
84
- const envNames = Object.keys(env)
85
- .filter((k) => ENV_NAME.test(k))
86
- .sort();
85
+ // Secrets are listed by name only in service.json; Compose reads their values from the project .env.
86
+ const values = new Map();
87
+ for (const key of strings(record(c.secrets).required))
88
+ values.set(key, `\${${key}}`);
89
+ for (const [key, value] of Object.entries(env))
90
+ values.set(key, String(value));
91
+ const envNames = [...values.keys()].filter((k) => ENV_NAME.test(k)).sort();
87
92
  if (envNames.length > 0) {
88
93
  lines.push(" environment:");
89
94
  for (const key of envNames)
90
- lines.push(` ${key}: ${q(String(env[key]))}`);
95
+ lines.push(` ${key}: ${q(values.get(key) ?? "")}`);
91
96
  }
92
97
  const volumes = Array.isArray(c.volumes) ? c.volumes : [];
93
98
  if (volumes.length > 0) {
@@ -127,8 +132,12 @@ function serviceLines(resource, outDir, known) {
127
132
  }
128
133
  const dependsOn = strings(c.dependsOn).filter((d) => known.has(d));
129
134
  if (dependsOn.length > 0) {
130
- lines.push(" depends_on:", ...dependsOn.map((d) => ` - ${q(d)}`));
135
+ lines.push(" depends_on:");
136
+ for (const d of dependsOn) {
137
+ lines.push(` ${d}:`, ` condition: ${dependencyCondition(known.get(d))}`);
138
+ }
131
139
  }
140
+ lines.push(...healthcheckLines(name, c.healthCheck));
132
141
  const capAdd = strings(container.capAdd);
133
142
  const capDrop = strings(container.capDrop);
134
143
  const securityOpt = strings(container.securityOpt);
@@ -151,6 +160,45 @@ function serviceLines(resource, outDir, known) {
151
160
  }
152
161
  return { lines, namedVolumes };
153
162
  }
163
+ /** What Compose waits for before starting a dependent: a passing check, a finished one-shot job, or the start. */
164
+ function dependencyCondition(dep) {
165
+ if (hasProbeCommand(dep.config.healthCheck))
166
+ return "service_healthy";
167
+ if (dep.config.restart === "no")
168
+ return "service_completed_successfully";
169
+ return "service_started";
170
+ }
171
+ function hasProbeCommand(value) {
172
+ return strings(record(value).command).length > 0;
173
+ }
174
+ /** A Compose `healthcheck` from `healthCheck.command` (exit 0 is ready); `healthCheck: false` turns it off. */
175
+ function healthcheckLines(name, value) {
176
+ if (value === false || record(value).enabled === false) {
177
+ return [" healthcheck:", " disable: true"];
178
+ }
179
+ const h = record(value);
180
+ const command = strings(h.command);
181
+ if (command.length === 0)
182
+ return [];
183
+ const lines = [" healthcheck:", ` test: [${['"CMD"', ...command.map(q)].join(", ")}]`];
184
+ for (const key of ["interval", "timeout"]) {
185
+ if (h[key] === undefined)
186
+ continue;
187
+ const seconds = Number(h[key]);
188
+ if (!Number.isFinite(seconds) || seconds <= 0) {
189
+ throw new ExportError(`${name}: healthCheck.${key} must be a number of seconds above 0`);
190
+ }
191
+ lines.push(` ${key}: ${q(`${seconds}s`)}`);
192
+ }
193
+ if (h.retries !== undefined) {
194
+ const retries = Number(h.retries);
195
+ if (!Number.isInteger(retries) || retries < 1) {
196
+ throw new ExportError(`${name}: healthCheck.retries must be a whole number above 0`);
197
+ }
198
+ lines.push(` retries: ${retries}`);
199
+ }
200
+ return lines;
201
+ }
154
202
  function resolveFrom(base, rel) {
155
203
  return rel.startsWith("/") ? rel : joinPosix(base, rel);
156
204
  }
@@ -175,7 +223,7 @@ export function buildCompose(outDir, resources) {
175
223
  `Exported type: ${COMPOSE_TYPE} (a service.json with an image or a Dockerfile, and no command)`,
176
224
  ]);
177
225
  }
178
- const known = new Set(services.map((s) => s.name));
226
+ const known = new Map(services.map((s) => [s.name, s]));
179
227
  const body = [];
180
228
  const volumes = new Set();
181
229
  for (const service of services) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flisk-sh/flisk-export",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Write a Render Blueprint (render.yaml), a Cloudflare wrangler.toml or a standalone docker-compose.yml from Flisk service.json files. Never deploys.",
5
5
  "type": "module",
6
6
  "license": "MIT",