@flisk-sh/flisk-export 0.3.0 → 0.4.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 +1 -0
- package/dist/compose.js +46 -8
- package/package.json +1 -1
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
|
@@ -7,12 +7,15 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { relative, sep } from "node:path";
|
|
9
9
|
import { ExportError } from "./errors.js";
|
|
10
|
+
/** extraPorts `publish`: digits, dots, colons and `${NAME:-default}`, as flisk core accepts it (flisk#2947). */
|
|
11
|
+
const PUBLISH = /^(?:[0-9.:]|\$\{[A-Za-z_][A-Za-z0-9_]*:-[0-9.:]+\})+$/;
|
|
10
12
|
/** The only resource type the Compose export covers: it is the one whose service.json is a Compose service. */
|
|
11
13
|
export const COMPOSE_TYPE = "bring-your-own";
|
|
12
14
|
const RESTART_POLICIES = new Set(["no", "on-failure", "unless-stopped", "always"]);
|
|
13
15
|
const SERVICE_NAME = /^[a-z0-9][a-z0-9_.-]*$/i;
|
|
14
16
|
const ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
15
17
|
const SHM_SIZE = /^\d+(b|k|kb|m|mb|g|gb)?$/i;
|
|
18
|
+
const DOCKER_NAME = /^[A-Za-z0-9][A-Za-z0-9_.-]*$/;
|
|
16
19
|
export function isComposeExportable(resource) {
|
|
17
20
|
return resource.type === COMPOSE_TYPE && typeof resource.config.command !== "string";
|
|
18
21
|
}
|
|
@@ -50,7 +53,7 @@ function serviceLines(resource, outDir, services) {
|
|
|
50
53
|
]);
|
|
51
54
|
}
|
|
52
55
|
const lines = [` ${name}:`];
|
|
53
|
-
const namedVolumes = new
|
|
56
|
+
const namedVolumes = new Map();
|
|
54
57
|
const models = new Map();
|
|
55
58
|
const image = typeof c.image === "string" && c.image ? c.image : undefined;
|
|
56
59
|
const dockerfile = typeof c.dockerfile === "string" && c.dockerfile ? c.dockerfile : undefined;
|
|
@@ -105,7 +108,11 @@ function serviceLines(resource, outDir, services) {
|
|
|
105
108
|
}
|
|
106
109
|
let source = v.source;
|
|
107
110
|
if (isNamedVolume(source)) {
|
|
108
|
-
|
|
111
|
+
const dockerName = typeof v.name === "string" ? v.name : undefined;
|
|
112
|
+
if (dockerName !== undefined && !DOCKER_NAME.test(dockerName)) {
|
|
113
|
+
throw new ExportError(`${name}: volume name "${dockerName}" is not a Docker volume name`);
|
|
114
|
+
}
|
|
115
|
+
namedVolumes.set(source, dockerName ?? namedVolumes.get(source));
|
|
109
116
|
}
|
|
110
117
|
else {
|
|
111
118
|
source = hostPath(outDir, resolveFrom(resource.path, source));
|
|
@@ -125,9 +132,20 @@ function serviceLines(resource, outDir, services) {
|
|
|
125
132
|
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
126
133
|
throw new ExportError(`${name}: extraPorts needs a port from 1 to 65535`);
|
|
127
134
|
}
|
|
128
|
-
const hostPort = p.hostPort === undefined ? port : Number(p.hostPort);
|
|
129
|
-
const bind = typeof p.bind === "string" && p.bind ? p.bind : "127.0.0.1";
|
|
130
135
|
const protocol = p.protocol === "udp" ? "udp" : "tcp";
|
|
136
|
+
// `publish` (flisk#2947): the host side as the project's Compose wrote it, e.g. "${APP_HTTP_BIND:-80}"; Compose resolves it.
|
|
137
|
+
if (p.publish !== undefined) {
|
|
138
|
+
if (typeof p.publish !== "string" || !PUBLISH.test(p.publish)) {
|
|
139
|
+
throw new ExportError(`${name}: extraPorts publish must be a port, an IPv4 address:port, or \${NAME:-default}`);
|
|
140
|
+
}
|
|
141
|
+
lines.push(` - ${q(`${p.publish}:${port}/${protocol}`)}`);
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
const hostPort = p.hostPort === undefined ? port : Number(p.hostPort);
|
|
145
|
+
const bind = p.bind === undefined ? "127.0.0.1" : p.bind;
|
|
146
|
+
if (bind !== "127.0.0.1" && bind !== "0.0.0.0") {
|
|
147
|
+
throw new ExportError(`${name}: extraPorts bind must be 127.0.0.1 or 0.0.0.0`);
|
|
148
|
+
}
|
|
131
149
|
lines.push(` - ${q(`${bind}:${hostPort}:${port}/${protocol}`)}`);
|
|
132
150
|
}
|
|
133
151
|
}
|
|
@@ -240,14 +258,22 @@ export function buildCompose(outDir, resources) {
|
|
|
240
258
|
]);
|
|
241
259
|
}
|
|
242
260
|
const known = new Map(services.map((s) => [s.name, s]));
|
|
261
|
+
const stacks = new Set(services
|
|
262
|
+
.map((s) => s.config.stack)
|
|
263
|
+
.filter((v) => typeof v === "string" && /^[a-z0-9][a-z0-9_-]*$/.test(v)));
|
|
243
264
|
const body = [];
|
|
244
|
-
const volumes = new
|
|
265
|
+
const volumes = new Map();
|
|
245
266
|
const models = new Map();
|
|
246
267
|
for (const service of services) {
|
|
247
268
|
const built = serviceLines(service, outDir, known);
|
|
248
269
|
body.push(...built.lines, "");
|
|
249
|
-
for (const v of built.namedVolumes)
|
|
250
|
-
volumes.
|
|
270
|
+
for (const [v, dockerName] of built.namedVolumes) {
|
|
271
|
+
const known = volumes.get(v);
|
|
272
|
+
if (known && dockerName && known !== dockerName) {
|
|
273
|
+
throw new ExportError(`volume "${v}" is given two names, "${known}" and "${dockerName}"`);
|
|
274
|
+
}
|
|
275
|
+
volumes.set(v, dockerName ?? known);
|
|
276
|
+
}
|
|
251
277
|
for (const [n, m] of built.models)
|
|
252
278
|
models.set(n, m);
|
|
253
279
|
}
|
|
@@ -259,6 +285,9 @@ export function buildCompose(outDir, resources) {
|
|
|
259
285
|
"# Runs with plain `docker compose up`; Flisk is not needed to start it.",
|
|
260
286
|
"###############################################################################",
|
|
261
287
|
"",
|
|
288
|
+
// Without a name Compose takes the folder's (autogenerated), so containers and unnamed volumes would not match the
|
|
289
|
+
// project the app had before. One stack gives the project its name.
|
|
290
|
+
...(stacks.size === 1 ? [`name: ${q([...stacks][0])}`, ""] : []),
|
|
262
291
|
"services:",
|
|
263
292
|
];
|
|
264
293
|
const tail = [
|
|
@@ -269,7 +298,16 @@ export function buildCompose(outDir, resources) {
|
|
|
269
298
|
"",
|
|
270
299
|
]
|
|
271
300
|
: []),
|
|
272
|
-
|
|
301
|
+
// A volume with a name keeps the Docker volume an existing install has, so its data is used as it is.
|
|
302
|
+
...(volumes.size > 0
|
|
303
|
+
? [
|
|
304
|
+
"volumes:",
|
|
305
|
+
...[...volumes]
|
|
306
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
307
|
+
.flatMap(([v, dockerName]) => dockerName ? [` ${v}:`, ` name: ${q(dockerName)}`] : [` ${v}:`]),
|
|
308
|
+
"",
|
|
309
|
+
]
|
|
310
|
+
: []),
|
|
273
311
|
];
|
|
274
312
|
return [...header, ...body, ...tail].join("\n").replace(/\n{3,}/g, "\n\n");
|
|
275
313
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flisk-sh/flisk-export",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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",
|