@flisk-sh/flisk-export 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -47,6 +47,7 @@ flisk-export compose --check # exit 1 when that file is out of date (for CI)
47
47
  Writes a **standalone** `docker-compose.yml` from the bring-your-own `service.json` files (`image` or `dockerfile`, `env`,
48
48
  `secrets.required`, `models`, `volumes`, `extraPorts`, `dependsOn`, `healthCheck`, `sidecarOf`, `container`). It runs with plain `docker compose up` and no Flisk: no external
49
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
+ A volume's `name` (the Docker volume an install already has) is kept, and the project is named after its stack, so `docker compose up` finds the same data as before.
50
51
  `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`.
51
52
  Other resource types and `command` (native) services are not exported. The file is generated: change `service.json` and run it
52
53
  again with `--force`. It never starts a container or calls an API.
package/dist/compose.js CHANGED
@@ -13,6 +13,7 @@ const RESTART_POLICIES = new Set(["no", "on-failure", "unless-stopped", "always"
13
13
  const SERVICE_NAME = /^[a-z0-9][a-z0-9_.-]*$/i;
14
14
  const ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
15
15
  const SHM_SIZE = /^\d+(b|k|kb|m|mb|g|gb)?$/i;
16
+ const DOCKER_NAME = /^[A-Za-z0-9][A-Za-z0-9_.-]*$/;
16
17
  export function isComposeExportable(resource) {
17
18
  return resource.type === COMPOSE_TYPE && typeof resource.config.command !== "string";
18
19
  }
@@ -50,7 +51,7 @@ function serviceLines(resource, outDir, services) {
50
51
  ]);
51
52
  }
52
53
  const lines = [` ${name}:`];
53
- const namedVolumes = new Set();
54
+ const namedVolumes = new Map();
54
55
  const models = new Map();
55
56
  const image = typeof c.image === "string" && c.image ? c.image : undefined;
56
57
  const dockerfile = typeof c.dockerfile === "string" && c.dockerfile ? c.dockerfile : undefined;
@@ -105,7 +106,11 @@ function serviceLines(resource, outDir, services) {
105
106
  }
106
107
  let source = v.source;
107
108
  if (isNamedVolume(source)) {
108
- namedVolumes.add(source);
109
+ const dockerName = typeof v.name === "string" ? v.name : undefined;
110
+ if (dockerName !== undefined && !DOCKER_NAME.test(dockerName)) {
111
+ throw new ExportError(`${name}: volume name "${dockerName}" is not a Docker volume name`);
112
+ }
113
+ namedVolumes.set(source, dockerName ?? namedVolumes.get(source));
109
114
  }
110
115
  else {
111
116
  source = hostPath(outDir, resolveFrom(resource.path, source));
@@ -240,14 +245,22 @@ export function buildCompose(outDir, resources) {
240
245
  ]);
241
246
  }
242
247
  const known = new Map(services.map((s) => [s.name, s]));
248
+ const stacks = new Set(services
249
+ .map((s) => s.config.stack)
250
+ .filter((v) => typeof v === "string" && /^[a-z0-9][a-z0-9_-]*$/.test(v)));
243
251
  const body = [];
244
- const volumes = new Set();
252
+ const volumes = new Map();
245
253
  const models = new Map();
246
254
  for (const service of services) {
247
255
  const built = serviceLines(service, outDir, known);
248
256
  body.push(...built.lines, "");
249
- for (const v of built.namedVolumes)
250
- volumes.add(v);
257
+ for (const [v, dockerName] of built.namedVolumes) {
258
+ const known = volumes.get(v);
259
+ if (known && dockerName && known !== dockerName) {
260
+ throw new ExportError(`volume "${v}" is given two names, "${known}" and "${dockerName}"`);
261
+ }
262
+ volumes.set(v, dockerName ?? known);
263
+ }
251
264
  for (const [n, m] of built.models)
252
265
  models.set(n, m);
253
266
  }
@@ -259,6 +272,9 @@ export function buildCompose(outDir, resources) {
259
272
  "# Runs with plain `docker compose up`; Flisk is not needed to start it.",
260
273
  "###############################################################################",
261
274
  "",
275
+ // Without a name Compose takes the folder's (autogenerated), so containers and unnamed volumes would not match the
276
+ // project the app had before. One stack gives the project its name.
277
+ ...(stacks.size === 1 ? [`name: ${q([...stacks][0])}`, ""] : []),
262
278
  "services:",
263
279
  ];
264
280
  const tail = [
@@ -269,7 +285,16 @@ export function buildCompose(outDir, resources) {
269
285
  "",
270
286
  ]
271
287
  : []),
272
- ...(volumes.size > 0 ? ["volumes:", ...[...volumes].sort().map((v) => ` ${v}:`), ""] : []),
288
+ // A volume with a name keeps the Docker volume an existing install has, so its data is used as it is.
289
+ ...(volumes.size > 0
290
+ ? [
291
+ "volumes:",
292
+ ...[...volumes]
293
+ .sort(([a], [b]) => a.localeCompare(b))
294
+ .flatMap(([v, dockerName]) => dockerName ? [` ${v}:`, ` name: ${q(dockerName)}`] : [` ${v}:`]),
295
+ "",
296
+ ]
297
+ : []),
273
298
  ];
274
299
  return [...header, ...body, ...tail].join("\n").replace(/\n{3,}/g, "\n\n");
275
300
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flisk-sh/flisk-export",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
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",