@intentius/chant-lexicon-docker 0.124.31 → 0.124.33

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 (97) hide show
  1. package/dist/codegen/docs-cli.js +7 -0
  2. package/dist/codegen/docs.js +163 -0
  3. package/dist/codegen/generate-cli.js +26 -0
  4. package/dist/codegen/generate-compose.js +16 -0
  5. package/dist/codegen/generate-dockerfile.js +16 -0
  6. package/dist/codegen/generate.d.ts.map +1 -1
  7. package/dist/codegen/generate.js +212 -0
  8. package/dist/codegen/naming.js +45 -0
  9. package/dist/codegen/package.js +40 -0
  10. package/dist/codegen/patches.js +31 -0
  11. package/dist/codegen/versions.js +41 -0
  12. package/dist/compose-key.d.ts +19 -0
  13. package/dist/compose-key.d.ts.map +1 -0
  14. package/dist/compose-key.js +26 -0
  15. package/dist/composites/catalog.js +113 -0
  16. package/dist/composites/docker-otel-collector.js +43 -0
  17. package/dist/composites/docker-web-service.js +51 -0
  18. package/dist/composites/index.js +6 -0
  19. package/dist/coverage.js +35 -0
  20. package/dist/default-labels.js +52 -0
  21. package/dist/detect.js +8 -0
  22. package/dist/dockerfile-order.js +16 -0
  23. package/dist/generated/index.d.ts +10 -0
  24. package/dist/generated/index.d.ts.map +1 -1
  25. package/dist/generated/index.js +8 -0
  26. package/dist/generated/runtime.js +4 -0
  27. package/dist/import/adapter.d.ts.map +1 -1
  28. package/dist/import/adapter.js +86 -0
  29. package/dist/import/generator.d.ts.map +1 -1
  30. package/dist/import/generator.js +159 -0
  31. package/dist/import/parser.d.ts +31 -0
  32. package/dist/import/parser.d.ts.map +1 -1
  33. package/dist/import/parser.js +181 -0
  34. package/dist/index.d.ts +1 -0
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +20 -0
  37. package/dist/integrity.json +3 -3
  38. package/dist/interpolation.js +54 -0
  39. package/dist/lint/audit-catalog.js +25 -0
  40. package/dist/lint/audit-lineage.js +39 -0
  41. package/dist/lint/post-synth/add-url-checksum.js +35 -0
  42. package/dist/lint/post-synth/apt-no-recommends.js +34 -0
  43. package/dist/lint/post-synth/apt-update-alone.js +34 -0
  44. package/dist/lint/post-synth/cache-busting-copy.js +61 -0
  45. package/dist/lint/post-synth/copy-from-stage.js +53 -0
  46. package/dist/lint/post-synth/docker-helpers.js +148 -0
  47. package/dist/lint/post-synth/index.js +30 -0
  48. package/dist/lint/post-synth/no-latest-image.js +30 -0
  49. package/dist/lint/post-synth/no-root-user.js +35 -0
  50. package/dist/lint/post-synth/prefer-copy.js +43 -0
  51. package/dist/lint/post-synth/relative-workdir.js +34 -0
  52. package/dist/lint/post-synth/repeated-cmd.js +30 -0
  53. package/dist/lint/post-synth/root-after-user.js +39 -0
  54. package/dist/lint/post-synth/ssh-port-exposed.js +58 -0
  55. package/dist/lint/post-synth/unused-global-arg.js +34 -0
  56. package/dist/lint/post-synth/unused-volume.js +41 -0
  57. package/dist/lint/rules/data/deprecated-images.js +26 -0
  58. package/dist/lint/rules/data/known-base-images.js +18 -0
  59. package/dist/lint/rules/index.js +5 -0
  60. package/dist/lint/rules/no-latest-tag.js +57 -0
  61. package/dist/lint/rules/per-kind-order.js +129 -0
  62. package/dist/list-artifacts.js +133 -0
  63. package/dist/lsp/completions.d.ts.map +1 -1
  64. package/dist/lsp/completions.js +18 -0
  65. package/dist/lsp/hover.d.ts.map +1 -1
  66. package/dist/lsp/hover.js +33 -0
  67. package/dist/manifest.json +1 -1
  68. package/dist/package-cli.js +20 -0
  69. package/dist/plugin.js +229 -0
  70. package/dist/serializer.d.ts.map +1 -1
  71. package/dist/serializer.js +390 -0
  72. package/dist/spec/fetch-compose.js +28 -0
  73. package/dist/spec/fetch-engine.js +19 -0
  74. package/dist/spec/parse-compose.js +160 -0
  75. package/dist/spec/parse-engine.js +36 -0
  76. package/dist/types/index.d.ts +10 -0
  77. package/dist/validate-cli.js +16 -0
  78. package/dist/validate.js +37 -0
  79. package/dist/variables.js +42 -0
  80. package/package.json +8 -8
  81. package/src/codegen/generate.test.ts +10 -0
  82. package/src/codegen/generate.ts +3 -0
  83. package/src/codegen/package.ts +2 -2
  84. package/src/compose-key.ts +31 -0
  85. package/src/generated/index.d.ts +10 -0
  86. package/src/generated/index.ts +10 -0
  87. package/src/import/adapter.ts +2 -1
  88. package/src/import/generator.test.ts +5 -4
  89. package/src/import/generator.ts +75 -48
  90. package/src/import/parser.ts +58 -65
  91. package/src/import/roundtrip.test.ts +46 -3
  92. package/src/import/testdata/names-and-fields.yaml +44 -0
  93. package/src/index.ts +3 -0
  94. package/src/lsp/completions.ts +2 -1
  95. package/src/lsp/hover.ts +2 -1
  96. package/src/serializer.test.ts +31 -0
  97. package/src/serializer.ts +15 -2
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * CLI entry for Docker lexicon docs generation.
4
+ */
5
+ import { generateDocs } from "./docs.js";
6
+ const verbose = process.argv.includes("--verbose") || process.argv.includes("-v");
7
+ await generateDocs({ verbose });
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Docker lexicon docs generation.
3
+ *
4
+ * Uses the shared docsPipeline/writeDocsSite flow like every other lexicon
5
+ * (chant #1731, #1755). Hand-written prose lives under docs/pages/, each
6
+ * page tagged with a Diátaxis quadrant; index.mdx and serialization.mdx are
7
+ * generated from the overview/outputFormat strings below, and rules.mdx is
8
+ * generated from the rule sources under src/lint/ the same way it always
9
+ * was — this used to be the one lexicon whose rules table could drift from
10
+ * source without anything catching it (#1312), and the shared pipeline
11
+ * keeps that guarantee.
12
+ */
13
+ import { dirname, join } from "path";
14
+ import { fileURLToPath } from "url";
15
+ import { docsPipeline, writeDocsSite } from "@intentius/chant/codegen/docs";
16
+ const overview = `
17
+ The **Docker** lexicon provides typed constructors for Docker Compose services
18
+ and Dockerfile build instructions — services, volumes, networks, configs, secrets,
19
+ and multi-stage Dockerfiles.
20
+
21
+ \`\`\`bash
22
+ npm install --save-dev @intentius/chant-lexicon-docker
23
+ \`\`\`
24
+
25
+ ## Quick Start
26
+
27
+ \`\`\`typescript
28
+ import { Service, Volume, Dockerfile, env } from "@intentius/chant-lexicon-docker";
29
+
30
+ export const db = new Service({
31
+ image: "postgres:16-alpine",
32
+ environment: {
33
+ POSTGRES_DB: "myapp",
34
+ POSTGRES_PASSWORD: env("DB_PASSWORD", { required: true }),
35
+ },
36
+ volumes: ["pgdata:/var/lib/postgresql/data"],
37
+ });
38
+
39
+ export const pgdata = new Volume({});
40
+
41
+ export const api = new Service({
42
+ image: "myapp:1.0",
43
+ ports: ["8080:8080"],
44
+ depends_on: ["db"],
45
+ });
46
+ \`\`\`
47
+
48
+ Build:
49
+
50
+ \`\`\`bash
51
+ chant build src --lexicon docker -o docker-compose.yml
52
+ \`\`\`
53
+
54
+ ## Output Domains
55
+
56
+ | Entity | Output file | Section |
57
+ |--------|-------------|---------|
58
+ | \`Service\` | \`docker-compose.yml\` | \`services:\` |
59
+ | \`Volume\` | \`docker-compose.yml\` | \`volumes:\` |
60
+ | \`Network\` | \`docker-compose.yml\` | \`networks:\` |
61
+ | \`Config\` | \`docker-compose.yml\` | \`configs:\` |
62
+ | \`Secret\` | \`docker-compose.yml\` | \`secrets:\` |
63
+ | \`Dockerfile\` | \`Dockerfile.{name}\` | — |`;
64
+ const outputFormat = `
65
+ The Docker lexicon serializes to two output formats.
66
+
67
+ ## docker-compose.yml
68
+
69
+ \`\`\`bash
70
+ chant build src --lexicon docker -o docker-compose.yml
71
+ \`\`\`
72
+
73
+ Each entity type maps to a top-level Compose section. The TypeScript export name becomes the key within that section.
74
+
75
+ \`\`\`typescript
76
+ export const db = new Service({ image: "postgres:16-alpine" });
77
+ export const pgdata = new Volume({});
78
+ export const backend = new Network({ driver: "bridge" });
79
+ \`\`\`
80
+
81
+ Produces:
82
+
83
+ \`\`\`yaml
84
+ services:
85
+ db:
86
+ image: postgres:16-alpine
87
+
88
+ volumes:
89
+ pgdata: null
90
+
91
+ networks:
92
+ backend:
93
+ driver: bridge
94
+ \`\`\`
95
+
96
+ \`defaultLabels()\` entities are not emitted as separate keys — their labels are merged into each service.
97
+
98
+ ## Dockerfile.\\{name\\}
99
+
100
+ \`\`\`bash
101
+ chant build src --lexicon docker
102
+ # Writes Dockerfile.app, Dockerfile.builder, etc.
103
+ \`\`\`
104
+
105
+ Each \`Dockerfile\` entity produces a separate file. The export name is the suffix.
106
+
107
+ \`\`\`typescript
108
+ export const builder = new Dockerfile({ from: "node:20-alpine", ... });
109
+ // → Dockerfile.builder
110
+ \`\`\`
111
+
112
+ ## Field mapping
113
+
114
+ | TypeScript | docker-compose.yml | Notes |
115
+ |-----------|-------------------|-------|
116
+ | \`env("X", { required: true })\` | \`\${X:?X is required}\` | Compose interpolation |
117
+ | \`env("X", { default: "v" })\` | \`\${X:-v}\` | Compose interpolation |
118
+ | \`env("X")\` | \`\${X}\` | Compose interpolation |
119
+ | \`external: true\` | \`external: true\` | Volume/Network |
120
+
121
+ ## Specifying lexicon at build time
122
+
123
+ The \`--lexicon docker\` flag restricts output to Docker entities only — useful in mixed-lexicon projects:
124
+
125
+ \`\`\`bash
126
+ # Only Docker output
127
+ chant build src --lexicon docker -o docker-compose.yml
128
+
129
+ # Other lexicons still get their own outputs
130
+ chant build src --lexicon aws -o template.json
131
+ \`\`\``;
132
+ // "Optional: hand this page to your coding agent", shown under the overview's
133
+ // title. The Dockerfile shape is the ordered \`steps\` list from chant #3926.
134
+ const prompt = `Read https://intentius.io/chant/lexicons/docker/ and https://intentius.io/chant/lexicons/docker/dockerfiles/ first.
135
+ Bring this project's Docker files into chant TypeScript under src/.
136
+ Import each existing file. \`-o\` names a directory, and each import writes main.ts in it: \`chant import --lexicon docker docker-compose.yml -o src/compose/\`, and \`chant import --lexicon docker Dockerfile -o src/<name>/\` for each Dockerfile.
137
+ An imported Dockerfile is exported as \`app\`. Rename each export after its image, since the export name sets the output file (Dockerfile.<export>), and point the Compose service's \`build.dockerfile\` at that export (\`dockerfile: webImage\`) instead of the old file name.
138
+ Write every Dockerfile stage as an ordered \`steps\` list, one instruction per step, with FROM as the stage's \`from\` and ARGs before the first FROM in \`globalArgs\`:
139
+ steps: [{ workdir: "/app" }, { copy: "package*.json ./" }, { run: "npm ci" }, { copy: ". ." }, { user: "node" }, { cmd: ["node", "dist/index.js"] }]
140
+ Where two Dockerfiles repeat a stage or a run of steps, export it once as a const (a \`DockerfileStage\` or a \`DockerfileStep[]\`) from its own module and import it in both. Spread imported consts, not function results; a Dockerfile that needs parameters is a \`Composite\`.
141
+ Run \`chant build src --lexicon docker -o build/docker-compose.yml\` (Dockerfiles are written beside it) and compare with the original files, then \`chant lint src\`. Run \`chant lint src --fix\` for DKRS002 (per-kind fields to \`steps\`, then reorder the steps it writes), fix every other finding by hand, and repeat until lint is clean.
142
+ Report the generated docker-compose.yml and each Dockerfile.<name>, and any finding you left and why.
143
+ Do not push an image, run \`docker compose up\` against anything but the local Docker daemon, or change files outside this project.
144
+ Never deploy to a real cloud account or shared environment (with \`chant run\`, \`chant components\` or a platform's own CLI), approve (a pull request review, \`chant approve\` or \`chant run approve\`), override a policy denial (\`chant approve policy-override\`), or merge; never cut or publish a release (\`chant release cut --yes\` or \`chant release publish\`), push or delete a \`ci/*\` tag (\`chant ci tick\` pushes them), or change the value of an existing flag; never touch \`.chant/allowed_signers\` or \`chant/lifecycle\`.`;
145
+ export async function generateDocs(opts) {
146
+ const pkgDir = dirname(dirname(dirname(fileURLToPath(import.meta.url))));
147
+ const config = {
148
+ name: "docker",
149
+ displayName: "Docker",
150
+ description: "Typed constructors for Docker Compose and Dockerfile configuration",
151
+ distDir: join(pkgDir, "dist"),
152
+ outDir: join(pkgDir, "docs"),
153
+ basePath: "/chant/lexicons/docker/",
154
+ overview,
155
+ outputFormat,
156
+ prompt,
157
+ };
158
+ const result = docsPipeline(config);
159
+ writeDocsSite(config, result);
160
+ if (opts?.verbose) {
161
+ console.error("Documentation generated");
162
+ }
163
+ }
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * CLI entry point for Docker lexicon generation.
4
+ */
5
+ import { dirname } from "path";
6
+ import { fileURLToPath } from "url";
7
+ import { generate, writeGeneratedFiles } from "./generate.js";
8
+ const pkgDir = dirname(dirname(dirname(fileURLToPath(import.meta.url))));
9
+ async function main() {
10
+ const verbose = process.argv.includes("--verbose") || process.argv.includes("-v");
11
+ const force = process.argv.includes("--force") || process.argv.includes("-f");
12
+ console.error("Generating Docker lexicon...");
13
+ const result = await generate({ verbose, force });
14
+ writeGeneratedFiles(result, pkgDir);
15
+ console.error(`Generated ${result.resources} entities, ${result.properties} property types, ${result.enums} enums`);
16
+ if (result.warnings.length > 0) {
17
+ console.error(`${result.warnings.length} warnings:`);
18
+ for (const w of result.warnings) {
19
+ console.error(` ${w.file}: ${w.error}`);
20
+ }
21
+ }
22
+ }
23
+ main().catch((err) => {
24
+ console.error("Generation failed:", err);
25
+ process.exit(1);
26
+ });
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Generate Compose entity types from the Compose Spec JSON Schema.
3
+ */
4
+ import { fetchComposeSpec } from "../spec/fetch-compose.js";
5
+ import { parseComposeSpec } from "../spec/parse-compose.js";
6
+ export async function generateComposePipeline(opts = {}) {
7
+ if (opts.verbose)
8
+ console.error("Fetching Compose Spec...");
9
+ const data = await fetchComposeSpec(opts.force);
10
+ if (opts.verbose)
11
+ console.error("Parsing Compose Spec...");
12
+ const results = parseComposeSpec(data);
13
+ if (opts.verbose)
14
+ console.error(`Parsed ${results.length} Compose entity types`);
15
+ return { results };
16
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Generate Dockerfile entity types from the Docker Engine API OpenAPI spec.
3
+ */
4
+ import { fetchEngineApi } from "../spec/fetch-engine.js";
5
+ import { parseEngineApi } from "../spec/parse-engine.js";
6
+ export async function generateDockerfilePipeline(opts = {}) {
7
+ if (opts.verbose)
8
+ console.error("Fetching Docker Engine API...");
9
+ const data = await fetchEngineApi(opts.force);
10
+ if (opts.verbose)
11
+ console.error("Parsing Engine API for Dockerfile types...");
12
+ const result = parseEngineApi(data);
13
+ if (opts.verbose)
14
+ console.error(`Parsed ${result.instructions.length} Dockerfile instructions`);
15
+ return { result };
16
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"generate.d.ts","sourceRoot":"","sources":["../../src/codegen/generate.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAWH,MAAM,WAAW,oBAAoB;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACjD,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;GAEG;AACH,wBAAsB,QAAQ,CAAC,IAAI,GAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAO,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAwB/G;AA2LD;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,oBAAoB,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAWvF"}
1
+ {"version":3,"file":"generate.d.ts","sourceRoot":"","sources":["../../src/codegen/generate.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAWH,MAAM,WAAW,oBAAoB;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACjD,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;GAEG;AACH,wBAAsB,QAAQ,CAAC,IAAI,GAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAO,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAwB/G;AA8LD;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,oBAAoB,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAWvF"}
@@ -0,0 +1,212 @@
1
+ /**
2
+ * Docker lexicon generation pipeline.
3
+ *
4
+ * Orchestrates both Compose and Dockerfile type generation,
5
+ * then writes merged artifacts to src/generated/.
6
+ */
7
+ import { writeFileSync, mkdirSync } from "fs";
8
+ import { join } from "path";
9
+ import { generateComposePipeline } from "./generate-compose.js";
10
+ import { generateDockerfilePipeline } from "./generate-dockerfile.js";
11
+ import { entityTypeToTsName } from "./naming.js";
12
+ import { PER_KIND_ORDER } from "../dockerfile-order.js";
13
+ import { COMPOSE_STRING } from "../spec/parse-compose.js";
14
+ /**
15
+ * Run the full Docker generation pipeline.
16
+ */
17
+ export async function generate(opts = {}) {
18
+ const [composeResult, dockerfileResult] = await Promise.all([
19
+ generateComposePipeline(opts),
20
+ generateDockerfilePipeline(opts),
21
+ ]);
22
+ const allCompose = composeResult.results;
23
+ const dockerfile = dockerfileResult.result;
24
+ const lexiconJSON = generateLexiconJSON(allCompose, dockerfile);
25
+ const typesDTS = generateTypesDTS(allCompose, dockerfile);
26
+ const indexTS = generateIndexTS(allCompose, dockerfile);
27
+ const propertyCount = allCompose.reduce((n, c) => n + c.properties.length, 0) + dockerfile.instructions.length;
28
+ return {
29
+ resources: allCompose.length + 1, // +1 for Dockerfile
30
+ properties: propertyCount,
31
+ enums: 0,
32
+ warnings: [],
33
+ lexiconJSON,
34
+ typesDTS,
35
+ indexTS,
36
+ };
37
+ }
38
+ function generateLexiconJSON(compose, dockerfile) {
39
+ const entries = {};
40
+ for (const c of compose) {
41
+ const tsName = entityTypeToTsName(c.typeName);
42
+ entries[tsName] = {
43
+ resourceType: c.typeName,
44
+ kind: "resource",
45
+ description: c.description ?? "",
46
+ properties: Object.fromEntries(c.properties.map((p) => [p.name, { type: p.type, description: p.description ?? "" }])),
47
+ };
48
+ }
49
+ const dfName = entityTypeToTsName(dockerfile.typeName);
50
+ entries[dfName] = {
51
+ resourceType: dockerfile.typeName,
52
+ kind: "resource",
53
+ description: dockerfile.description ?? "",
54
+ properties: {
55
+ ...Object.fromEntries(dockerfile.instructions.filter((i) => !i.stepOnly).map((i) => [i.name, { description: i.description ?? "" }])),
56
+ globalArgs: { description: "ARG instructions written before the first FROM" },
57
+ steps: { description: "Instructions after FROM, written in the order given; one instruction per step" },
58
+ },
59
+ };
60
+ return JSON.stringify(entries, null, 2);
61
+ }
62
+ function generateTypesDTS(compose, dockerfile) {
63
+ const lines = [
64
+ "// Code generated by chant generate. DO NOT EDIT.",
65
+ 'import type { Declarable } from "@intentius/chant/declarable";',
66
+ "",
67
+ // This file is type-checked on its own (validate's types-compile), so it
68
+ // can't import the interpolation module; an env() value is matched by shape.
69
+ "/** An `env()` interpolation, as returned by the lexicon's `env`. */",
70
+ "interface EnvIntrinsic { toJSON(): string; toString(): string; }",
71
+ "",
72
+ ];
73
+ lines.push(...composeTypeLines(compose));
74
+ for (const c of compose) {
75
+ const tsName = entityTypeToTsName(c.typeName);
76
+ lines.push(`export declare const ${tsName}: new (props?: ${tsName}Props, attributes?: Record<string, unknown>) => Declarable;`);
77
+ }
78
+ lines.push("");
79
+ const dfName = entityTypeToTsName(dockerfile.typeName);
80
+ lines.push(...dockerfileTypeLines(dockerfile));
81
+ lines.push(`export declare const ${dfName}: new (props?: DockerfileProps, attributes?: Record<string, unknown>) => Declarable;`);
82
+ lines.push("");
83
+ return lines.join("\n");
84
+ }
85
+ function generateIndexTS(compose, dockerfile) {
86
+ const lines = [
87
+ "// Code generated by chant generate. DO NOT EDIT.",
88
+ 'import { createResource } from "./runtime";',
89
+ 'import type { Declarable } from "@intentius/chant/declarable";',
90
+ 'import type { EnvIntrinsic } from "../interpolation.js";',
91
+ "",
92
+ // createResource returns a constructor with untyped props. Each constructor
93
+ // below is cast to one that takes its props type, so a misspelled key or a
94
+ // wrong field type is a compile error where the resource is written, and
95
+ // the editor completes the fields (#3934).
96
+ "/** A docker resource constructor whose props are type-checked. */",
97
+ "export type DockerResourceConstructor<P> = new (props?: P, attributes?: Record<string, unknown>) => InstanceType<ReturnType<typeof createResource>>;",
98
+ "",
99
+ ];
100
+ lines.push(...composeTypeLines(compose));
101
+ for (const c of compose) {
102
+ const tsName = entityTypeToTsName(c.typeName);
103
+ lines.push(`export const ${tsName} = createResource("${c.typeName}", "docker", {}) as unknown as DockerResourceConstructor<${tsName}Props>;`);
104
+ }
105
+ lines.push("");
106
+ // The Dockerfile prop types are exported from here too (not only from
107
+ // index.d.ts, which index.ts shadows) so a module can type a shared stage
108
+ // or step list: `export const install: DockerfileStep[] = [...]`.
109
+ lines.push(...dockerfileTypeLines(dockerfile));
110
+ lines.push("");
111
+ const dfName = entityTypeToTsName(dockerfile.typeName);
112
+ lines.push(`export const ${dfName} = createResource("${dockerfile.typeName}", "docker", {}) as unknown as DockerResourceConstructor<DockerfileProps>;`);
113
+ lines.push("");
114
+ return lines.join("\n");
115
+ }
116
+ /** One props interface per Compose entity. */
117
+ function composeTypeLines(compose) {
118
+ const lines = [
119
+ "/**",
120
+ " * A Compose string value: a string, an `env()` interpolation, or an entity",
121
+ " * (service, network, volume, config, secret or Dockerfile), which is written as its name.",
122
+ " */",
123
+ `export type ${COMPOSE_STRING} = string | EnvIntrinsic | Declarable;`,
124
+ "",
125
+ ];
126
+ for (const c of compose) {
127
+ const tsName = entityTypeToTsName(c.typeName);
128
+ if (c.description)
129
+ lines.push(`/** ${c.description} */`);
130
+ lines.push(`export interface ${tsName}Props {`);
131
+ for (const p of c.properties) {
132
+ const opt = p.required ? "" : "?";
133
+ if (p.description)
134
+ lines.push(` /** ${p.description} */`);
135
+ lines.push(` ${p.name}${opt}: ${p.type};`);
136
+ }
137
+ // The Compose spec allows `x-` extension fields on every entity, and import keeps them (#4136).
138
+ lines.push(` /** A Compose extension field. The spec allows any \`x-\` key, and Compose itself ignores it. */`);
139
+ lines.push(" [extension: `x-${string}`]: unknown;");
140
+ lines.push(`}`);
141
+ lines.push("");
142
+ }
143
+ return lines;
144
+ }
145
+ /**
146
+ * The Dockerfile prop types: one `DockerfileStep` variant per instruction,
147
+ * the stage, and the props. The per-kind fields keep the instructions they
148
+ * always had; SHELL, STOPSIGNAL and ONBUILD are steps only.
149
+ */
150
+ function dockerfileTypeLines(dockerfile) {
151
+ const lines = [];
152
+ const stepInstructions = dockerfile.instructions.filter((i) => i.name !== "from");
153
+ const perKind = stepInstructions.filter((i) => !i.stepOnly);
154
+ const stepValue = (i) => ["string", ...(i.numeric ? ["number"] : []), ...(i.execForm ? ["string[]"] : [])].join(" | ");
155
+ const fixed = PER_KIND_ORDER.map((k) => k.toUpperCase()).join(", ");
156
+ lines.push(`/** The Dockerfile instructions a step can name. FROM is the stage's own \`from\` field. */`);
157
+ lines.push(`export type DockerfileInstructionName = ${stepInstructions.map((i) => JSON.stringify(i.name)).join(" | ")};`);
158
+ lines.push(`type DockerfileStepOf<K extends DockerfileInstructionName, V> = { [P in K]: V } & { [P in Exclude<DockerfileInstructionName, K>]?: never };`);
159
+ lines.push(`/**`);
160
+ lines.push(` * One Dockerfile instruction, named by its single key: \`{ run: "npm ci" }\` is \`RUN npm ci\`.`);
161
+ lines.push(` * A string[] value (run, cmd, entrypoint, shell) is written in JSON exec form.`);
162
+ lines.push(` */`);
163
+ lines.push(`export type DockerfileStep =`);
164
+ for (const i of stepInstructions) {
165
+ lines.push(` /** ${i.name.toUpperCase()}: ${i.description ?? ""} */`);
166
+ lines.push(` | DockerfileStepOf<${JSON.stringify(i.name)}, ${stepValue(i)}>`);
167
+ }
168
+ lines[lines.length - 1] += ";";
169
+ const fields = () => {
170
+ lines.push(` /** Instructions after FROM, written exactly in this order. Can't be combined with the per-kind fields. */`);
171
+ lines.push(` steps?: DockerfileStep[];`);
172
+ for (const i of perKind) {
173
+ lines.push(` /** ${i.name.toUpperCase()} as a per-kind field, written in the fixed order. Use \`steps\` to choose the order. */`);
174
+ lines.push(` ${i.name}?: ${i.multi ? "string[]" : "string"};`);
175
+ }
176
+ };
177
+ const fixedDoc = ` * Without \`steps\`, the per-kind fields are written in a fixed order: ${fixed}.`;
178
+ lines.push(`/**`);
179
+ lines.push(` * One build stage: FROM, then its instructions.`);
180
+ lines.push(fixedDoc);
181
+ lines.push(` */`);
182
+ lines.push(`export interface DockerfileStage {`);
183
+ lines.push(` from: string;`);
184
+ lines.push(` as?: string;`);
185
+ fields();
186
+ lines.push(`}`);
187
+ lines.push(`/**`);
188
+ lines.push(` * A Dockerfile: \`stages\` for a multi-stage build, or \`from\` and the stage fields for one stage.`);
189
+ lines.push(fixedDoc);
190
+ lines.push(` */`);
191
+ lines.push(`export interface DockerfileProps {`);
192
+ lines.push(` name?: string;`);
193
+ lines.push(` /** ARG instructions written before the first FROM, e.g. \`["NODE_VERSION=20"]\`. */`);
194
+ lines.push(` globalArgs?: string[];`);
195
+ lines.push(` from?: string;`);
196
+ lines.push(` as?: string;`);
197
+ lines.push(` stages?: DockerfileStage[];`);
198
+ fields();
199
+ lines.push(`}`);
200
+ return lines;
201
+ }
202
+ /**
203
+ * Write generated artifacts to disk under baseDir/src/generated/.
204
+ */
205
+ export function writeGeneratedFiles(result, baseDir) {
206
+ const generatedDir = join(baseDir, "src", "generated");
207
+ mkdirSync(generatedDir, { recursive: true });
208
+ writeFileSync(join(generatedDir, "lexicon-docker.json"), result.lexiconJSON);
209
+ writeFileSync(join(generatedDir, "index.d.ts"), result.typesDTS);
210
+ writeFileSync(join(generatedDir, "index.ts"), result.indexTS);
211
+ writeFileSync(join(generatedDir, "runtime.ts"), `/**\n * Runtime factory constructors — re-exported from core.\n */\nexport { createResource, createProperty } from "@intentius/chant/runtime";\n`);
212
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Naming strategy for Docker lexicon entities.
3
+ *
4
+ * Converts Docker/Compose entity type names to TypeScript class names.
5
+ */
6
+ /** Map entityType → TypeScript class name */
7
+ const ENTITY_TYPE_TO_TS_NAME = {
8
+ "Docker::Compose::Service": "Service",
9
+ "Docker::Compose::Volume": "Volume",
10
+ "Docker::Compose::Network": "Network",
11
+ "Docker::Compose::Config": "DockerConfig",
12
+ "Docker::Compose::Secret": "DockerSecret",
13
+ "Docker::Dockerfile": "Dockerfile",
14
+ };
15
+ /** Map TypeScript class name → entityType */
16
+ const TS_NAME_TO_ENTITY_TYPE = Object.fromEntries(Object.entries(ENTITY_TYPE_TO_TS_NAME).map(([k, v]) => [v, k]));
17
+ /**
18
+ * Convert a Docker entity type name to a TypeScript class name.
19
+ * e.g. "Docker::Compose::Service" → "Service"
20
+ */
21
+ export function entityTypeToTsName(entityType) {
22
+ return ENTITY_TYPE_TO_TS_NAME[entityType] ?? entityType.split("::").pop() ?? entityType;
23
+ }
24
+ /**
25
+ * Convert a TypeScript class name back to an entity type.
26
+ */
27
+ export function tsNameToEntityType(tsName) {
28
+ return TS_NAME_TO_ENTITY_TYPE[tsName];
29
+ }
30
+ /**
31
+ * All known Docker entity types, in declaration order.
32
+ */
33
+ export const ALL_ENTITY_TYPES = Object.keys(ENTITY_TYPE_TO_TS_NAME);
34
+ /**
35
+ * Check whether an entity type is a Compose resource.
36
+ */
37
+ export function isComposeType(entityType) {
38
+ return entityType.startsWith("Docker::Compose::");
39
+ }
40
+ /**
41
+ * Check whether an entity type is the Dockerfile resource.
42
+ */
43
+ export function isDockerfileType(entityType) {
44
+ return entityType === "Docker::Dockerfile";
45
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Docker lexicon packaging — delegates to core packagePipeline.
3
+ */
4
+ import { readFileSync } from "fs";
5
+ import { dockerPlugin } from "../plugin.js";
6
+ import { join, dirname } from "path";
7
+ import { moduleSourceDir } from "@intentius/chant/module-source";
8
+ import { packagePipeline, collectSkills, } from "@intentius/chant/codegen/package";
9
+ import { generate } from "./generate.js";
10
+ const pkgDir = dirname(moduleSourceDir(import.meta.url));
11
+ /**
12
+ * Package the Docker lexicon into a distributable BundleSpec.
13
+ */
14
+ export async function packageLexicon(opts = {}) {
15
+ const pkgJson = JSON.parse(readFileSync(join(pkgDir, "..", "package.json"), "utf-8"));
16
+ return packagePipeline({
17
+ generate: (genOpts) => generate({ verbose: genOpts.verbose, force: genOpts.force }),
18
+ buildManifest: (_genResult) => {
19
+ // Derived from the plugin's own registration (chant #1067) — this
20
+ // used to be a hand-maintained array independent of ../plugin.ts,
21
+ // free to drift from the real registration without anything
22
+ // noticing.
23
+ const intrinsics = dockerPlugin.intrinsics?.() ?? [];
24
+ return {
25
+ name: "docker",
26
+ version: pkgJson.version ?? "0.0.0",
27
+ chantVersion: ">=0.1.0",
28
+ namespace: "Docker",
29
+ intrinsics,
30
+ pseudoParameters: {},
31
+ };
32
+ },
33
+ srcDir: pkgDir,
34
+ collectSkills: () => {
35
+ const skillDefs = dockerPlugin.skills?.() ?? [];
36
+ return collectSkills(skillDefs);
37
+ },
38
+ version: pkgJson.version ?? "0.0.0",
39
+ }, opts);
40
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Manual overrides for upstream Docker spec quirks.
3
+ *
4
+ * These patches adjust entity names, property types, or descriptions
5
+ * where the upstream spec is ambiguous, incorrect, or overly broad.
6
+ */
7
+ /**
8
+ * Patches keyed by entity typeName.
9
+ */
10
+ export const ENTITY_PATCHES = {
11
+ "Docker::Compose::Service": {
12
+ propertyOverrides: {
13
+ // The spec allows both string and object forms for these — normalize to object
14
+ depends_on: {
15
+ description: "Service dependencies (may be string[] or condition map)",
16
+ },
17
+ // healthcheck interval/timeout are duration strings in Docker syntax
18
+ healthcheck: {
19
+ description: "Health check configuration with Docker duration strings (e.g., '30s', '1m30s')",
20
+ },
21
+ },
22
+ },
23
+ "Docker::Dockerfile": {
24
+ propertyOverrides: {
25
+ // Multi-stage builds use stages array
26
+ stages: {
27
+ description: "Multi-stage build stages — each stage has its own FROM and instructions",
28
+ },
29
+ },
30
+ },
31
+ };
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Pinned URLs and version tags for upstream Docker specs.
3
+ */
4
+ /**
5
+ * Pinned commit of compose-spec/compose-spec (#1146).
6
+ *
7
+ * compose-spec/compose-spec has no tagged releases, so unlike
8
+ * ENGINE_API_URL below (pinned to the `v27.3.1` tag) there is no version tag
9
+ * to pin against — codegen used to fetch the default branch directly (the
10
+ * repo's default branch is `main`; the old URL fetched `master`, a legacy
11
+ * alias GitHub still redirects, but a moving-branch alias all the same).
12
+ * That meant a cold cache picked up whatever upstream state had landed since
13
+ * the cache last expired, while a warm cache kept serving whatever was
14
+ * current when it was last filled — the exact CI nondeterminism this fix
15
+ * closed for azure (#1144).
16
+ *
17
+ * Pinning to a commit SHA makes generation reproducible regardless of cache
18
+ * state. Bump policy: #523's scheduled lexicon-upgrade cron (rolling-spec
19
+ * bucket) is meant to propose bumps to this constant as a reviewable PR once
20
+ * built. Until then, bump by hand: take the current `main` HEAD sha from
21
+ * https://github.com/compose-spec/compose-spec/commits/main, regenerate, fix
22
+ * up any composite left referencing a renamed or vanished export, and update
23
+ * this constant + comment.
24
+ */
25
+ export const COMPOSE_SPEC_COMMIT = "11296e387ba76c77db1db768b9153a4304a3c9bd";
26
+ /** Compose Spec JSON Schema — pinned to COMPOSE_SPEC_COMMIT, not a moving branch */
27
+ export const COMPOSE_SPEC_URL = `https://raw.githubusercontent.com/compose-spec/compose-spec/${COMPOSE_SPEC_COMMIT}/schema/compose-spec.json`;
28
+ /** Docker Engine API OpenAPI spec (v1.45) */
29
+ export const ENGINE_API_URL = "https://raw.githubusercontent.com/moby/moby/v27.3.1/api/swagger.yaml";
30
+ /**
31
+ * Cache filenames under ~/.chant/.
32
+ *
33
+ * COMPOSE_SPEC_CACHE embeds the pin so a stale schema file fetched before a
34
+ * bump can never satisfy the new pin: restoring an old ~/.chant (e.g. a CI
35
+ * cache partial-key fallback, or a developer's pre-existing cache) simply
36
+ * misses this filename and re-downloads, instead of silently serving
37
+ * pre-bump schema under a name that still matches (#1146, mirroring azure's
38
+ * AZURE_SCHEMA_COMMIT-keyed cache filename, #1144).
39
+ */
40
+ export const COMPOSE_SPEC_CACHE = `docker-compose-spec-${COMPOSE_SPEC_COMMIT}.json`;
41
+ export const ENGINE_API_CACHE = "docker-engine-api.yaml";
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The key an entity takes in docker-compose.yml.
3
+ *
4
+ * A compose entity's key is its export name by default. A key that is not a
5
+ * TypeScript identifier (`db-password`, `nginx.conf`) cannot be an export
6
+ * name, so `composeKey` sets it explicitly (#3937):
7
+ *
8
+ * @example
9
+ * export const dbPassword = composeKey("db-password", new DockerSecret({ file: "./db-password.txt" }));
10
+ *
11
+ * References to the entity (`secrets: [dbPassword]`) serialize as the key.
12
+ */
13
+ import type { Declarable } from "@intentius/chant/declarable";
14
+ export declare const COMPOSE_KEY: unique symbol;
15
+ /** Give a compose entity an explicit key in docker-compose.yml. Returns the entity. */
16
+ export declare function composeKey<T extends Declarable>(key: string, entity: T): T;
17
+ /** The key set by `composeKey`, or undefined. */
18
+ export declare function getComposeKey(entity: Declarable): string | undefined;
19
+ //# sourceMappingURL=compose-key.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compose-key.d.ts","sourceRoot":"","sources":["../src/compose-key.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAE9D,eAAO,MAAM,WAAW,eAAkC,CAAC;AAE3D,uFAAuF;AACvF,wBAAgB,UAAU,CAAC,CAAC,SAAS,UAAU,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,GAAG,CAAC,CAM1E;AAED,iDAAiD;AACjD,wBAAgB,aAAa,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,GAAG,SAAS,CAGpE"}
@@ -0,0 +1,26 @@
1
+ /**
2
+ * The key an entity takes in docker-compose.yml.
3
+ *
4
+ * A compose entity's key is its export name by default. A key that is not a
5
+ * TypeScript identifier (`db-password`, `nginx.conf`) cannot be an export
6
+ * name, so `composeKey` sets it explicitly (#3937):
7
+ *
8
+ * @example
9
+ * export const dbPassword = composeKey("db-password", new DockerSecret({ file: "./db-password.txt" }));
10
+ *
11
+ * References to the entity (`secrets: [dbPassword]`) serialize as the key.
12
+ */
13
+ export const COMPOSE_KEY = Symbol.for("docker.composeKey");
14
+ /** Give a compose entity an explicit key in docker-compose.yml. Returns the entity. */
15
+ export function composeKey(key, entity) {
16
+ if (typeof key !== "string" || key === "") {
17
+ throw new Error("composeKey: the key must be a non-empty string");
18
+ }
19
+ Object.defineProperty(entity, COMPOSE_KEY, { value: key, enumerable: false, configurable: true });
20
+ return entity;
21
+ }
22
+ /** The key set by `composeKey`, or undefined. */
23
+ export function getComposeKey(entity) {
24
+ const key = entity[COMPOSE_KEY];
25
+ return typeof key === "string" ? key : undefined;
26
+ }