@intentius/chant-lexicon-docker 0.1.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 (81) hide show
  1. package/README.md +24 -0
  2. package/dist/integrity.json +19 -0
  3. package/dist/manifest.json +15 -0
  4. package/dist/meta.json +222 -0
  5. package/dist/rules/apt-no-recommends.ts +43 -0
  6. package/dist/rules/docker-helpers.ts +114 -0
  7. package/dist/rules/no-latest-image.ts +36 -0
  8. package/dist/rules/no-latest-tag.ts +63 -0
  9. package/dist/rules/no-root-user.ts +36 -0
  10. package/dist/rules/prefer-copy.ts +53 -0
  11. package/dist/rules/ssh-port-exposed.ts +68 -0
  12. package/dist/rules/unused-volume.ts +49 -0
  13. package/dist/skills/chant-docker-patterns.md +153 -0
  14. package/dist/skills/chant-docker.md +129 -0
  15. package/dist/types/index.d.ts +93 -0
  16. package/package.json +53 -0
  17. package/src/codegen/docs-cli.ts +10 -0
  18. package/src/codegen/docs.ts +12 -0
  19. package/src/codegen/generate-cli.ts +36 -0
  20. package/src/codegen/generate-compose.ts +21 -0
  21. package/src/codegen/generate-dockerfile.ts +21 -0
  22. package/src/codegen/generate.test.ts +105 -0
  23. package/src/codegen/generate.ts +158 -0
  24. package/src/codegen/naming.test.ts +81 -0
  25. package/src/codegen/naming.ts +54 -0
  26. package/src/codegen/package.ts +65 -0
  27. package/src/codegen/patches.ts +42 -0
  28. package/src/codegen/versions.ts +15 -0
  29. package/src/composites/index.ts +12 -0
  30. package/src/coverage.test.ts +33 -0
  31. package/src/coverage.ts +54 -0
  32. package/src/default-labels.test.ts +85 -0
  33. package/src/default-labels.ts +72 -0
  34. package/src/generated/index.d.ts +93 -0
  35. package/src/generated/index.ts +10 -0
  36. package/src/generated/lexicon-docker.json +222 -0
  37. package/src/generated/runtime.ts +4 -0
  38. package/src/import/generator.test.ts +133 -0
  39. package/src/import/generator.ts +127 -0
  40. package/src/import/parser.test.ts +137 -0
  41. package/src/import/parser.ts +190 -0
  42. package/src/import/roundtrip.test.ts +49 -0
  43. package/src/import/testdata/full.yaml +43 -0
  44. package/src/import/testdata/simple.yaml +9 -0
  45. package/src/import/testdata/webapp.yaml +41 -0
  46. package/src/index.ts +29 -0
  47. package/src/interpolation.test.ts +41 -0
  48. package/src/interpolation.ts +76 -0
  49. package/src/lint/post-synth/apt-no-recommends.ts +43 -0
  50. package/src/lint/post-synth/docker-helpers.ts +114 -0
  51. package/src/lint/post-synth/no-latest-image.ts +36 -0
  52. package/src/lint/post-synth/no-root-user.ts +36 -0
  53. package/src/lint/post-synth/post-synth.test.ts +181 -0
  54. package/src/lint/post-synth/prefer-copy.ts +53 -0
  55. package/src/lint/post-synth/ssh-port-exposed.ts +68 -0
  56. package/src/lint/post-synth/unused-volume.ts +49 -0
  57. package/src/lint/rules/data/deprecated-images.ts +28 -0
  58. package/src/lint/rules/data/known-base-images.ts +20 -0
  59. package/src/lint/rules/index.ts +5 -0
  60. package/src/lint/rules/no-latest-tag.ts +63 -0
  61. package/src/lint/rules/rules.test.ts +82 -0
  62. package/src/lsp/completions.test.ts +34 -0
  63. package/src/lsp/completions.ts +20 -0
  64. package/src/lsp/hover.test.ts +34 -0
  65. package/src/lsp/hover.ts +38 -0
  66. package/src/package-cli.ts +42 -0
  67. package/src/plugin.test.ts +117 -0
  68. package/src/plugin.ts +250 -0
  69. package/src/serializer.test.ts +294 -0
  70. package/src/serializer.ts +322 -0
  71. package/src/skills/chant-docker-patterns.md +153 -0
  72. package/src/skills/chant-docker.md +129 -0
  73. package/src/spec/fetch-compose.ts +35 -0
  74. package/src/spec/fetch-engine.ts +25 -0
  75. package/src/spec/parse-compose.ts +110 -0
  76. package/src/spec/parse-engine.ts +47 -0
  77. package/src/validate-cli.ts +19 -0
  78. package/src/validate.test.ts +16 -0
  79. package/src/validate.ts +44 -0
  80. package/src/variables.test.ts +32 -0
  81. package/src/variables.ts +47 -0
