@intentius/chant-lexicon-docker 0.124.21 → 0.124.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/codegen/docs.d.ts.map +1 -1
- package/dist/codegen/generate.d.ts.map +1 -1
- package/dist/dockerfile-order.d.ts +13 -0
- package/dist/dockerfile-order.d.ts.map +1 -0
- package/dist/generated/index.d.ts +703 -6
- package/dist/generated/index.d.ts.map +1 -1
- package/dist/import/adapter.d.ts.map +1 -1
- package/dist/import/parser.d.ts +23 -0
- package/dist/import/parser.d.ts.map +1 -1
- package/dist/integrity.json +17 -8
- package/dist/lint/audit-catalog.d.ts.map +1 -1
- package/dist/lint/audit-lineage.d.ts.map +1 -1
- package/dist/lint/post-synth/add-url-checksum.d.ts +9 -0
- package/dist/lint/post-synth/add-url-checksum.d.ts.map +1 -0
- package/dist/lint/post-synth/apt-update-alone.d.ts +10 -0
- package/dist/lint/post-synth/apt-update-alone.d.ts.map +1 -0
- package/dist/lint/post-synth/cache-busting-copy.d.ts +11 -0
- package/dist/lint/post-synth/cache-busting-copy.d.ts.map +1 -0
- package/dist/lint/post-synth/copy-from-stage.d.ts +10 -0
- package/dist/lint/post-synth/copy-from-stage.d.ts.map +1 -0
- package/dist/lint/post-synth/docker-helpers.d.ts +35 -0
- package/dist/lint/post-synth/docker-helpers.d.ts.map +1 -1
- package/dist/lint/post-synth/index.d.ts.map +1 -1
- package/dist/lint/post-synth/no-root-user.d.ts +3 -2
- package/dist/lint/post-synth/no-root-user.d.ts.map +1 -1
- package/dist/lint/post-synth/relative-workdir.d.ts +10 -0
- package/dist/lint/post-synth/relative-workdir.d.ts.map +1 -0
- package/dist/lint/post-synth/repeated-cmd.d.ts +9 -0
- package/dist/lint/post-synth/repeated-cmd.d.ts.map +1 -0
- package/dist/lint/post-synth/root-after-user.d.ts +10 -0
- package/dist/lint/post-synth/root-after-user.d.ts.map +1 -0
- package/dist/lint/post-synth/unused-global-arg.d.ts +10 -0
- package/dist/lint/post-synth/unused-global-arg.d.ts.map +1 -0
- package/dist/lint/rules/index.d.ts +1 -0
- package/dist/lint/rules/index.d.ts.map +1 -1
- package/dist/lint/rules/per-kind-order.d.ts +15 -0
- package/dist/lint/rules/per-kind-order.d.ts.map +1 -0
- package/dist/manifest.json +1 -1
- package/dist/meta.json +444 -86
- package/dist/okf/index.md +10 -1
- package/dist/okf/rules/DKRD012.md +2 -2
- package/dist/okf/rules/DKRD013.md +11 -0
- package/dist/okf/rules/DKRD014.md +11 -0
- package/dist/okf/rules/DKRD015.md +11 -0
- package/dist/okf/rules/DKRD016.md +11 -0
- package/dist/okf/rules/DKRD017.md +11 -0
- package/dist/okf/rules/DKRD018.md +11 -0
- package/dist/okf/rules/DKRD019.md +11 -0
- package/dist/okf/rules/DKRD020.md +11 -0
- package/dist/okf/rules/DKRS002.md +15 -0
- package/dist/okf/types/DockerConfig.md +7 -5
- package/dist/okf/types/DockerSecret.md +8 -4
- package/dist/okf/types/Dockerfile.md +4 -1
- package/dist/okf/types/Network.md +10 -6
- package/dist/okf/types/Service.md +93 -15
- package/dist/okf/types/Volume.md +5 -5
- package/dist/plugin.d.ts.map +1 -1
- package/dist/rules/add-url-checksum.ts +38 -0
- package/dist/rules/apt-update-alone.ts +39 -0
- package/dist/rules/cache-busting-copy.ts +65 -0
- package/dist/rules/copy-from-stage.ts +54 -0
- package/dist/rules/docker-helpers.ts +80 -0
- package/dist/rules/no-root-user.ts +19 -17
- package/dist/rules/per-kind-order.ts +130 -0
- package/dist/rules/relative-workdir.ts +38 -0
- package/dist/rules/repeated-cmd.ts +35 -0
- package/dist/rules/root-after-user.ts +44 -0
- package/dist/rules/unused-global-arg.ts +38 -0
- package/dist/serializer.d.ts.map +1 -1
- package/dist/skills/chant-docker-patterns.md +93 -24
- package/dist/skills/chant-docker.md +43 -16
- package/dist/spec/parse-compose.d.ts +29 -2
- package/dist/spec/parse-compose.d.ts.map +1 -1
- package/dist/spec/parse-engine.d.ts +9 -0
- package/dist/spec/parse-engine.d.ts.map +1 -1
- package/dist/types/index.d.ts +344 -41
- package/package.json +3 -3
- package/src/codegen/__snapshots__/okf.test.ts.snap +278 -39
- package/src/codegen/docs.ts +14 -0
- package/src/codegen/generate.ts +128 -31
- package/src/codegen/okf.test.ts +1 -1
- package/src/dockerfile-order.ts +20 -0
- package/src/docs-examples.test.ts +141 -0
- package/src/generated/index.d.ts +344 -41
- package/src/generated/index.ts +396 -6
- package/src/generated/lexicon-docker.json +444 -86
- package/src/import/adapter.ts +8 -2
- package/src/import/generator.test.ts +4 -4
- package/src/import/generator.ts +53 -37
- package/src/import/parser.ts +82 -13
- package/src/import/roundtrip.test.ts +167 -2
- package/src/import/testdata/configs-secrets.yaml +15 -0
- package/src/lint/audit-catalog.ts +9 -1
- package/src/lint/audit-lineage.ts +14 -0
- package/src/lint/post-synth/add-url-checksum.ts +38 -0
- package/src/lint/post-synth/apt-update-alone.ts +39 -0
- package/src/lint/post-synth/cache-busting-copy.ts +65 -0
- package/src/lint/post-synth/copy-from-stage.ts +54 -0
- package/src/lint/post-synth/docker-helpers.ts +80 -0
- package/src/lint/post-synth/index.ts +16 -0
- package/src/lint/post-synth/no-root-user.ts +19 -17
- package/src/lint/post-synth/post-synth.test.ts +149 -0
- package/src/lint/post-synth/relative-workdir.ts +38 -0
- package/src/lint/post-synth/repeated-cmd.ts +35 -0
- package/src/lint/post-synth/root-after-user.ts +44 -0
- package/src/lint/post-synth/unused-global-arg.ts +38 -0
- package/src/lint/rules/index.ts +1 -0
- package/src/lint/rules/per-kind-order.ts +130 -0
- package/src/lint/rules/rules.test.ts +88 -0
- package/src/plugin.test.ts +4 -0
- package/src/plugin.ts +2 -1
- package/src/serializer.test.ts +112 -0
- package/src/serializer.ts +58 -11
- package/src/skills/chant-docker-patterns.md +93 -24
- package/src/skills/chant-docker.md +43 -16
- package/src/spec/parse-compose.ts +167 -95
- package/src/spec/parse-engine.ts +17 -5
- package/src/typecheck.e2e.test.ts +107 -0
- package/src/typed-props.test.ts +60 -0
package/src/codegen/generate.ts
CHANGED
|
@@ -10,8 +10,9 @@ import { join } from "path";
|
|
|
10
10
|
import { generateComposePipeline } from "./generate-compose";
|
|
11
11
|
import { generateDockerfilePipeline } from "./generate-dockerfile";
|
|
12
12
|
import { entityTypeToTsName } from "./naming";
|
|
13
|
-
import
|
|
14
|
-
import type
|
|
13
|
+
import { PER_KIND_ORDER } from "../dockerfile-order";
|
|
14
|
+
import { COMPOSE_STRING, type ComposeParseResult } from "../spec/parse-compose";
|
|
15
|
+
import type { DockerfileParseResult, DockerfileInstruction } from "../spec/parse-engine";
|
|
15
16
|
|
|
16
17
|
export interface DockerGenerateResult {
|
|
17
18
|
resources: number;
|
|
@@ -72,9 +73,13 @@ function generateLexiconJSON(compose: ComposeParseResult[], dockerfile: Dockerfi
|
|
|
72
73
|
resourceType: dockerfile.typeName,
|
|
73
74
|
kind: "resource",
|
|
74
75
|
description: dockerfile.description ?? "",
|
|
75
|
-
properties:
|
|
76
|
-
|
|
77
|
-
|
|
76
|
+
properties: {
|
|
77
|
+
...Object.fromEntries(
|
|
78
|
+
dockerfile.instructions.filter((i) => !i.stepOnly).map((i) => [i.name, { description: i.description ?? "" }]),
|
|
79
|
+
),
|
|
80
|
+
globalArgs: { description: "ARG instructions written before the first FROM" },
|
|
81
|
+
steps: { description: "Instructions after FROM, written in the order given; one instruction per step" },
|
|
82
|
+
},
|
|
78
83
|
};
|
|
79
84
|
|
|
80
85
|
return JSON.stringify(entries, null, 2);
|
|
@@ -85,37 +90,23 @@ function generateTypesDTS(compose: ComposeParseResult[], dockerfile: DockerfileP
|
|
|
85
90
|
"// Code generated by chant generate. DO NOT EDIT.",
|
|
86
91
|
'import type { Declarable } from "@intentius/chant/declarable";',
|
|
87
92
|
"",
|
|
93
|
+
// This file is type-checked on its own (validate's types-compile), so it
|
|
94
|
+
// can't import the interpolation module; an env() value is matched by shape.
|
|
95
|
+
"/** An `env()` interpolation, as returned by the lexicon's `env`. */",
|
|
96
|
+
"interface EnvIntrinsic { toJSON(): string; toString(): string; }",
|
|
97
|
+
"",
|
|
88
98
|
];
|
|
89
99
|
|
|
100
|
+
lines.push(...composeTypeLines(compose));
|
|
90
101
|
for (const c of compose) {
|
|
91
102
|
const tsName = entityTypeToTsName(c.typeName);
|
|
92
|
-
lines.push(`export
|
|
93
|
-
for (const p of c.properties) {
|
|
94
|
-
const opt = p.required ? "" : "?";
|
|
95
|
-
lines.push(` ${p.name}${opt}: ${p.type};`);
|
|
96
|
-
}
|
|
97
|
-
lines.push(`}`);
|
|
98
|
-
lines.push(`export declare const ${tsName}: new (props?: ${tsName}Props) => Declarable;`);
|
|
99
|
-
lines.push("");
|
|
103
|
+
lines.push(`export declare const ${tsName}: new (props?: ${tsName}Props, attributes?: Record<string, unknown>) => Declarable;`);
|
|
100
104
|
}
|
|
105
|
+
lines.push("");
|
|
101
106
|
|
|
102
107
|
const dfName = entityTypeToTsName(dockerfile.typeName);
|
|
103
|
-
lines.push(
|
|
104
|
-
lines.push(`
|
|
105
|
-
lines.push(` as?: string;`);
|
|
106
|
-
for (const i of dockerfile.instructions.filter((i) => i.name !== "from")) {
|
|
107
|
-
lines.push(` ${i.name}?: ${i.multi ? "string[]" : "string"};`);
|
|
108
|
-
}
|
|
109
|
-
lines.push(`}`);
|
|
110
|
-
lines.push(`export interface DockerfileProps {`);
|
|
111
|
-
lines.push(` name?: string;`);
|
|
112
|
-
lines.push(` from?: string;`);
|
|
113
|
-
lines.push(` stages?: DockerfileStage[];`);
|
|
114
|
-
for (const i of dockerfile.instructions.filter((i) => i.name !== "from")) {
|
|
115
|
-
lines.push(` ${i.name}?: ${i.multi ? "string[]" : "string"};`);
|
|
116
|
-
}
|
|
117
|
-
lines.push(`}`);
|
|
118
|
-
lines.push(`export declare const ${dfName}: new (props?: DockerfileProps) => Declarable;`);
|
|
108
|
+
lines.push(...dockerfileTypeLines(dockerfile));
|
|
109
|
+
lines.push(`export declare const ${dfName}: new (props?: DockerfileProps, attributes?: Record<string, unknown>) => Declarable;`);
|
|
119
110
|
lines.push("");
|
|
120
111
|
|
|
121
112
|
return lines.join("\n");
|
|
@@ -125,22 +116,128 @@ function generateIndexTS(compose: ComposeParseResult[], dockerfile: DockerfilePa
|
|
|
125
116
|
const lines: string[] = [
|
|
126
117
|
"// Code generated by chant generate. DO NOT EDIT.",
|
|
127
118
|
'import { createResource } from "./runtime";',
|
|
119
|
+
'import type { Declarable } from "@intentius/chant/declarable";',
|
|
120
|
+
'import type { EnvIntrinsic } from "../interpolation";',
|
|
121
|
+
"",
|
|
122
|
+
// createResource returns a constructor with untyped props. Each constructor
|
|
123
|
+
// below is cast to one that takes its props type, so a misspelled key or a
|
|
124
|
+
// wrong field type is a compile error where the resource is written, and
|
|
125
|
+
// the editor completes the fields (#3934).
|
|
126
|
+
"/** A docker resource constructor whose props are type-checked. */",
|
|
127
|
+
"export type DockerResourceConstructor<P> = new (props?: P, attributes?: Record<string, unknown>) => InstanceType<ReturnType<typeof createResource>>;",
|
|
128
128
|
"",
|
|
129
129
|
];
|
|
130
130
|
|
|
131
|
+
lines.push(...composeTypeLines(compose));
|
|
131
132
|
for (const c of compose) {
|
|
132
133
|
const tsName = entityTypeToTsName(c.typeName);
|
|
133
|
-
lines.push(
|
|
134
|
+
lines.push(
|
|
135
|
+
`export const ${tsName} = createResource("${c.typeName}", "docker", {}) as unknown as DockerResourceConstructor<${tsName}Props>;`,
|
|
136
|
+
);
|
|
134
137
|
}
|
|
135
138
|
|
|
139
|
+
lines.push("");
|
|
140
|
+
// The Dockerfile prop types are exported from here too (not only from
|
|
141
|
+
// index.d.ts, which index.ts shadows) so a module can type a shared stage
|
|
142
|
+
// or step list: `export const install: DockerfileStep[] = [...]`.
|
|
143
|
+
lines.push(...dockerfileTypeLines(dockerfile));
|
|
136
144
|
lines.push("");
|
|
137
145
|
const dfName = entityTypeToTsName(dockerfile.typeName);
|
|
138
|
-
lines.push(
|
|
146
|
+
lines.push(
|
|
147
|
+
`export const ${dfName} = createResource("${dockerfile.typeName}", "docker", {}) as unknown as DockerResourceConstructor<DockerfileProps>;`,
|
|
148
|
+
);
|
|
139
149
|
lines.push("");
|
|
140
150
|
|
|
141
151
|
return lines.join("\n");
|
|
142
152
|
}
|
|
143
153
|
|
|
154
|
+
/** One props interface per Compose entity. */
|
|
155
|
+
function composeTypeLines(compose: ComposeParseResult[]): string[] {
|
|
156
|
+
const lines: string[] = [
|
|
157
|
+
"/**",
|
|
158
|
+
" * A Compose string value: a string, an `env()` interpolation, or an entity",
|
|
159
|
+
" * (service, network, volume, config, secret or Dockerfile), which is written as its name.",
|
|
160
|
+
" */",
|
|
161
|
+
`export type ${COMPOSE_STRING} = string | EnvIntrinsic | Declarable;`,
|
|
162
|
+
"",
|
|
163
|
+
];
|
|
164
|
+
for (const c of compose) {
|
|
165
|
+
const tsName = entityTypeToTsName(c.typeName);
|
|
166
|
+
if (c.description) lines.push(`/** ${c.description} */`);
|
|
167
|
+
lines.push(`export interface ${tsName}Props {`);
|
|
168
|
+
for (const p of c.properties) {
|
|
169
|
+
const opt = p.required ? "" : "?";
|
|
170
|
+
if (p.description) lines.push(` /** ${p.description} */`);
|
|
171
|
+
lines.push(` ${p.name}${opt}: ${p.type};`);
|
|
172
|
+
}
|
|
173
|
+
lines.push(`}`);
|
|
174
|
+
lines.push("");
|
|
175
|
+
}
|
|
176
|
+
return lines;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* The Dockerfile prop types: one `DockerfileStep` variant per instruction,
|
|
181
|
+
* the stage, and the props. The per-kind fields keep the instructions they
|
|
182
|
+
* always had; SHELL, STOPSIGNAL and ONBUILD are steps only.
|
|
183
|
+
*/
|
|
184
|
+
function dockerfileTypeLines(dockerfile: DockerfileParseResult): string[] {
|
|
185
|
+
const lines: string[] = [];
|
|
186
|
+
const stepInstructions = dockerfile.instructions.filter((i) => i.name !== "from");
|
|
187
|
+
const perKind = stepInstructions.filter((i) => !i.stepOnly);
|
|
188
|
+
const stepValue = (i: DockerfileInstruction): string =>
|
|
189
|
+
["string", ...(i.numeric ? ["number"] : []), ...(i.execForm ? ["string[]"] : [])].join(" | ");
|
|
190
|
+
const fixed = PER_KIND_ORDER.map((k) => k.toUpperCase()).join(", ");
|
|
191
|
+
|
|
192
|
+
lines.push(`/** The Dockerfile instructions a step can name. FROM is the stage's own \`from\` field. */`);
|
|
193
|
+
lines.push(`export type DockerfileInstructionName = ${stepInstructions.map((i) => JSON.stringify(i.name)).join(" | ")};`);
|
|
194
|
+
lines.push(`type DockerfileStepOf<K extends DockerfileInstructionName, V> = { [P in K]: V } & { [P in Exclude<DockerfileInstructionName, K>]?: never };`);
|
|
195
|
+
lines.push(`/**`);
|
|
196
|
+
lines.push(` * One Dockerfile instruction, named by its single key: \`{ run: "npm ci" }\` is \`RUN npm ci\`.`);
|
|
197
|
+
lines.push(` * A string[] value (run, cmd, entrypoint, shell) is written in JSON exec form.`);
|
|
198
|
+
lines.push(` */`);
|
|
199
|
+
lines.push(`export type DockerfileStep =`);
|
|
200
|
+
for (const i of stepInstructions) {
|
|
201
|
+
lines.push(` /** ${i.name.toUpperCase()}: ${i.description ?? ""} */`);
|
|
202
|
+
lines.push(` | DockerfileStepOf<${JSON.stringify(i.name)}, ${stepValue(i)}>`);
|
|
203
|
+
}
|
|
204
|
+
lines[lines.length - 1] += ";";
|
|
205
|
+
|
|
206
|
+
const fields = (): void => {
|
|
207
|
+
lines.push(` /** Instructions after FROM, written exactly in this order. Can't be combined with the per-kind fields. */`);
|
|
208
|
+
lines.push(` steps?: DockerfileStep[];`);
|
|
209
|
+
for (const i of perKind) {
|
|
210
|
+
lines.push(` /** ${i.name.toUpperCase()} as a per-kind field, written in the fixed order. Use \`steps\` to choose the order. */`);
|
|
211
|
+
lines.push(` ${i.name}?: ${i.multi ? "string[]" : "string"};`);
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
const fixedDoc = ` * Without \`steps\`, the per-kind fields are written in a fixed order: ${fixed}.`;
|
|
215
|
+
|
|
216
|
+
lines.push(`/**`);
|
|
217
|
+
lines.push(` * One build stage: FROM, then its instructions.`);
|
|
218
|
+
lines.push(fixedDoc);
|
|
219
|
+
lines.push(` */`);
|
|
220
|
+
lines.push(`export interface DockerfileStage {`);
|
|
221
|
+
lines.push(` from: string;`);
|
|
222
|
+
lines.push(` as?: string;`);
|
|
223
|
+
fields();
|
|
224
|
+
lines.push(`}`);
|
|
225
|
+
lines.push(`/**`);
|
|
226
|
+
lines.push(` * A Dockerfile: \`stages\` for a multi-stage build, or \`from\` and the stage fields for one stage.`);
|
|
227
|
+
lines.push(fixedDoc);
|
|
228
|
+
lines.push(` */`);
|
|
229
|
+
lines.push(`export interface DockerfileProps {`);
|
|
230
|
+
lines.push(` name?: string;`);
|
|
231
|
+
lines.push(` /** ARG instructions written before the first FROM, e.g. \`["NODE_VERSION=20"]\`. */`);
|
|
232
|
+
lines.push(` globalArgs?: string[];`);
|
|
233
|
+
lines.push(` from?: string;`);
|
|
234
|
+
lines.push(` as?: string;`);
|
|
235
|
+
lines.push(` stages?: DockerfileStage[];`);
|
|
236
|
+
fields();
|
|
237
|
+
lines.push(`}`);
|
|
238
|
+
return lines;
|
|
239
|
+
}
|
|
240
|
+
|
|
144
241
|
/**
|
|
145
242
|
* Write generated artifacts to disk under baseDir/src/generated/.
|
|
146
243
|
*/
|
package/src/codegen/okf.test.ts
CHANGED
|
@@ -25,7 +25,7 @@ describe("docker OKF bundle", () => {
|
|
|
25
25
|
expect(front.type).toBe("resource-type");
|
|
26
26
|
expect(front.resource_type).toBe("Docker::Compose::Service");
|
|
27
27
|
// Registry-carried property description reaches the concept body.
|
|
28
|
-
expect(service.body).toContain("- `image` (`
|
|
28
|
+
expect(service.body).toContain("- `image` (`ComposeString`): Specify the image to start the container from.");
|
|
29
29
|
// The no-latest-tag rule governs Service…
|
|
30
30
|
expect(service.body).toContain("[DKRS001](/rules/DKRS001.md)");
|
|
31
31
|
// …and points back at it.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The order a Dockerfile stage's per-kind fields (`run`, `copy`, `user`, ...)
|
|
3
|
+
* are written in when the stage has no `steps`. It never changes, so a
|
|
4
|
+
* Dockerfile declared with per-kind fields builds the same output it always
|
|
5
|
+
* has; `steps` is how a stage controls its own order.
|
|
6
|
+
*/
|
|
7
|
+
export const PER_KIND_ORDER = [
|
|
8
|
+
"arg", "env", "workdir", "user", "run", "copy", "add",
|
|
9
|
+
"expose", "volume", "label", "entrypoint", "cmd", "healthcheck",
|
|
10
|
+
] as const;
|
|
11
|
+
|
|
12
|
+
export type PerKindField = (typeof PER_KIND_ORDER)[number];
|
|
13
|
+
|
|
14
|
+
/** The step keys whose array value is written in JSON exec form. */
|
|
15
|
+
export const EXEC_FORM_STEPS: ReadonlySet<string> = new Set(["run", "cmd", "entrypoint", "shell"]);
|
|
16
|
+
|
|
17
|
+
/** Every instruction a step can name. FROM is the stage's own field. */
|
|
18
|
+
export const STEP_INSTRUCTIONS: ReadonlySet<string> = new Set([
|
|
19
|
+
...PER_KIND_ORDER, "shell", "stopsignal", "onbuild",
|
|
20
|
+
]);
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Dockerfile examples in the docs and skills build, and build what the
|
|
3
|
+
* page says they do.
|
|
4
|
+
*
|
|
5
|
+
* - A ```typescript block with no title that declares a Dockerfile is run
|
|
6
|
+
* and serialized. When the next code block is an untitled ```dockerfile
|
|
7
|
+
* block, the built file must equal it.
|
|
8
|
+
* - A titled ```typescript block (`title="src/web.ts"`) must equal that file
|
|
9
|
+
* in examples/dockerfile-sharing, and a titled ```dockerfile block
|
|
10
|
+
* (`title="Dockerfile.web"`) must equal that file from building the
|
|
11
|
+
* example.
|
|
12
|
+
* - In importing.mdx, importing the ```dockerfile block titled "Dockerfile"
|
|
13
|
+
* must give the ```typescript block that follows it.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { describe, test, expect } from "vitest";
|
|
17
|
+
import { readFileSync } from "fs";
|
|
18
|
+
import { join } from "path";
|
|
19
|
+
import * as ts from "typescript";
|
|
20
|
+
import { build } from "@intentius/chant/build";
|
|
21
|
+
import * as chant from "@intentius/chant";
|
|
22
|
+
import type { Declarable } from "@intentius/chant/declarable";
|
|
23
|
+
import * as lexicon from "./index";
|
|
24
|
+
import { dockerSerializer } from "./serializer";
|
|
25
|
+
import { DockerTemplateGenerator, DockerTemplateParser } from "./import/adapter";
|
|
26
|
+
|
|
27
|
+
const root = join(import.meta.dirname, "..");
|
|
28
|
+
const sharingExample = join(root, "examples", "dockerfile-sharing");
|
|
29
|
+
|
|
30
|
+
interface Block {
|
|
31
|
+
lang: string;
|
|
32
|
+
title?: string;
|
|
33
|
+
code: string;
|
|
34
|
+
/** The prose between the previous code block and this one. */
|
|
35
|
+
before: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function blocks(file: string): Block[] {
|
|
39
|
+
const text = readFileSync(join(root, file), "utf8");
|
|
40
|
+
const out: Block[] = [];
|
|
41
|
+
let last = 0;
|
|
42
|
+
for (const m of text.matchAll(/^```(\w+)([^\n]*)\n([\s\S]*?)^```$/gm)) {
|
|
43
|
+
const title = m[2]!.match(/title="([^"]+)"/)?.[1];
|
|
44
|
+
out.push({ lang: m[1]!, ...(title ? { title } : {}), code: m[3]!, before: text.slice(last, m.index) });
|
|
45
|
+
last = m.index! + m[0].length;
|
|
46
|
+
}
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Run one TypeScript block as a module and serialize the Dockerfiles it
|
|
52
|
+
* exports. A block whose first line is a path comment (`// src/web.ts`) is
|
|
53
|
+
* one module of a multi-file example: `modules` holds the exports of the
|
|
54
|
+
* page's earlier modules, by path, for its relative imports.
|
|
55
|
+
*/
|
|
56
|
+
function buildBlock(code: string, pageModules: Map<string, Record<string, unknown>> = new Map()): Record<string, string> {
|
|
57
|
+
const js = ts.transpileModule(code, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 } }).outputText;
|
|
58
|
+
const modules: Record<string, unknown> = { "@intentius/chant-lexicon-docker": lexicon, "@intentius/chant": chant };
|
|
59
|
+
const path = code.match(/^\/\/ (src\/\S+)\.ts\s*$/m)?.[1];
|
|
60
|
+
const req = (name: string): unknown => {
|
|
61
|
+
if (name.startsWith("./") && path) {
|
|
62
|
+
const target = join(path, "..", name).replace(/\\/g, "/");
|
|
63
|
+
const found = pageModules.get(target);
|
|
64
|
+
if (found) return found;
|
|
65
|
+
}
|
|
66
|
+
if (!(name in modules)) throw new Error(`docs example imports ${name}, which this test does not provide`);
|
|
67
|
+
return modules[name];
|
|
68
|
+
};
|
|
69
|
+
const mod = { exports: {} as Record<string, unknown> };
|
|
70
|
+
// Snippets that leave out the import line still see the lexicon's exports.
|
|
71
|
+
const globals = Object.keys(lexicon).filter((k) => /^[A-Za-z_$][\w$]*$/.test(k));
|
|
72
|
+
new Function("require", "module", "exports", ...globals, js)(req, mod, mod.exports, ...globals.map((k) => (lexicon as Record<string, unknown>)[k]));
|
|
73
|
+
if (path) pageModules.set(path, mod.exports);
|
|
74
|
+
const entities = new Map<string, Declarable>();
|
|
75
|
+
for (const [name, value] of Object.entries(mod.exports)) {
|
|
76
|
+
if (value && typeof value === "object" && "entityType" in value) entities.set(name, value as Declarable);
|
|
77
|
+
}
|
|
78
|
+
const result = dockerSerializer.serialize(entities);
|
|
79
|
+
return typeof result === "string" ? {} : (result.files ?? {});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const PAGES = [
|
|
83
|
+
"docs/pages/dockerfiles.mdx",
|
|
84
|
+
"docs/pages/examples.mdx",
|
|
85
|
+
"docs/pages/lint-rules.mdx",
|
|
86
|
+
"src/skills/chant-docker.md",
|
|
87
|
+
"src/skills/chant-docker-patterns.md",
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
describe("docs Dockerfile examples", () => {
|
|
91
|
+
for (const page of PAGES) {
|
|
92
|
+
test(`${page}: every Dockerfile example builds what the page shows`, () => {
|
|
93
|
+
const list = blocks(page);
|
|
94
|
+
const pageModules = new Map<string, Record<string, unknown>>();
|
|
95
|
+
let checked = 0;
|
|
96
|
+
list.forEach((b, i) => {
|
|
97
|
+
if (b.lang !== "typescript" || b.title) return;
|
|
98
|
+
const isModule = /^\/\/ src\/\S+\.ts\s*$/m.test(b.code);
|
|
99
|
+
if (!b.code.includes("new Dockerfile(") && !isModule) return;
|
|
100
|
+
const files = buildBlock(b.code, pageModules);
|
|
101
|
+
if (!b.code.includes("new Dockerfile(")) return;
|
|
102
|
+
expect(Object.keys(files).length, `${page} block ${i + 1} declares a Dockerfile`).toBeGreaterThan(0);
|
|
103
|
+
checked++;
|
|
104
|
+
// The output shown right after it, under a "`Dockerfile.name`:" line.
|
|
105
|
+
const next = list[i + 1];
|
|
106
|
+
if (next && next.lang === "dockerfile" && !next.title && /^\s*`Dockerfile\.\w+`:\s*$/.test(next.before)) {
|
|
107
|
+
expect(Object.values(files).join("---\n"), `${page} block ${i + 1}`).toBe(next.code);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
expect(checked, `${page} has Dockerfile examples`).toBeGreaterThan(0);
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
test("dockerfiles.mdx sharing section matches the dockerfile-sharing example and its build", async () => {
|
|
115
|
+
const titled = blocks("docs/pages/dockerfiles.mdx").filter((b) => b.title);
|
|
116
|
+
const sources = titled.filter((b) => b.lang === "typescript");
|
|
117
|
+
const outputs = titled.filter((b) => b.lang === "dockerfile");
|
|
118
|
+
expect(sources.map((b) => b.title)).toEqual(["src/shared/node.ts", "src/web.ts", "src/worker.ts", "src/shared/node-image.ts", "src/api.ts"]);
|
|
119
|
+
for (const b of sources) {
|
|
120
|
+
expect(b.code, b.title).toBe(readFileSync(join(sharingExample, b.title!), "utf8"));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const result = await build(join(sharingExample, "src"), [dockerSerializer]);
|
|
124
|
+
expect(result.errors).toEqual([]);
|
|
125
|
+
const files = (result.outputs.get("docker") as unknown as { files: Record<string, string> }).files;
|
|
126
|
+
expect(outputs.map((b) => b.title).sort()).toEqual(Object.keys(files).sort());
|
|
127
|
+
for (const b of outputs) {
|
|
128
|
+
expect(b.code, b.title).toBe(files[b.title!]);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("importing.mdx shows what importing its Dockerfile produces", () => {
|
|
133
|
+
const list = blocks("docs/pages/importing.mdx");
|
|
134
|
+
const at = list.findIndex((b) => b.lang === "dockerfile" && b.title === "Dockerfile");
|
|
135
|
+
expect(at).toBeGreaterThan(-1);
|
|
136
|
+
const [file] = new DockerTemplateGenerator().generate(new DockerTemplateParser().parse(list[at]!.code));
|
|
137
|
+
expect(list[at + 1]!.code).toBe(file!.content.endsWith("\n") ? file!.content : `${file!.content}\n`);
|
|
138
|
+
// And building the import gives the Dockerfile back.
|
|
139
|
+
expect(Object.values(buildBlock(list[at + 1]!.code))[0]).toBe(list[at]!.code.replace(/^#.*\n/gm, ""));
|
|
140
|
+
});
|
|
141
|
+
});
|