@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,159 @@
1
+ /**
2
+ * Docker IR → TypeScript source generator.
3
+ *
4
+ * Converts parsed IR to valid chant TypeScript source code.
5
+ */
6
+ /**
7
+ * Generator for Docker entities — IR → TypeScript chant source.
8
+ */
9
+ export class DockerGenerator {
10
+ generate(entities) {
11
+ const warnings = [];
12
+ const imports = new Set();
13
+ const lines = [];
14
+ const names = new ExportNames();
15
+ for (const entity of entities) {
16
+ if (entity.kind === "dockerfile") {
17
+ imports.add("Dockerfile");
18
+ lines.push(generateDockerfile(entity, warnings));
19
+ continue;
20
+ }
21
+ const ctor = CONSTRUCTORS[entity.kind];
22
+ imports.add(ctor);
23
+ const exportName = names.take(entity.name, entity.kind);
24
+ const expr = `new ${ctor}(${propsLiteral(entity)})`;
25
+ // An export name that differs from the compose key would rename the
26
+ // key on build, and every reference to it by name would dangle, so
27
+ // the key is set explicitly (#3937).
28
+ if (exportName === entity.name) {
29
+ lines.push(`export const ${exportName} = ${expr};`);
30
+ }
31
+ else {
32
+ imports.add("composeKey");
33
+ lines.push(`export const ${exportName} = composeKey(${JSON.stringify(entity.name)}, ${expr});`);
34
+ }
35
+ }
36
+ const importLine = `import { ${[...imports].sort().join(", ")} } from "@intentius/chant-lexicon-docker";`;
37
+ const source = [importLine, "", ...lines].join("\n");
38
+ return { source, warnings };
39
+ }
40
+ }
41
+ const CONSTRUCTORS = {
42
+ service: "Service",
43
+ volume: "Volume",
44
+ network: "Network",
45
+ config: "DockerConfig",
46
+ secret: "DockerSecret",
47
+ };
48
+ function propsLiteral(entity) {
49
+ if (entity.kind === "service") {
50
+ return JSON.stringify(entity.props, null, 2).replace(/"([a-z_][a-z0-9_]*)":/g, "$1:");
51
+ }
52
+ return Object.keys(entity.props).length > 0 ? JSON.stringify(entity.props) : "{}";
53
+ }
54
+ /** Words an export cannot be named, and the names the generated file imports. */
55
+ const RESERVED = new Set([
56
+ "break", "case", "catch", "class", "const", "continue", "debugger", "default", "delete", "do",
57
+ "else", "enum", "export", "extends", "false", "finally", "for", "function", "if", "import", "in",
58
+ "instanceof", "new", "null", "return", "super", "switch", "this", "throw", "true", "try", "typeof",
59
+ "var", "void", "while", "with", "yield", "let", "static", "implements", "interface", "package",
60
+ "private", "protected", "public", "await", "arguments", "eval", "undefined", "NaN", "Infinity",
61
+ "Service", "Volume", "Network", "DockerConfig", "DockerSecret", "Dockerfile", "composeKey",
62
+ ]);
63
+ /**
64
+ * Export names for compose keys: identifiers, unique within the file. A key
65
+ * shared by two kinds (a service and a volume both named `db`) gives the
66
+ * second one a kind suffix (`dbVolume`).
67
+ */
68
+ class ExportNames {
69
+ constructor() {
70
+ this.used = new Set();
71
+ }
72
+ take(key, kind) {
73
+ const base = sanitizeName(key);
74
+ const suffixed = `${base}${kind[0].toUpperCase()}${kind.slice(1)}`;
75
+ for (const c of [base, suffixed]) {
76
+ if (!RESERVED.has(c) && !this.used.has(c)) {
77
+ this.used.add(c);
78
+ return c;
79
+ }
80
+ }
81
+ for (let i = 2;; i++) {
82
+ const c = `${suffixed}${i}`;
83
+ if (!this.used.has(c)) {
84
+ this.used.add(c);
85
+ return c;
86
+ }
87
+ }
88
+ }
89
+ }
90
+ /** The instructions a step can name; anything else (MAINTAINER) is left as a comment. */
91
+ const STEP_KEYS = new Set([
92
+ "arg", "env", "workdir", "user", "run", "copy", "add", "expose", "volume",
93
+ "label", "entrypoint", "cmd", "healthcheck", "shell", "stopsignal", "onbuild",
94
+ ]);
95
+ /**
96
+ * A Dockerfile always imports as `steps`, one per instruction in source
97
+ * order, so building the import gives the same instructions in the same
98
+ * order (the per-kind fields would reorder them).
99
+ */
100
+ function generateDockerfile(df, warnings) {
101
+ const out = [`export const ${sanitizeName(df.name)} = new Dockerfile({`];
102
+ if (df.globalArgs?.length)
103
+ out.push(` globalArgs: [${df.globalArgs.map((a) => JSON.stringify(a)).join(", ")}],`);
104
+ const steps = (stage, indent) => {
105
+ out.push(`${indent}steps: [`);
106
+ for (const { instruction, value } of stage.instructions) {
107
+ const key = instruction.toLowerCase();
108
+ if (STEP_KEYS.has(key)) {
109
+ out.push(`${indent} { ${key}: ${quote(value)} },`);
110
+ }
111
+ else {
112
+ warnings.push(`Dockerfile ${df.name}: ${instruction} has no step and was left as a comment`);
113
+ out.push(`${indent} // ${instruction} ${value.replace(/\n/g, " ")}`);
114
+ }
115
+ }
116
+ out.push(`${indent}],`);
117
+ };
118
+ if (df.stages.length === 1) {
119
+ const stage = df.stages[0];
120
+ out.push(` from: ${JSON.stringify(stage.from)},`);
121
+ if (stage.as)
122
+ out.push(` as: ${JSON.stringify(stage.as)},`);
123
+ steps(stage, " ");
124
+ }
125
+ else {
126
+ out.push(` stages: [`);
127
+ for (const stage of df.stages) {
128
+ out.push(` {`);
129
+ out.push(` from: ${JSON.stringify(stage.from)},`);
130
+ if (stage.as)
131
+ out.push(` as: ${JSON.stringify(stage.as)},`);
132
+ steps(stage, " ");
133
+ out.push(` },`);
134
+ }
135
+ out.push(` ],`);
136
+ }
137
+ out.push(`});`);
138
+ return out.join("\n");
139
+ }
140
+ /** A string literal, single-quoted when that avoids escaping double quotes (exec form: `'["node", "a.js"]'`). */
141
+ function quote(value) {
142
+ if (value.includes('"') && !/['\\\n]/.test(value))
143
+ return `'${value}'`;
144
+ return JSON.stringify(value);
145
+ }
146
+ /**
147
+ * A TypeScript identifier for a compose key: kebab, snake and dotted keys
148
+ * become camelCase, other characters are dropped, and a leading digit gets
149
+ * an underscore.
150
+ */
151
+ function sanitizeName(name) {
152
+ let id = name.replace(/[^A-Za-z0-9$]+([A-Za-z0-9])/g, (_, c) => c.toUpperCase());
153
+ id = id.replace(/[^A-Za-z0-9_$]/g, "");
154
+ if (id === "")
155
+ id = "entity";
156
+ if (/^[0-9]/.test(id))
157
+ id = `_${id}`;
158
+ return id;
159
+ }
@@ -52,6 +52,36 @@ export interface ParseResult {
52
52
  entities: DockerIR[];
53
53
  warnings: string[];
54
54
  }
55
+ /** The compose sections import reads, with the kind each holds and its entry in the lexicon. */
56
+ declare const SECTIONS: readonly [{
57
+ readonly section: "services";
58
+ readonly kind: "service";
59
+ readonly entry: "Service";
60
+ }, {
61
+ readonly section: "volumes";
62
+ readonly kind: "volume";
63
+ readonly entry: "Volume";
64
+ }, {
65
+ readonly section: "networks";
66
+ readonly kind: "network";
67
+ readonly entry: "Network";
68
+ }, {
69
+ readonly section: "configs";
70
+ readonly kind: "config";
71
+ readonly entry: "DockerConfig";
72
+ }, {
73
+ readonly section: "secrets";
74
+ readonly kind: "secret";
75
+ readonly entry: "DockerSecret";
76
+ }];
77
+ type ComposeKind = (typeof SECTIONS)[number]["kind"];
78
+ /**
79
+ * The fields the Compose spec defines for each kind, read from the generated
80
+ * lexicon (`lexicon-docker.json`, or `dist/meta.json` in a package built
81
+ * without it). The props types come from the same spec (#3934), so import
82
+ * keeps exactly what the types accept (#3937).
83
+ */
84
+ export declare function composeFields(): Map<ComposeKind, Set<string>>;
55
85
  /**
56
86
  * Parser for docker-compose.yml files.
57
87
  * Converts YAML to an IR consumable by the TypeScript generator.
@@ -81,4 +111,5 @@ export declare class DockerfileParser {
81
111
  }
82
112
  /** Whether content reads as a Dockerfile: its first instruction is FROM or ARG, or it starts with a parser directive. */
83
113
  export declare function looksLikeDockerfile(content: string): boolean;
114
+ export {};
84
115
  //# sourceMappingURL=parser.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../../src/import/parser.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,SAAS,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC;AAED,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,QAAQ,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC;AAED,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,SAAS,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC;AAED,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,QAAQ,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC;AAED,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,QAAQ,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ;;;;OAIG;IACH,YAAY,EAAE,KAAK,CAAC;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAC7D;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,YAAY,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,iDAAiD;IACjD,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,MAAM,EAAE,eAAe,EAAE,CAAC;CAC3B;AAED,MAAM,MAAM,QAAQ,GAAG,SAAS,GAAG,QAAQ,GAAG,SAAS,GAAG,QAAQ,GAAG,QAAQ,GAAG,YAAY,CAAC;AAE7F,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,QAAQ,EAAE,CAAC;IACrB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AA0BD;;;GAGG;AACH,qBAAa,YAAY;IACvB,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,WAAW,CAoElC;CACF;AAED,0FAA0F;AAC1F,MAAM,WAAW,qBAAqB;IACpC,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;GAOG;AACH,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,MAAM,GAAG,qBAAqB,EAAE,CAuCpF;AAED;;GAEG;AACH,qBAAa,gBAAgB;IAC3B,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,YAAY,CAuBjD;CACF;AAED,yHAAyH;AACzH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAS5D"}
1
+ {"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../../src/import/parser.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Docker Compose + Dockerfile parser — converts existing YAML/Dockerfiles
3
+ * to an intermediate representation (IR) for TypeScript code generation.
4
+ */
5
+ import { existsSync, readFileSync } from "node:fs";
6
+ import { dirname, join } from "node:path";
7
+ import { fileURLToPath } from "node:url";
8
+ import { parseYAML } from "@intentius/chant/yaml";
9
+ /** The compose sections import reads, with the kind each holds and its entry in the lexicon. */
10
+ const SECTIONS = [
11
+ { section: "services", kind: "service", entry: "Service" },
12
+ { section: "volumes", kind: "volume", entry: "Volume" },
13
+ { section: "networks", kind: "network", entry: "Network" },
14
+ { section: "configs", kind: "config", entry: "DockerConfig" },
15
+ { section: "secrets", kind: "secret", entry: "DockerSecret" },
16
+ ];
17
+ let fieldsCache;
18
+ /**
19
+ * The fields the Compose spec defines for each kind, read from the generated
20
+ * lexicon (`lexicon-docker.json`, or `dist/meta.json` in a package built
21
+ * without it). The props types come from the same spec (#3934), so import
22
+ * keeps exactly what the types accept (#3937).
23
+ */
24
+ export function composeFields() {
25
+ if (fieldsCache)
26
+ return fieldsCache;
27
+ const here = dirname(fileURLToPath(import.meta.url));
28
+ const candidates = [join(here, "..", "generated", "lexicon-docker.json"), join(here, "..", "..", "dist", "meta.json")];
29
+ const path = candidates.find((c) => existsSync(c));
30
+ if (!path) {
31
+ throw new Error(`docker import: the generated lexicon is missing (tried ${candidates.join(", ")}); run npm run generate in lexicons/docker`);
32
+ }
33
+ const lexicon = JSON.parse(readFileSync(path, "utf-8"));
34
+ const fields = new Map();
35
+ for (const { kind, entry } of SECTIONS) {
36
+ fields.set(kind, new Set(Object.keys(lexicon[entry]?.properties ?? {})));
37
+ }
38
+ fieldsCache = fields;
39
+ return fields;
40
+ }
41
+ /**
42
+ * Keep every field the Compose spec defines for the kind, and `x-`
43
+ * extension fields. Anything else is dropped and named in `warnings`.
44
+ */
45
+ function extractProps(raw, allowed, where, kind, warnings) {
46
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
47
+ return {};
48
+ const props = {};
49
+ for (const [key, value] of Object.entries(raw)) {
50
+ if (allowed.has(key) || key.startsWith("x-"))
51
+ props[key] = value;
52
+ else
53
+ warnings.push(`${where}: "${key}" is not a Compose ${kind} field and was dropped`);
54
+ }
55
+ return props;
56
+ }
57
+ /**
58
+ * Parser for docker-compose.yml files.
59
+ * Converts YAML to an IR consumable by the TypeScript generator.
60
+ */
61
+ export class DockerParser {
62
+ parse(content) {
63
+ if (!content.trim())
64
+ return { entities: [], warnings: [] };
65
+ const entities = [];
66
+ const warnings = [];
67
+ const doc = parseYAML(content);
68
+ const fields = composeFields();
69
+ const known = new Set(SECTIONS.map((s) => s.section));
70
+ for (const key of Object.keys(doc ?? {})) {
71
+ if (!known.has(key))
72
+ warnings.push(`"${key}" at the top level is not imported`);
73
+ }
74
+ for (const { section, kind } of SECTIONS) {
75
+ const entries = doc?.[section];
76
+ if (!entries || typeof entries !== "object" || Array.isArray(entries))
77
+ continue;
78
+ for (const [name, raw] of Object.entries(entries)) {
79
+ entities.push({
80
+ kind,
81
+ name,
82
+ props: extractProps(raw, fields.get(kind), `${section}.${name}`, kind, warnings),
83
+ });
84
+ }
85
+ }
86
+ return { entities, warnings };
87
+ }
88
+ }
89
+ /**
90
+ * Split Dockerfile content into logical instructions, in order. Comment
91
+ * and blank lines are dropped (also inside a continued instruction, as
92
+ * Docker does). A line ending in a backslash continues onto the next line,
93
+ * and a heredoc (`RUN <<EOF` ... `EOF`) runs to its terminator; both are
94
+ * kept in the value exactly as written, so writing the value back gives
95
+ * the same instruction.
96
+ */
97
+ export function splitDockerfileInstructions(content) {
98
+ const out = [];
99
+ const lines = content.replace(/\r\n/g, "\n").split("\n");
100
+ let i = 0;
101
+ while (i < lines.length) {
102
+ const first = lines[i];
103
+ i++;
104
+ const trimmed = first.trim();
105
+ if (!trimmed || trimmed.startsWith("#"))
106
+ continue;
107
+ const parts = [first.trimStart()];
108
+ let last = first;
109
+ while (/\\\s*$/.test(last) && i < lines.length) {
110
+ const next = lines[i];
111
+ i++;
112
+ // Docker skips comment and empty lines inside a continued instruction.
113
+ if (!next.trim() || next.trim().startsWith("#"))
114
+ continue;
115
+ parts.push(next);
116
+ last = next;
117
+ }
118
+ // Heredocs: <<EOF, <<-EOF, <<"EOF", <<'EOF' (possibly several).
119
+ const joined = parts.join("\n");
120
+ const terminators = [...joined.matchAll(/<<(-?)(["']?)([A-Za-z_][A-Za-z0-9_]*)\2/g)].map((m) => ({ dash: m[1] === "-", word: m[3] }));
121
+ for (const t of terminators) {
122
+ while (i < lines.length) {
123
+ const body = lines[i];
124
+ i++;
125
+ parts.push(body);
126
+ if ((t.dash ? body.replace(/^\t+/, "") : body) === t.word)
127
+ break;
128
+ }
129
+ }
130
+ const text = parts.join("\n").trimEnd();
131
+ const match = text.match(/^([A-Za-z]+)(?:\s+([\s\S]*))?$/);
132
+ if (!match)
133
+ continue;
134
+ out.push({ instruction: match[1].toUpperCase(), value: (match[2] ?? "").trimStart() });
135
+ }
136
+ return out;
137
+ }
138
+ /**
139
+ * Parser for Dockerfile content.
140
+ */
141
+ export class DockerfileParser {
142
+ parse(name, content) {
143
+ const stages = [];
144
+ const globalArgs = [];
145
+ let current = null;
146
+ for (const { instruction, value } of splitDockerfileInstructions(content)) {
147
+ if (instruction === "FROM") {
148
+ // Parse "FROM [--platform=...] image AS stagename"
149
+ const asMatch = value.match(/^(.+?)\s+[Aa][Ss]\s+(\S+)$/);
150
+ if (asMatch) {
151
+ current = { from: asMatch[1].trim(), as: asMatch[2].trim(), instructions: [] };
152
+ }
153
+ else {
154
+ current = { from: value.trim(), instructions: [] };
155
+ }
156
+ stages.push(current);
157
+ }
158
+ else if (current) {
159
+ current.instructions.push({ instruction, value });
160
+ }
161
+ else if (instruction === "ARG") {
162
+ globalArgs.push(value);
163
+ }
164
+ }
165
+ return { kind: "dockerfile", name, ...(globalArgs.length > 0 ? { globalArgs } : {}), stages };
166
+ }
167
+ }
168
+ /** Whether content reads as a Dockerfile: its first instruction is FROM or ARG, or it starts with a parser directive. */
169
+ export function looksLikeDockerfile(content) {
170
+ for (const line of content.split("\n")) {
171
+ const t = line.trim();
172
+ if (!t)
173
+ continue;
174
+ if (/^#\s*(syntax|escape|check)\s*=/i.test(t))
175
+ return true;
176
+ if (t.startsWith("#"))
177
+ continue;
178
+ return /^(FROM|ARG)\s/i.test(t);
179
+ }
180
+ return false;
181
+ }
package/dist/index.d.ts CHANGED
@@ -7,6 +7,7 @@ export type { DockerVar, ComposeVar } from "./variables.js";
7
7
  export { defaultLabels, defaultAnnotations, isDefaultLabels, isDefaultAnnotations } from "./default-labels.js";
8
8
  export { DEFAULT_LABELS_MARKER, DEFAULT_ANNOTATIONS_MARKER } from "./default-labels.js";
9
9
  export type { DefaultLabels, DefaultAnnotations } from "./default-labels.js";
10
+ export { composeKey, getComposeKey, COMPOSE_KEY } from "./compose-key.js";
10
11
  export * from "./generated/index.js";
11
12
  export * from "./composites/index.js";
12
13
  export { generate, writeGeneratedFiles } from "./codegen/generate.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAGhD,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAGxC,OAAO,EAAE,GAAG,EAAE,MAAM,iBAAiB,CAAC;AACtC,YAAY,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAGhE,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AACxD,YAAY,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAGzD,OAAO,EAAE,aAAa,EAAE,kBAAkB,EAAE,eAAe,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAC5G,OAAO,EAAE,qBAAqB,EAAE,0BAA0B,EAAE,MAAM,kBAAkB,CAAC;AACrF,YAAY,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAG1E,cAAc,mBAAmB,CAAC;AAGlC,cAAc,oBAAoB,CAAC;AAGnC,OAAO,EAAE,QAAQ,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AACnE,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACnD,YAAY,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAGhD,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAGxC,OAAO,EAAE,GAAG,EAAE,MAAM,iBAAiB,CAAC;AACtC,YAAY,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAGhE,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AACxD,YAAY,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAGzD,OAAO,EAAE,aAAa,EAAE,kBAAkB,EAAE,eAAe,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAC5G,OAAO,EAAE,qBAAqB,EAAE,0BAA0B,EAAE,MAAM,kBAAkB,CAAC;AACrF,YAAY,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAG1E,OAAO,EAAE,UAAU,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAGvE,cAAc,mBAAmB,CAAC;AAGlC,cAAc,oBAAoB,CAAC;AAGnC,OAAO,EAAE,QAAQ,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AACnE,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACnD,YAAY,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,20 @@
1
+ // Serializer
2
+ export { dockerSerializer } from "./serializer.js";
3
+ // Plugin
4
+ export { dockerPlugin } from "./plugin.js";
5
+ // Interpolation intrinsic
6
+ export { env } from "./interpolation.js";
7
+ // Context variables
8
+ export { DOCKER_VARS, COMPOSE_VARS } from "./variables.js";
9
+ // Default labels
10
+ export { defaultLabels, defaultAnnotations, isDefaultLabels, isDefaultAnnotations } from "./default-labels.js";
11
+ export { DEFAULT_LABELS_MARKER, DEFAULT_ANNOTATIONS_MARKER } from "./default-labels.js";
12
+ // Compose keys that are not identifiers (#3937)
13
+ export { composeKey, getComposeKey, COMPOSE_KEY } from "./compose-key.js";
14
+ // Generated entities — populated by `npm run generate`
15
+ export * from "./generated/index.js";
16
+ // Composites
17
+ export * from "./composites/index.js";
18
+ // Codegen pipeline (for external tooling)
19
+ export { generate, writeGeneratedFiles } from "./codegen/generate.js";
20
+ export { packageLexicon } from "./codegen/package.js";
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "algorithm": "sha256",
3
3
  "artifacts": {
4
- "manifest.json": "714485e54afc638466db2fe9747fd3a766cc372ddcfce2851674c377935f1240",
4
+ "manifest.json": "b5756f0b60762a3250ef8b0fe3274c91f10d6aab9380727215ae02d21f2a4c93",
5
5
  "meta.json": "b566d5cb84a87836a861dc5840c229577e860c5427123192810b7839f764a9ee",
6
- "types/index.d.ts": "f34a417496b2c02f047b0adc0bf41bacf9a3b207ff15af17bbe664329bbfa448",
6
+ "types/index.d.ts": "256644d2bffa2845900cf86984cc7dde3d9e79da5994ab13a22b0b83edf7f62a",
7
7
  "rules/no-latest-tag.ts": "2281be0457c02e782fbafe015f65ff5fd2b008ef0f879226e9b548a3ea65399b",
8
8
  "rules/per-kind-order.ts": "50622c849e9e76e312c4ca7abd97858f74641e0d7f673d2de47b055f7f6b6c6b",
9
9
  "rules/add-url-checksum.ts": "b317a30103f65a9dc658cc858aeca797f96f4c34f76525c88e4618dcb4a5cd05",
@@ -24,5 +24,5 @@
24
24
  "skills/chant-docker.md": "d499e18525f2cfd7ecc5b5be4a4c52c6931ebdef8d70f872d53279e53bed6f7e",
25
25
  "skills/chant-docker-patterns.md": "655f437c833796e0d520bbe08740a4226d48fcec4912b0438672ed5a299897e7"
26
26
  },
27
- "composite": "df5bacee85a82bd437bfbd3eeb236744b1afd8fcefbefa086f3c099b2da6c057"
27
+ "composite": "9a5b409d8cf4f842421dcf59a3018b072a07d73d07b0f25b28729bdd3014409e"
28
28
  }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Docker Compose variable interpolation intrinsics.
3
+ *
4
+ * Docker Compose supports:
5
+ * ${VAR} — required variable (fails if unset)
6
+ * ${VAR:-default} — use default if VAR is unset or empty
7
+ * ${VAR:?error} — fail with error message if VAR is unset or empty
8
+ * ${VAR:+value} — use value if VAR is set
9
+ *
10
+ * @example
11
+ * import { env } from "@intentius/chant-lexicon-docker";
12
+ *
13
+ * export const api = new Service({
14
+ * image: env("APP_IMAGE", { default: "myapp:latest" }),
15
+ * environment: {
16
+ * DB_URL: env("DB_URL", { required: true }),
17
+ * },
18
+ * });
19
+ *
20
+ * // Serializes to:
21
+ * // services:
22
+ * // api:
23
+ * // image: ${APP_IMAGE:-myapp:latest}
24
+ * // environment:
25
+ * // DB_URL: ${DB_URL:?DB_URL is required}
26
+ */
27
+ import { INTRINSIC_MARKER } from "@intentius/chant/intrinsic";
28
+ /**
29
+ * Create a Docker Compose variable interpolation intrinsic.
30
+ *
31
+ * @param name - Environment variable name
32
+ * @param opts - Interpolation options
33
+ */
34
+ export function env(name, opts = {}) {
35
+ function interpolationString() {
36
+ if (opts.ifSet !== undefined) {
37
+ return `\${${name}:+${opts.ifSet}}`;
38
+ }
39
+ if (opts.default !== undefined) {
40
+ return `\${${name}:-${opts.default}}`;
41
+ }
42
+ if (opts.required || opts.errorMessage) {
43
+ const msg = opts.errorMessage ?? `${name} is required`;
44
+ return `\${${name}:?${msg}}`;
45
+ }
46
+ return `\${${name}}`;
47
+ }
48
+ const value = interpolationString();
49
+ return {
50
+ [INTRINSIC_MARKER]: true,
51
+ toJSON: () => value,
52
+ toString: () => value,
53
+ };
54
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * The docker lexicon's `chant audit` catalog — metadata for the DKRD*
3
+ * Dockerfile/Compose post-synth rules. Contributed via `dockerPlugin.auditCatalog()` (#687).
4
+ */
5
+ import { auditRule, SCORECARD_PINNED, applyLineage } from "@intentius/chant/audit/catalog";
6
+ import { dockerAuditLineage } from "./audit-lineage.js";
7
+ const DOCKER_SEC = { name: "Docker — Security best practices", url: "https://docs.docker.com/develop/security-best-practices/" };
8
+ export const dockerAuditCatalog = {
9
+ DKRD001: auditRule("DKRD001", "merge-worthy", "guidance", "Service uses :latest or untagged image", "Pin the image to an explicit version tag (ideally a digest).", { authority: [SCORECARD_PINNED] }),
10
+ DKRD002: auditRule("DKRD002", "report-only", "guidance", "Named volume declared but unused", "Remove the unused volume or mount it in a service.", { category: "best-practice" }),
11
+ DKRD003: auditRule("DKRD003", "merge-worthy", "guidance", "Service exposes SSH (port 22)", "Don't expose SSH from a container; use exec/ephemeral access instead.", { authority: [DOCKER_SEC] }),
12
+ DKRD010: auditRule("DKRD010", "report-only", "guidance", "apt-get install without --no-install-recommends", "Add --no-install-recommends to keep images small.", { category: "best-practice" }),
13
+ DKRD011: auditRule("DKRD011", "report-only", "guidance", "ADD used where COPY would do", "Prefer COPY unless fetching a URL or extracting an archive.", { category: "best-practice" }),
14
+ DKRD012: auditRule("DKRD012", "merge-worthy", "guidance", "Container runs as root: the final stage has no USER, or ends on USER root", "Add a non-root USER instruction to the final stage, after the root setup steps.", { authority: [DOCKER_SEC] }),
15
+ DKRD013: auditRule("DKRD013", "report-only", "guidance", "Whole build context copied before a dependency install", "Copy the dependency manifests, run the install, then copy the rest of the source.", { category: "efficiency" }),
16
+ DKRD014: auditRule("DKRD014", "report-only", "guidance", "Root-only RUN after a non-root USER", "Run package installs and user/ownership changes before switching USER.", { category: "correctness" }),
17
+ DKRD015: auditRule("DKRD015", "report-only", "guidance", "More than one CMD or ENTRYPOINT in a stage", "Keep one CMD and one ENTRYPOINT per stage; only the last takes effect.", { category: "correctness" }),
18
+ DKRD016: auditRule("DKRD016", "report-only", "guidance", "COPY --from names no earlier stage", "Name a stage defined earlier in the Dockerfile, or a tagged image.", { category: "correctness" }),
19
+ DKRD017: auditRule("DKRD017", "report-only", "guidance", "ARG before FROM is never used", "Use the ARG in a FROM line, declare it again in the stage that needs it, or remove it.", { category: "best-practice" }),
20
+ DKRD018: auditRule("DKRD018", "report-only", "guidance", "Relative WORKDIR", "Use an absolute WORKDIR path.", { category: "best-practice" }),
21
+ DKRD019: auditRule("DKRD019", "report-only", "guidance", "ADD of a remote URL without --checksum", "Add --checksum=sha256:<digest> to ADD of an http(s) URL.", { category: "security" }),
22
+ DKRD020: auditRule("DKRD020", "report-only", "guidance", "apt-get update in its own RUN", "Run apt-get update and apt-get install in the same RUN.", { category: "correctness" }),
23
+ };
24
+ // Prior art credits live beside the rules in ./audit-lineage.ts (see core audit/prior-art.ts).
25
+ applyLineage(dockerAuditCatalog, dockerAuditLineage);
@@ -0,0 +1,39 @@
1
+ export const dockerAuditLineage = {
2
+ DKRD001: [
3
+ { tool: "hadolint", rule: "DL3007", url: "https://github.com/hadolint/hadolint/wiki/DL3007", relation: "overlaps" },
4
+ { tool: "checkov", rule: "CKV_DOCKER_7", url: "https://www.checkov.io/5.Policy%20Index/dockerfile.html", relation: "overlaps" },
5
+ { tool: "kics", rule: "image_version_using_latest", url: "https://docs.kics.io/latest/queries/dockerfile-queries/f45ea400-6bbe-4501-9fc7-1c3d75c32067/", relation: "overlaps" },
6
+ ],
7
+ DKRD003: [
8
+ { tool: "checkov", rule: "CKV_DOCKER_1", url: "https://www.checkov.io/5.Policy%20Index/dockerfile.html", relation: "overlaps" },
9
+ { tool: "kics", rule: "Exposing Port 22 (SSH)", url: "https://docs.kics.io/latest/queries/dockerfile-queries/5907595b-5b6d-4142-b173-dbb0e73fbff8/", relation: "overlaps" },
10
+ ],
11
+ DKRD010: [
12
+ { tool: "hadolint", rule: "DL3015", url: "https://github.com/hadolint/hadolint/wiki/DL3015", relation: "equivalent" },
13
+ { tool: "kics", rule: "apt_get_not_avoiding_additional_packages", url: "https://docs.kics.io/latest/queries/dockerfile-queries/7384dfb2-fcd1-4fbf-91cd-6c44c318c33c/", relation: "equivalent" },
14
+ ],
15
+ DKRD011: [
16
+ { tool: "hadolint", rule: "DL3020", url: "https://github.com/hadolint/hadolint/wiki/DL3020", relation: "equivalent" },
17
+ { tool: "checkov", rule: "CKV_DOCKER_4", url: "https://www.checkov.io/5.Policy%20Index/dockerfile.html", relation: "overlaps" },
18
+ { tool: "dockle", rule: "CIS-DI-0009", url: "https://github.com/goodwithtech/dockle/blob/master/CHECKPOINT.md#cis-di-0009", relation: "overlaps" },
19
+ ],
20
+ DKRD012: [
21
+ { tool: "checkov", rule: "CKV_DOCKER_3", url: "https://www.checkov.io/5.Policy%20Index/dockerfile.html", relation: "equivalent" },
22
+ { tool: "kics", rule: "missing_user_instruction", url: "https://docs.kics.io/latest/queries/dockerfile-queries/fd54f200-402c-4333-a5a4-36ef6709af2f/", relation: "equivalent" },
23
+ { tool: "dockle", rule: "CIS-DI-0001", url: "https://github.com/goodwithtech/dockle/blob/master/CHECKPOINT.md#cis-di-0001", relation: "overlaps" },
24
+ { tool: "hadolint", rule: "DL3002", url: "https://github.com/hadolint/hadolint/wiki/DL3002", relation: "overlaps" },
25
+ ],
26
+ DKRD015: [
27
+ { tool: "hadolint", rule: "DL4003", url: "https://github.com/hadolint/hadolint/wiki/DL4003", relation: "overlaps" },
28
+ { tool: "hadolint", rule: "DL4004", url: "https://github.com/hadolint/hadolint/wiki/DL4004", relation: "overlaps" },
29
+ ],
30
+ DKRD016: [
31
+ { tool: "hadolint", rule: "DL3022", url: "https://github.com/hadolint/hadolint/wiki/DL3022", relation: "overlaps" },
32
+ ],
33
+ DKRD018: [
34
+ { tool: "hadolint", rule: "DL3000", url: "https://github.com/hadolint/hadolint/wiki/DL3000", relation: "overlaps" },
35
+ ],
36
+ DKRD020: [
37
+ { tool: "checkov", rule: "CKV_DOCKER_5", url: "https://www.checkov.io/5.Policy%20Index/dockerfile.html", relation: "overlaps" },
38
+ ],
39
+ };
@@ -0,0 +1,35 @@
1
+ /**
2
+ * DKRD019: ADD of a remote URL without --checksum
3
+ *
4
+ * ADD fetches an http(s) URL at build time and trusts whatever comes back.
5
+ * `ADD --checksum=sha256:...` makes the build fail when the content changes.
6
+ */
7
+ import { eachDockerfile, stageLabel } from "./docker-helpers.js";
8
+ export const dkrd019 = {
9
+ id: "DKRD019",
10
+ description: "ADD fetches a remote URL without --checksum",
11
+ check(ctx) {
12
+ const diagnostics = [];
13
+ for (const { fileName, parsed } of eachDockerfile(ctx.outputs)) {
14
+ for (const stage of parsed.stages) {
15
+ for (const instr of stage.instructions) {
16
+ if (instr.instruction !== "ADD")
17
+ continue;
18
+ const words = instr.value.split(/\s+/);
19
+ if (words.some((w) => w.startsWith("--checksum=")))
20
+ continue;
21
+ const url = words.find((w) => /^https?:\/\//i.test(w));
22
+ if (!url)
23
+ continue;
24
+ diagnostics.push({
25
+ checkId: "DKRD019",
26
+ severity: "warning",
27
+ message: `${fileName} ${stageLabel(stage)}: ADD ${url} has no --checksum, so the build accepts whatever the URL serves. Add --checksum=sha256:<digest>.`,
28
+ lexicon: "docker",
29
+ });
30
+ }
31
+ }
32
+ }
33
+ return diagnostics;
34
+ },
35
+ };
@@ -0,0 +1,34 @@
1
+ /**
2
+ * DKRD010: apt-get install without --no-install-recommends
3
+ *
4
+ * Detects RUN instructions with apt-get install missing
5
+ * --no-install-recommends, which leads to bloated images.
6
+ */
7
+ import { extractDockerfiles } from "./docker-helpers.js";
8
+ export const dkrd010 = {
9
+ id: "DKRD010",
10
+ description: "apt-get install without --no-install-recommends adds unnecessary packages",
11
+ check(ctx) {
12
+ const diagnostics = [];
13
+ for (const [_outputName, output] of ctx.outputs) {
14
+ const dockerfiles = extractDockerfiles(output);
15
+ for (const [fileName, content] of dockerfiles) {
16
+ const lines = content.split("\n");
17
+ for (let i = 0; i < lines.length; i++) {
18
+ const line = lines[i].trim();
19
+ if (/^RUN\s+/.test(line) &&
20
+ line.includes("apt-get install") &&
21
+ !line.includes("--no-install-recommends")) {
22
+ diagnostics.push({
23
+ checkId: "DKRD010",
24
+ severity: "warning",
25
+ message: `${fileName}: RUN apt-get install should use --no-install-recommends to reduce image size.`,
26
+ lexicon: "docker",
27
+ });
28
+ }
29
+ }
30
+ }
31
+ }
32
+ return diagnostics;
33
+ },
34
+ };