@@ -0,0 +1,85 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import {
3
+ DEFAULT_LABELS_MARKER,
4
+ DEFAULT_ANNOTATIONS_MARKER,
5
+ defaultLabels,
6
+ defaultAnnotations,
7
+ isDefaultLabels,
8
+ isDefaultAnnotations,
9
+ } from "./default-labels";
10
+
11
+ describe("DEFAULT_LABELS_MARKER", () => {
12
+ test("is a well-known symbol", () => {
13
+ expect(typeof DEFAULT_LABELS_MARKER).toBe("symbol");
14
+ expect(DEFAULT_LABELS_MARKER.toString()).toContain("docker.defaultLabels");
15
+ });
16
+ });
17
+
18
+ describe("DEFAULT_ANNOTATIONS_MARKER", () => {
19
+ test("is a well-known symbol", () => {
20
+ expect(typeof DEFAULT_ANNOTATIONS_MARKER).toBe("symbol");
21
+ expect(DEFAULT_ANNOTATIONS_MARKER.toString()).toContain("docker.defaultAnnotations");
22
+ });
23
+ });
24
+
25
+ describe("defaultLabels", () => {
26
+ test("creates a DefaultLabels entity", () => {
27
+ const labels = { "app.team": "platform", "app.managed-by": "chant" };
28
+ const entity = defaultLabels(labels);
29
+
30
+ expect(entity.lexicon).toBe("docker");
31
+ expect(entity.entityType).toBe("Docker::DefaultLabels");
32
+ expect(entity.props.labels).toEqual(labels);
33
+ });
34
+
35
+ test("has DEFAULT_LABELS_MARKER", () => {
36
+ const entity = defaultLabels({ "x": "y" });
37
+ expect(DEFAULT_LABELS_MARKER in entity).toBe(true);
38
+ });
39
+ });
40
+
41
+ describe("defaultAnnotations", () => {
42
+ test("creates a DefaultAnnotations entity", () => {
43
+ const annotations = { "app.version": "1.0.0" };
44
+ const entity = defaultAnnotations(annotations);
45
+
46
+ expect(entity.lexicon).toBe("docker");
47
+ expect(entity.entityType).toBe("Docker::DefaultAnnotations");
48
+ expect(entity.props.annotations).toEqual(annotations);
49
+ });
50
+
51
+ test("has DEFAULT_ANNOTATIONS_MARKER", () => {
52
+ const entity = defaultAnnotations({ "x": "y" });
53
+ expect(DEFAULT_ANNOTATIONS_MARKER in entity).toBe(true);
54
+ });
55
+ });
56
+
57
+ describe("isDefaultLabels", () => {
58
+ test("returns true for defaultLabels entity", () => {
59
+ const entity = defaultLabels({ "x": "y" });
60
+ expect(isDefaultLabels(entity)).toBe(true);
61
+ });
62
+
63
+ test("returns false for plain objects", () => {
64
+ expect(isDefaultLabels({})).toBe(false);
65
+ expect(isDefaultLabels(null)).toBe(false);
66
+ expect(isDefaultLabels("string")).toBe(false);
67
+ });
68
+ });
69
+
70
+ describe("isDefaultAnnotations", () => {
71
+ test("returns true for defaultAnnotations entity", () => {
72
+ const entity = defaultAnnotations({ "x": "y" });
73
+ expect(isDefaultAnnotations(entity)).toBe(true);
74
+ });
75
+
76
+ test("returns false for defaultLabels", () => {
77
+ const entity = defaultLabels({ "x": "y" });
78
+ expect(isDefaultAnnotations(entity)).toBe(false);
79
+ });
80
+
81
+ test("returns false for plain objects", () => {
82
+ expect(isDefaultAnnotations({})).toBe(false);
83
+ expect(isDefaultAnnotations(null)).toBe(false);
84
+ });
85
+ });
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Default labels for Docker Compose services.
3
+ *
4
+ * Follows the K8s lexicon pattern for defaultLabels/defaultAnnotations.
5
+ * The serializer merges these into every Compose service's labels map.
6
+ */
7
+
8
+ import { DECLARABLE_MARKER, type Declarable } from "@intentius/chant/declarable";
9
+
10
+ export const DEFAULT_LABELS_MARKER = Symbol.for("docker.defaultLabels");
11
+ export const DEFAULT_ANNOTATIONS_MARKER = Symbol.for("docker.defaultAnnotations");
12
+
13
+ export interface DefaultLabels extends Declarable {
14
+ readonly [DEFAULT_LABELS_MARKER]: true;
15
+ readonly props: { labels: Record<string, string> };
16
+ }
17
+
18
+ export interface DefaultAnnotations extends Declarable {
19
+ readonly [DEFAULT_ANNOTATIONS_MARKER]: true;
20
+ readonly props: { annotations: Record<string, string> };
21
+ }
22
+
23
+ export function isDefaultLabels(value: unknown): value is DefaultLabels {
24
+ return (
25
+ typeof value === "object" &&
26
+ value !== null &&
27
+ DEFAULT_LABELS_MARKER in value
28
+ );
29
+ }
30
+
31
+ export function isDefaultAnnotations(value: unknown): value is DefaultAnnotations {
32
+ return (
33
+ typeof value === "object" &&
34
+ value !== null &&
35
+ DEFAULT_ANNOTATIONS_MARKER in value
36
+ );
37
+ }
38
+
39
+ /**
40
+ * Create a DefaultLabels entity that the serializer will merge into all services.
41
+ *
42
+ * @example
43
+ * export const labels = defaultLabels({
44
+ * "com.example.team": "platform",
45
+ * "com.example.managed-by": "chant",
46
+ * });
47
+ */
48
+ export function defaultLabels(labels: Record<string, string>): DefaultLabels {
49
+ return {
50
+ [DECLARABLE_MARKER]: true,
51
+ [DEFAULT_LABELS_MARKER]: true,
52
+ lexicon: "docker",
53
+ entityType: "Docker::DefaultLabels",
54
+ kind: "resource",
55
+ props: { labels },
56
+ };
57
+ }
58
+
59
+ /**
60
+ * Create a DefaultAnnotations entity.
61
+ * (Not used by Compose directly, but useful for Dockerfile LABEL instructions.)
62
+ */
63
+ export function defaultAnnotations(annotations: Record<string, string>): DefaultAnnotations {
64
+ return {
65
+ [DECLARABLE_MARKER]: true,
66
+ [DEFAULT_ANNOTATIONS_MARKER]: true,
67
+ lexicon: "docker",
68
+ entityType: "Docker::DefaultAnnotations",
69
+ kind: "resource",
70
+ props: { annotations },
71
+ };
72
+ }
@@ -0,0 +1,93 @@
1
+ // Code generated by chant generate. DO NOT EDIT.
2
+ import type { Declarable } from "@intentius/chant/declarable";
3
+
4
+ export interface ServiceProps {
5
+ image?: string;
6
+ build?: object;
7
+ command?: string | string[];
8
+ entrypoint?: string | string[];
9
+ environment?: Record<string, string>;
10
+ ports?: string[];
11
+ volumes?: string[];
12
+ networks?: string[];
13
+ depends_on?: string[];
14
+ restart?: string;
15
+ labels?: Record<string, string>;
16
+ healthcheck?: object;
17
+ deploy?: object;
18
+ secrets?: string[];
19
+ configs?: string[];
20
+ }
21
+ export declare const Service: new (props?: ServiceProps) => Declarable;
22
+
23
+ export interface VolumeProps {
24
+ driver?: string;
25
+ driver_opts?: Record<string, string>;
26
+ external?: boolean;
27
+ labels?: Record<string, string>;
28
+ name?: string;
29
+ }
30
+ export declare const Volume: new (props?: VolumeProps) => Declarable;
31
+
32
+ export interface NetworkProps {
33
+ driver?: string;
34
+ driver_opts?: Record<string, string>;
35
+ external?: boolean;
36
+ labels?: Record<string, string>;
37
+ internal?: boolean;
38
+ ipam?: object;
39
+ }
40
+ export declare const Network: new (props?: NetworkProps) => Declarable;
41
+
42
+ export interface DockerConfigProps {
43
+ file?: string;
44
+ external?: boolean;
45
+ labels?: Record<string, string>;
46
+ name?: string;
47
+ }
48
+ export declare const DockerConfig: new (props?: DockerConfigProps) => Declarable;
49
+
50
+ export interface DockerSecretProps {
51
+ file?: string;
52
+ external?: boolean;
53
+ labels?: Record<string, string>;
54
+ name?: string;
55
+ }
56
+ export declare const DockerSecret: new (props?: DockerSecretProps) => Declarable;
57
+
58
+ export interface DockerfileStage {
59
+ from: string;
60
+ as?: string;
61
+ arg?: string[];
62
+ env?: string[];
63
+ run?: string[];
64
+ copy?: string[];
65
+ add?: string[];
66
+ workdir?: string;
67
+ user?: string;
68
+ expose?: string[];
69
+ volume?: string[];
70
+ label?: string[];
71
+ entrypoint?: string;
72
+ cmd?: string;
73
+ healthcheck?: string;
74
+ }
75
+ export interface DockerfileProps {
76
+ name?: string;
77
+ from?: string;
78
+ stages?: DockerfileStage[];
79
+ arg?: string[];
80
+ env?: string[];
81
+ run?: string[];
82
+ copy?: string[];
83
+ add?: string[];
84
+ workdir?: string;
85
+ user?: string;
86
+ expose?: string[];
87
+ volume?: string[];
88
+ label?: string[];
89
+ entrypoint?: string;
90
+ cmd?: string;
91
+ healthcheck?: string;
92
+ }
93
+ export declare const Dockerfile: new (props?: DockerfileProps) => Declarable;
@@ -0,0 +1,10 @@
1
+ // Code generated by chant generate. DO NOT EDIT.
2
+ import { createResource } from "./runtime";
3
+
4
+ export const Service = createResource("Docker::Compose::Service", "docker", {});
5
+ export const Volume = createResource("Docker::Compose::Volume", "docker", {});
6
+ export const Network = createResource("Docker::Compose::Network", "docker", {});
7
+ export const DockerConfig = createResource("Docker::Compose::Config", "docker", {});
8
+ export const DockerSecret = createResource("Docker::Compose::Secret", "docker", {});
9
+
10
+ export const Dockerfile = createResource("Docker::Dockerfile", "docker", {});
@@ -0,0 +1,222 @@
1
+ {
2
+ "Service": {
3
+ "resourceType": "Docker::Compose::Service",
4
+ "kind": "resource",
5
+ "description": "A containerized service definition in Docker Compose",
6
+ "properties": {
7
+ "image": {
8
+ "type": "string",
9
+ "description": "Container image to use"
10
+ },
11
+ "build": {
12
+ "type": "object",
13
+ "description": "Build configuration"
14
+ },
15
+ "command": {
16
+ "type": "string | string[]",
17
+ "description": "Override the default command"
18
+ },
19
+ "entrypoint": {
20
+ "type": "string | string[]",
21
+ "description": "Override the default entrypoint"
22
+ },
23
+ "environment": {
24
+ "type": "Record<string, string>",
25
+ "description": "Environment variables"
26
+ },
27
+ "ports": {
28
+ "type": "string[]",
29
+ "description": "Published ports"
30
+ },
31
+ "volumes": {
32
+ "type": "string[]",
33
+ "description": "Volume mounts"
34
+ },
35
+ "networks": {
36
+ "type": "string[]",
37
+ "description": "Networks to attach"
38
+ },
39
+ "depends_on": {
40
+ "type": "string[]",
41
+ "description": "Service dependencies"
42
+ },
43
+ "restart": {
44
+ "type": "string",
45
+ "description": "Restart policy"
46
+ },
47
+ "labels": {
48
+ "type": "Record<string, string>",
49
+ "description": "Container labels"
50
+ },
51
+ "healthcheck": {
52
+ "type": "object",
53
+ "description": "Health check configuration"
54
+ },
55
+ "deploy": {
56
+ "type": "object",
57
+ "description": "Swarm deployment configuration"
58
+ },
59
+ "secrets": {
60
+ "type": "string[]",
61
+ "description": "Secrets to expose"
62
+ },
63
+ "configs": {
64
+ "type": "string[]",
65
+ "description": "Configs to expose"
66
+ }
67
+ }
68
+ },
69
+ "Volume": {
70
+ "resourceType": "Docker::Compose::Volume",
71
+ "kind": "resource",
72
+ "description": "A named volume definition in Docker Compose",
73
+ "properties": {
74
+ "driver": {
75
+ "type": "string",
76
+ "description": "Volume driver"
77
+ },
78
+ "driver_opts": {
79
+ "type": "Record<string, string>",
80
+ "description": "Driver options"
81
+ },
82
+ "external": {
83
+ "type": "boolean",
84
+ "description": "Whether the volume is external"
85
+ },
86
+ "labels": {
87
+ "type": "Record<string, string>",
88
+ "description": "Volume labels"
89
+ },
90
+ "name": {
91
+ "type": "string",
92
+ "description": "Custom volume name"
93
+ }
94
+ }
95
+ },
96
+ "Network": {
97
+ "resourceType": "Docker::Compose::Network",
98
+ "kind": "resource",
99
+ "description": "A network definition in Docker Compose",
100
+ "properties": {
101
+ "driver": {
102
+ "type": "string",
103
+ "description": "Network driver"
104
+ },
105
+ "driver_opts": {
106
+ "type": "Record<string, string>",
107
+ "description": "Driver options"
108
+ },
109
+ "external": {
110
+ "type": "boolean",
111
+ "description": "Whether the network is external"
112
+ },
113
+ "labels": {
114
+ "type": "Record<string, string>",
115
+ "description": "Network labels"
116
+ },
117
+ "internal": {
118
+ "type": "boolean",
119
+ "description": "Restrict external access"
120
+ },
121
+ "ipam": {
122
+ "type": "object",
123
+ "description": "IP address management"
124
+ }
125
+ }
126
+ },
127
+ "DockerConfig": {
128
+ "resourceType": "Docker::Compose::Config",
129
+ "kind": "resource",
130
+ "description": "A config definition in Docker Compose",
131
+ "properties": {
132
+ "file": {
133
+ "type": "string",
134
+ "description": "Path to the config file"
135
+ },
136
+ "external": {
137
+ "type": "boolean",
138
+ "description": "Whether the config is external"
139
+ },
140
+ "labels": {
141
+ "type": "Record<string, string>",
142
+ "description": "Config labels"
143
+ },
144
+ "name": {
145
+ "type": "string",
146
+ "description": "Custom config name"
147
+ }
148
+ }
149
+ },
150
+ "DockerSecret": {
151
+ "resourceType": "Docker::Compose::Secret",
152
+ "kind": "resource",
153
+ "description": "A secret definition in Docker Compose",
154
+ "properties": {
155
+ "file": {
156
+ "type": "string",
157
+ "description": "Path to the secret file"
158
+ },
159
+ "external": {
160
+ "type": "boolean",
161
+ "description": "Whether the secret is external"
162
+ },
163
+ "labels": {
164
+ "type": "Record<string, string>",
165
+ "description": "Secret labels"
166
+ },
167
+ "name": {
168
+ "type": "string",
169
+ "description": "Custom secret name"
170
+ }
171
+ }
172
+ },
173
+ "Dockerfile": {
174
+ "resourceType": "Docker::Dockerfile",
175
+ "kind": "resource",
176
+ "description": "A Dockerfile definition with ordered build instructions",
177
+ "properties": {
178
+ "from": {
179
+ "description": "Base image (required first instruction)"
180
+ },
181
+ "arg": {
182
+ "description": "Build-time argument"
183
+ },
184
+ "env": {
185
+ "description": "Environment variable"
186
+ },
187
+ "run": {
188
+ "description": "Run a command during build"
189
+ },
190
+ "copy": {
191
+ "description": "Copy files from build context"
192
+ },
193
+ "add": {
194
+ "description": "Add files (supports URLs and archives)"
195
+ },
196
+ "workdir": {
197
+ "description": "Set working directory"
198
+ },
199
+ "user": {
200
+ "description": "Set user for subsequent instructions"
201
+ },
202
+ "expose": {
203
+ "description": "Document exposed ports"
204
+ },
205
+ "volume": {
206
+ "description": "Create mount points"
207
+ },
208
+ "label": {
209
+ "description": "Add metadata labels"
210
+ },
211
+ "entrypoint": {
212
+ "description": "Set the entrypoint executable"
213
+ },
214
+ "cmd": {
215
+ "description": "Default command arguments"
216
+ },
217
+ "healthcheck": {
218
+ "description": "Health check instruction"
219
+ }
220
+ }
221
+ }
222
+ }
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Runtime factory constructors — re-exported from core.
3
+ */
4
+ export { createResource, createProperty } from "@intentius/chant/runtime";
@@ -0,0 +1,133 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { DockerGenerator } from "./generator";
3
+ import type { ServiceIR, VolumeIR, NetworkIR, ConfigIR, SecretIR, DockerfileIR } from "./parser";
4
+
5
+ describe("DockerGenerator", () => {
6
+ test("generates Service from IR with image", () => {
7
+ const entities: ServiceIR[] = [
8
+ { kind: "service", name: "api", props: { image: "nginx:1.25" } },
9
+ ];
10
+ const { source } = new DockerGenerator().generate(entities);
11
+ expect(source).toContain("import { Service }");
12
+ expect(source).toContain("new Service(");
13
+ expect(source).toContain("nginx:1.25");
14
+ });
15
+
16
+ test("generates Service with ports and environment", () => {
17
+ const entities: ServiceIR[] = [
18
+ {
19
+ kind: "service",
20
+ name: "api",
21
+ props: {
22
+ image: "node:20",
23
+ ports: ["3000:3000"],
24
+ environment: { NODE_ENV: "production" },
25
+ },
26
+ },
27
+ ];
28
+ const { source } = new DockerGenerator().generate(entities);
29
+ expect(source).toContain("3000:3000");
30
+ expect(source).toContain("NODE_ENV");
31
+ expect(source).toContain("production");
32
+ });
33
+
34
+ test("generates Config entity", () => {
35
+ const entities: ConfigIR[] = [
36
+ { kind: "config", name: "app-config", props: { file: "./config/app.conf" } },
37
+ ];
38
+ const { source } = new DockerGenerator().generate(entities);
39
+ expect(source).toContain("import { Config }");
40
+ expect(source).toContain("new Config(");
41
+ expect(source).toContain("app.conf");
42
+ });
43
+
44
+ test("generates Secret entity", () => {
45
+ const entities: SecretIR[] = [
46
+ { kind: "secret", name: "db-password", props: { file: "./secrets/db.txt" } },
47
+ ];
48
+ const { source } = new DockerGenerator().generate(entities);
49
+ expect(source).toContain("import { Secret }");
50
+ expect(source).toContain("new Secret(");
51
+ });
52
+
53
+ test("single-stage Dockerfile generates flat props (existing behaviour)", () => {
54
+ const entities: DockerfileIR[] = [
55
+ {
56
+ kind: "dockerfile",
57
+ name: "builder",
58
+ stages: [
59
+ {
60
+ from: "node:20-alpine",
61
+ instructions: [
62
+ { instruction: "RUN", value: "npm ci" },
63
+ { instruction: "CMD", value: '["node", "index.js"]' },
64
+ ],
65
+ },
66
+ ],
67
+ },
68
+ ];
69
+ const { source } = new DockerGenerator().generate(entities);
70
+ expect(source).toContain("import { Dockerfile }");
71
+ expect(source).toContain("new Dockerfile(");
72
+ expect(source).toContain("node:20-alpine");
73
+ expect(source).not.toContain('"stages"');
74
+ });
75
+
76
+ test("multi-stage Dockerfile generates stages: array form", () => {
77
+ const entities: DockerfileIR[] = [
78
+ {
79
+ kind: "dockerfile",
80
+ name: "app",
81
+ stages: [
82
+ { from: "node:20-alpine", as: "builder", instructions: [{ instruction: "RUN", value: "npm ci" }] },
83
+ { from: "nginx:1.25", as: "runner", instructions: [] },
84
+ ],
85
+ },
86
+ ];
87
+ const { source } = new DockerGenerator().generate(entities);
88
+ expect(source).toContain("stages");
89
+ expect(source).toContain("node:20-alpine");
90
+ expect(source).toContain("nginx:1.25");
91
+ });
92
+
93
+ test("import line includes only needed types", () => {
94
+ const entities: VolumeIR[] = [
95
+ { kind: "volume", name: "data", props: {} },
96
+ ];
97
+ const { source } = new DockerGenerator().generate(entities);
98
+ expect(source).toContain("import { Volume }");
99
+ expect(source).not.toContain("Service");
100
+ expect(source).not.toContain("Network");
101
+ });
102
+
103
+ test("generates correct imports for multiple entity types", () => {
104
+ const entities = [
105
+ { kind: "service" as const, name: "api", props: { image: "nginx:1.25" } },
106
+ {
107
+ kind: "dockerfile" as const,
108
+ name: "build",
109
+ stages: [{ from: "node:20", instructions: [] }],
110
+ },
111
+ ];
112
+ const { source } = new DockerGenerator().generate(entities);
113
+ expect(source).toContain("Dockerfile");
114
+ expect(source).toContain("Service");
115
+ });
116
+
117
+ test("sanitizes kebab-case names to camelCase", () => {
118
+ const entities: ServiceIR[] = [
119
+ { kind: "service", name: "my-web-server", props: { image: "nginx:1.25" } },
120
+ ];
121
+ const { source } = new DockerGenerator().generate(entities);
122
+ expect(source).toContain("myWebServer");
123
+ });
124
+
125
+ test("generates Network entity", () => {
126
+ const entities: NetworkIR[] = [
127
+ { kind: "network", name: "backend", props: { driver: "bridge" } },
128
+ ];
129
+ const { source } = new DockerGenerator().generate(entities);
130
+ expect(source).toContain("import { Network }");
131
+ expect(source).toContain("new Network(");
132
+ });
133
+ });