@docker-doctor/cli 0.5.1 → 0.5.2

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/index.d.cts CHANGED
@@ -13,6 +13,7 @@ interface Diagnostic {
13
13
  severity: DiagnosticSeverity;
14
14
  message: string;
15
15
  help: string;
16
+ category?: RuleCategory;
16
17
  line?: number;
17
18
  column?: number;
18
19
  }
@@ -45,34 +46,34 @@ interface DockerDoctorConfig {
45
46
  }
46
47
  //#endregion
47
48
  //#region ../core/src/project-info/discover.d.ts
48
- declare const discoverProject: (rootDir: string, options?: {
49
+ export declare const discoverProject: (rootDir: string, options?: {
49
50
  ignoreFiles?: readonly string[];
50
51
  }) => Promise<ProjectInfo>;
51
52
  //#endregion
52
53
  //#region ../core/src/parsers/dockerfile-parser.d.ts
53
- declare const parseDockerfile: (content: string) => DockerfileInstruction[];
54
+ export declare const parseDockerfile: (content: string, file?: string) => DockerfileInstruction[];
54
55
  //#endregion
55
56
  //#region ../core/src/parsers/compose-parser.d.ts
56
- declare const parseCompose: (content: string, filepath: string) => unknown;
57
+ export declare const parseCompose: (content: string, filepath: string) => unknown;
57
58
  //#endregion
58
59
  //#region ../core/src/runners/dockerfile-runner.d.ts
59
- declare const runDockerfileRules: (instructions: DockerfileInstruction[], file: string, projectFiles: string[], rulesConfig?: Record<string, RuleSeverity>, categoriesConfig?: Record<string, RuleSeverity>) => Diagnostic[];
60
+ export declare const runDockerfileRules: (instructions: DockerfileInstruction[], file: string, projectFiles: string[], rulesConfig?: Record<string, RuleSeverity>, categoriesConfig?: Record<string, RuleSeverity>) => Diagnostic[];
60
61
  //#endregion
61
62
  //#region ../core/src/runners/compose-runner.d.ts
62
- declare const runComposeRules: (composeContent: unknown, file: string, rulesConfig?: Record<string, RuleSeverity>, categoriesConfig?: Record<string, RuleSeverity>, locate?: ComposeLocator) => Diagnostic[];
63
+ export declare const runComposeRules: (composeContent: unknown, file: string, rulesConfig?: Record<string, RuleSeverity>, categoriesConfig?: Record<string, RuleSeverity>, locate?: ComposeLocator) => Diagnostic[];
63
64
  //#endregion
64
65
  //#region ../core/src/rules/index.d.ts
65
- declare const allRules: RuleDefinition[];
66
- declare const findRule: (key: string) => RuleDefinition | undefined;
66
+ export declare const allRules: RuleDefinition[];
67
+ export declare const findRule: (key: string) => RuleDefinition | undefined;
67
68
  //#endregion
68
69
  //#region ../core/src/config/define-config.d.ts
69
- declare const defineConfig: (config: DockerDoctorConfig) => DockerDoctorConfig;
70
+ export declare const defineConfig: (config: DockerDoctorConfig) => DockerDoctorConfig;
70
71
  //#endregion
71
72
  //#region ../core/src/config/loader.d.ts
72
- declare const loadConfig: (rootDir: string, customPath?: string, onWarning?: (message: string) => void) => Promise<DockerDoctorConfig>;
73
+ export declare const loadConfig: (rootDir: string, customPath?: string, onWarning?: (message: string) => void) => Promise<DockerDoctorConfig>;
73
74
  //#endregion
74
75
  //#region ../core/src/scoring.d.ts
75
- declare const calculateScore: (diagnostics: Diagnostic[]) => {
76
+ export declare const calculateScore: (diagnostics: Diagnostic[]) => {
76
77
  score: number;
77
78
  label: string;
78
79
  };
@@ -80,6 +81,7 @@ declare const calculateScore: (diagnostics: Diagnostic[]) => {
80
81
  //#region ../core/src/report.d.ts
81
82
  interface JsonReport {
82
83
  diagnostics: {
84
+ category: RuleCategory;
83
85
  column?: number;
84
86
  file: string;
85
87
  help: string;
@@ -94,10 +96,10 @@ interface JsonReport {
94
96
  score: number;
95
97
  timestamp: string;
96
98
  }
97
- declare const toJsonReport: (diagnostics: Diagnostic[], score: number, label: string, project: ProjectInfo) => JsonReport;
99
+ export declare const toJsonReport: (diagnostics: Diagnostic[], score: number, label: string, project: ProjectInfo) => JsonReport;
98
100
  //#endregion
99
101
  //#region src/index.d.ts
100
- declare const version: string;
102
+ export declare const version: string;
101
103
  //#endregion
102
- export { type Diagnostic, type DockerDoctorConfig, type RuleCategory, type RuleSeverity, allRules, calculateScore, defineConfig, discoverProject, findRule, loadConfig, parseCompose, parseDockerfile, runComposeRules, runDockerfileRules, toJsonReport, version };
104
+ export type { Diagnostic, DockerDoctorConfig, RuleCategory, RuleSeverity };
103
105
  //# sourceMappingURL=index.d.cts.map
package/dist/index.d.mts CHANGED
@@ -13,6 +13,7 @@ interface Diagnostic {
13
13
  severity: DiagnosticSeverity;
14
14
  message: string;
15
15
  help: string;
16
+ category?: RuleCategory;
16
17
  line?: number;
17
18
  column?: number;
18
19
  }
@@ -45,34 +46,34 @@ interface DockerDoctorConfig {
45
46
  }
46
47
  //#endregion
47
48
  //#region ../core/src/project-info/discover.d.ts
48
- declare const discoverProject: (rootDir: string, options?: {
49
+ export declare const discoverProject: (rootDir: string, options?: {
49
50
  ignoreFiles?: readonly string[];
50
51
  }) => Promise<ProjectInfo>;
51
52
  //#endregion
52
53
  //#region ../core/src/parsers/dockerfile-parser.d.ts
53
- declare const parseDockerfile: (content: string) => DockerfileInstruction[];
54
+ export declare const parseDockerfile: (content: string, file?: string) => DockerfileInstruction[];
54
55
  //#endregion
55
56
  //#region ../core/src/parsers/compose-parser.d.ts
56
- declare const parseCompose: (content: string, filepath: string) => unknown;
57
+ export declare const parseCompose: (content: string, filepath: string) => unknown;
57
58
  //#endregion
58
59
  //#region ../core/src/runners/dockerfile-runner.d.ts
59
- declare const runDockerfileRules: (instructions: DockerfileInstruction[], file: string, projectFiles: string[], rulesConfig?: Record<string, RuleSeverity>, categoriesConfig?: Record<string, RuleSeverity>) => Diagnostic[];
60
+ export declare const runDockerfileRules: (instructions: DockerfileInstruction[], file: string, projectFiles: string[], rulesConfig?: Record<string, RuleSeverity>, categoriesConfig?: Record<string, RuleSeverity>) => Diagnostic[];
60
61
  //#endregion
61
62
  //#region ../core/src/runners/compose-runner.d.ts
62
- declare const runComposeRules: (composeContent: unknown, file: string, rulesConfig?: Record<string, RuleSeverity>, categoriesConfig?: Record<string, RuleSeverity>, locate?: ComposeLocator) => Diagnostic[];
63
+ export declare const runComposeRules: (composeContent: unknown, file: string, rulesConfig?: Record<string, RuleSeverity>, categoriesConfig?: Record<string, RuleSeverity>, locate?: ComposeLocator) => Diagnostic[];
63
64
  //#endregion
64
65
  //#region ../core/src/rules/index.d.ts
65
- declare const allRules: RuleDefinition[];
66
- declare const findRule: (key: string) => RuleDefinition | undefined;
66
+ export declare const allRules: RuleDefinition[];
67
+ export declare const findRule: (key: string) => RuleDefinition | undefined;
67
68
  //#endregion
68
69
  //#region ../core/src/config/define-config.d.ts
69
- declare const defineConfig: (config: DockerDoctorConfig) => DockerDoctorConfig;
70
+ export declare const defineConfig: (config: DockerDoctorConfig) => DockerDoctorConfig;
70
71
  //#endregion
71
72
  //#region ../core/src/config/loader.d.ts
72
- declare const loadConfig: (rootDir: string, customPath?: string, onWarning?: (message: string) => void) => Promise<DockerDoctorConfig>;
73
+ export declare const loadConfig: (rootDir: string, customPath?: string, onWarning?: (message: string) => void) => Promise<DockerDoctorConfig>;
73
74
  //#endregion
74
75
  //#region ../core/src/scoring.d.ts
75
- declare const calculateScore: (diagnostics: Diagnostic[]) => {
76
+ export declare const calculateScore: (diagnostics: Diagnostic[]) => {
76
77
  score: number;
77
78
  label: string;
78
79
  };
@@ -80,6 +81,7 @@ declare const calculateScore: (diagnostics: Diagnostic[]) => {
80
81
  //#region ../core/src/report.d.ts
81
82
  interface JsonReport {
82
83
  diagnostics: {
84
+ category: RuleCategory;
83
85
  column?: number;
84
86
  file: string;
85
87
  help: string;
@@ -94,10 +96,10 @@ interface JsonReport {
94
96
  score: number;
95
97
  timestamp: string;
96
98
  }
97
- declare const toJsonReport: (diagnostics: Diagnostic[], score: number, label: string, project: ProjectInfo) => JsonReport;
99
+ export declare const toJsonReport: (diagnostics: Diagnostic[], score: number, label: string, project: ProjectInfo) => JsonReport;
98
100
  //#endregion
99
101
  //#region src/index.d.ts
100
- declare const version: string;
102
+ export declare const version: string;
101
103
  //#endregion
102
- export { type Diagnostic, type DockerDoctorConfig, type RuleCategory, type RuleSeverity, allRules, calculateScore, defineConfig, discoverProject, findRule, loadConfig, parseCompose, parseDockerfile, runComposeRules, runDockerfileRules, toJsonReport, version };
104
+ export type { Diagnostic, DockerDoctorConfig, RuleCategory, RuleSeverity };
103
105
  //# sourceMappingURL=index.d.mts.map
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { a as runDockerfileRules, d as discoverProject, f as version$1, i as runComposeRules, l as parseCompose, n as calculateScore, o as allRules, r as loadConfig, s as findRule, t as toJsonReport, u as parseDockerfile } from "./src-NqSwkGiS.mjs";
2
+ import { a as runDockerfileRules, d as discoverProject, f as version$1, i as runComposeRules, l as parseCompose, n as calculateScore, o as allRules, r as loadConfig, s as findRule, t as toJsonReport, u as parseDockerfile } from "./src-BOGzkp17.mjs";
3
3
 
4
4
  //#region ../core/src/config/define-config.ts
5
5
  const defineConfig = (config) => config;
@@ -4,7 +4,7 @@ import path from "node:path";
4
4
  import { LineCounter, isAlias, isMap, isScalar, isSeq, parse, parseDocument } from "yaml";
5
5
 
6
6
  //#region package.json
7
- var version = "0.5.1";
7
+ var version = "0.5.2";
8
8
 
9
9
  //#endregion
10
10
  //#region ../core/src/project-info/ignore.ts
@@ -71,6 +71,7 @@ const walk = async (dir, fileList = []) => {
71
71
  }));
72
72
  return fileList;
73
73
  };
74
+ const toPosixRelative = (rootDir, file) => path.relative(rootDir, file).split(path.sep).join("/");
74
75
  const discoverProject = async (rootDir, options) => {
75
76
  const allFiles = await walk(rootDir);
76
77
  const dockerfiles = [];
@@ -78,10 +79,13 @@ const discoverProject = async (rootDir, options) => {
78
79
  const dockerignores = [];
79
80
  const isIgnored = createIgnoreMatcher(options?.ignoreFiles);
80
81
  for (const file of allFiles) {
81
- const relative = path.relative(rootDir, file);
82
+ const relative = toPosixRelative(rootDir, file);
82
83
  if (isIgnored(relative)) continue;
83
84
  const base = path.basename(file).toLowerCase();
84
- if (base === ".dockerignore") dockerignores.push(relative);
85
+ if (base.endsWith(".dockerignore")) {
86
+ dockerignores.push(relative);
87
+ continue;
88
+ }
85
89
  if (base === "dockerfile" || base.startsWith("dockerfile.") || base.endsWith(".dockerfile")) dockerfiles.push(relative);
86
90
  if (base === "docker-compose.yml" || base === "docker-compose.yaml" || base === "compose.yml" || base === "compose.yaml" || (base.startsWith("docker-compose.") || base.startsWith("compose.")) && (base.endsWith(".yml") || base.endsWith(".yaml"))) composeFiles.push(relative);
87
91
  }
@@ -92,6 +96,72 @@ const discoverProject = async (rootDir, options) => {
92
96
  };
93
97
  };
94
98
 
99
+ //#endregion
100
+ //#region ../core/src/errors/config-error.ts
101
+ var ConfigError = class extends Error {
102
+ _tag = "ConfigError";
103
+ constructor(options) {
104
+ super(options.message);
105
+ this.name = "ConfigError";
106
+ }
107
+ };
108
+
109
+ //#endregion
110
+ //#region ../core/src/errors/parse-error.ts
111
+ var ParseError = class extends Error {
112
+ _tag = "ParseError";
113
+ file;
114
+ constructor(options) {
115
+ super(options.message);
116
+ this.name = "ParseError";
117
+ this.file = options.file;
118
+ }
119
+ };
120
+
121
+ //#endregion
122
+ //#region ../core/src/parsers/shell-quotes.ts
123
+ /**
124
+ * Replaces every character inside a single- or double-quoted region with a
125
+ * space, keeping the quotes themselves and the string length, so callers can
126
+ * run position-sensitive regexes over shell text without matching inside
127
+ * string literals. A backslash inside double quotes escapes the next
128
+ * character; single quotes have no escapes (POSIX sh). An unclosed quote
129
+ * masks to the end of the text.
130
+ *
131
+ * What this deliberately does NOT do: it is not a tokenizer. It performs no
132
+ * variable or command expansion, knows nothing about subshells, backslash
133
+ * line continuations, `$'...'` or `<<` heredoc bodies, and does not split
134
+ * words. It only blanks quoted spans so regexes keep their positions.
135
+ */
136
+ const maskQuotedText = (text) => {
137
+ let out = "";
138
+ let quote = null;
139
+ let index = 0;
140
+ while (index < text.length) {
141
+ const char = text[index];
142
+ if (quote === null) {
143
+ if (char === "\"" || char === "'") quote = char;
144
+ out += char;
145
+ index += 1;
146
+ continue;
147
+ }
148
+ if (char === quote) {
149
+ quote = null;
150
+ out += char;
151
+ index += 1;
152
+ continue;
153
+ }
154
+ if (quote === "\"" && char === "\\" && index + 1 < text.length) {
155
+ out += " ";
156
+ index += 2;
157
+ continue;
158
+ }
159
+ out += " ";
160
+ index += 1;
161
+ }
162
+ return out;
163
+ };
164
+
95
165
  //#endregion
96
166
  //#region ../core/src/parsers/dockerfile-parser.ts
97
167
  const DOCKERFILE_KEYWORDS = /* @__PURE__ */ new Set([
@@ -154,7 +224,10 @@ const matchInstructionKeyword = (lineContent) => {
154
224
  };
155
225
  return null;
156
226
  };
157
- const findHeredocDelimiters = (lineContent) => [...lineContent.matchAll(HEREDOC_OPENER_RE)].map((m) => m.groups?.delim ?? "");
227
+ const findHeredocDelimiters = (lineContent) => {
228
+ const masked = maskQuotedText(lineContent);
229
+ return [...lineContent.matchAll(HEREDOC_OPENER_RE)].filter((match) => masked[match.index] === "<").map((match) => match.groups?.delim ?? "");
230
+ };
158
231
  const processHeredocLine = (state, trimmed) => {
159
232
  if (trimmed === state.heredocQueue[0]) state.heredocQueue.shift();
160
233
  else state.currentArgs += (state.currentArgs ? " " : "") + trimmed;
@@ -177,7 +250,7 @@ const processInstructionLine = (state, trimmed, lineNum) => {
177
250
  if (state.heredocQueue.length > 0) return;
178
251
  if (!hasContinuation) closeInstruction(state);
179
252
  };
180
- const parseDockerfile = (content) => {
253
+ const parseDockerfile = (content, file = "Dockerfile") => {
181
254
  const state = createParserState();
182
255
  const lines = content.split(/\r?\n/u);
183
256
  for (const [i, rawLine] of lines.entries()) {
@@ -190,32 +263,14 @@ const parseDockerfile = (content) => {
190
263
  if (insideHeredoc) processHeredocLine(state, trimmed);
191
264
  else processInstructionLine(state, trimmed, lineNum);
192
265
  }
266
+ if (state.heredocQueue.length > 0) throw new ParseError({
267
+ file,
268
+ message: `Unterminated heredoc: delimiter "${state.heredocQueue[0]}" (opened on line ${state.startLine}) never closed. BuildKit rejects this Dockerfile too.`
269
+ });
193
270
  closeInstruction(state);
194
271
  return state.instructions;
195
272
  };
196
273
 
197
- //#endregion
198
- //#region ../core/src/errors/config-error.ts
199
- var ConfigError = class extends Error {
200
- _tag = "ConfigError";
201
- constructor(options) {
202
- super(options.message);
203
- this.name = "ConfigError";
204
- }
205
- };
206
-
207
- //#endregion
208
- //#region ../core/src/errors/parse-error.ts
209
- var ParseError = class extends Error {
210
- _tag = "ParseError";
211
- file;
212
- constructor(options) {
213
- super(options.message);
214
- this.name = "ParseError";
215
- this.file = options.file;
216
- }
217
- };
218
-
219
274
  //#endregion
220
275
  //#region ../core/src/parsers/compose-parser.ts
221
276
  const parseCompose = (content, filepath) => {
@@ -449,6 +504,7 @@ const combineAptUpdateInstall = {
449
504
  };
450
505
  const PIPEFAIL_SETTING_RE = /(?:^|\s)-[A-Za-z]*o\s+pipefail\b/u;
451
506
  const HAS_PIPE_RE = /(?<!\|)\|(?!\|)/u;
507
+ const CD_COMMAND_RE = /(?:^|&&|\|\||;|\||\(|`|\$\()\s*cd(?=\s|$)/u;
452
508
  const shellDirectiveEnablesPipefail = (args) => {
453
509
  const argv = parseExecForm(args);
454
510
  return argv !== null && PIPEFAIL_SETTING_RE.test(argv.join(" "));
@@ -476,8 +532,9 @@ const usePipefail = {
476
532
  }
477
533
  if (inst.instruction !== "RUN") continue;
478
534
  const { args } = inst;
479
- if (!HAS_PIPE_RE.test(args)) continue;
480
535
  const execArgv = parseExecForm(args);
536
+ const pipeText = execArgv === null ? maskQuotedText(args) : execArgv.join(" ");
537
+ if (!HAS_PIPE_RE.test(pipeText)) continue;
481
538
  if (!(execArgv === null ? shellHasPipefail || PIPEFAIL_SETTING_RE.test(args) : PIPEFAIL_SETTING_RE.test(execArgv.join(" ")))) diagnostics.push(createDiagnostic(file, this.key, this.defaultSeverity, "RUN instruction uses a pipe (|) but does not configure 'pipefail'. If a command in the pipe fails, the step may still succeed silently.", this.help, inst.line));
482
539
  }
483
540
  return diagnostics;
@@ -506,7 +563,7 @@ const avoidRunCd = {
506
563
  category: "Best Practices",
507
564
  check(instructions, file) {
508
565
  const diagnostics = [];
509
- for (const inst of instructions) if (inst.instruction === "RUN" && /\bcd\b/u.test(inst.args)) diagnostics.push(createDiagnostic(file, this.key, this.defaultSeverity, "Avoid using 'cd' in RUN instructions. Use WORKDIR instead to change the working directory stably across layers.", this.help, inst.line));
566
+ for (const inst of instructions) if (inst.instruction === "RUN" && CD_COMMAND_RE.test(maskQuotedText(inst.args))) diagnostics.push(createDiagnostic(file, this.key, this.defaultSeverity, "Avoid using 'cd' in RUN instructions. Use WORKDIR instead to change the working directory stably across layers.", this.help, inst.line));
510
567
  return diagnostics;
511
568
  },
512
569
  defaultSeverity: "info",
@@ -569,12 +626,13 @@ const bestPracticesRules = [
569
626
  * with a null body (`web:` with nothing under it) is returned as an empty
570
627
  * config so rules still check it: it is the least-configured service in
571
628
  * the file, not a service to skip. Scalar and array bodies are invalid
572
- * compose and are dropped.
629
+ * compose and are dropped. A `services:` written as a YAML sequence is
630
+ * invalid Compose and yields no services.
573
631
  */
574
632
  const composeServices = (composeContent) => {
575
633
  if (!composeContent || typeof composeContent !== "object" || !("services" in composeContent)) return [];
576
634
  const { services } = composeContent;
577
- if (!services || typeof services !== "object") return [];
635
+ if (!services || typeof services !== "object" || Array.isArray(services)) return [];
578
636
  const entries = [];
579
637
  for (const [name, config] of Object.entries(services)) if (config === null || config === void 0) entries.push([name, {}]);
580
638
  else if (typeof config === "object" && !Array.isArray(config)) entries.push([name, config]);
@@ -1242,12 +1300,16 @@ const resolveSeverity = (rule, rulesConfig, categoriesConfig) => rulesConfig?.[r
1242
1300
  //#endregion
1243
1301
  //#region ../core/src/runners/dockerfile-runner.ts
1244
1302
  const runDockerfileRules = (instructions, file, projectFiles, rulesConfig, categoriesConfig) => {
1303
+ if (instructions.length === 0) return [];
1245
1304
  const diagnostics = [];
1246
1305
  for (const rule of allDockerfileRules) {
1247
1306
  const severity = resolveSeverity(rule, rulesConfig, categoriesConfig);
1248
1307
  if (severity === "off") continue;
1249
1308
  const ruleDiagnostics = rule.check(instructions, file, { projectFiles });
1250
- if (severity !== rule.defaultSeverity) for (const diag of ruleDiagnostics) diag.severity = severity;
1309
+ for (const diag of ruleDiagnostics) {
1310
+ diag.category = rule.category;
1311
+ if (severity !== rule.defaultSeverity) diag.severity = severity;
1312
+ }
1251
1313
  diagnostics.push(...ruleDiagnostics);
1252
1314
  }
1253
1315
  return diagnostics;
@@ -1261,7 +1323,10 @@ const runComposeRules = (composeContent, file, rulesConfig, categoriesConfig, lo
1261
1323
  const severity = resolveSeverity(rule, rulesConfig, categoriesConfig);
1262
1324
  if (severity === "off") continue;
1263
1325
  const ruleDiagnostics = rule.check(composeContent, file, { locate });
1264
- if (severity !== rule.defaultSeverity) for (const diag of ruleDiagnostics) diag.severity = severity;
1326
+ for (const diag of ruleDiagnostics) {
1327
+ diag.category = rule.category;
1328
+ if (severity !== rule.defaultSeverity) diag.severity = severity;
1329
+ }
1265
1330
  diagnostics.push(...ruleDiagnostics);
1266
1331
  }
1267
1332
  return diagnostics;
@@ -1371,6 +1436,16 @@ const parseConfigFile = async (filePath, format, parse) => {
1371
1436
  throw new ConfigError({ message: `Failed to parse config ${format}: ${msg}` });
1372
1437
  }
1373
1438
  };
1439
+ const TYPESCRIPT_CONFIG_PATTERN = /\.[cm]?ts$/u;
1440
+ const TYPE_STRIPPING_FAILURES = {
1441
+ ERR_UNKNOWN_FILE_EXTENSION: "this Node.js is not stripping TypeScript types, which needs 22.18 or newer with stripping left on (--no-experimental-strip-types turns it off)",
1442
+ ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING: "Node.js never strips TypeScript types for files under node_modules, on any version, so the config has to live outside it"
1443
+ };
1444
+ const typeStrippingFailure = (filePath, error) => {
1445
+ if (!TYPESCRIPT_CONFIG_PATTERN.test(filePath)) return;
1446
+ if (typeof error !== "object" || error === null || !("code" in error) || typeof error.code !== "string") return;
1447
+ return TYPE_STRIPPING_FAILURES[error.code];
1448
+ };
1374
1449
  const importConfig = async (filePath) => {
1375
1450
  if (filePath.endsWith(".json")) return parseConfigFile(filePath, "JSON", JSON.parse);
1376
1451
  if (filePath.endsWith(".yaml") || filePath.endsWith(".yml")) return parseConfigFile(filePath, "YAML", parse);
@@ -1378,6 +1453,8 @@ const importConfig = async (filePath) => {
1378
1453
  const configModule = await import(filePath);
1379
1454
  return configModule.default || configModule;
1380
1455
  } catch (error) {
1456
+ const reason = typeStrippingFailure(filePath, error);
1457
+ if (reason) throw new ConfigError({ message: `Cannot load ${filePath} on Node.js ${process.version}: ${reason}. A docker-doctor.config.yaml or .json needs no TypeScript support at all.` });
1381
1458
  const msg = error instanceof Error ? error.message : String(error);
1382
1459
  throw new ConfigError({ message: `Failed to load config file ${filePath}: ${msg}` });
1383
1460
  }
@@ -1472,9 +1549,15 @@ const calculateScore = (diagnostics) => {
1472
1549
 
1473
1550
  //#endregion
1474
1551
  //#region ../core/src/report.ts
1475
- const REPORT_SCHEMA_VERSION = 2;
1552
+ const REPORT_SCHEMA_VERSION = 3;
1553
+ const resolveCategory = (diagnostic) => {
1554
+ const category = diagnostic.category ?? findRule(diagnostic.rule)?.category;
1555
+ if (!category) throw new Error(`Diagnostic for unknown rule "${diagnostic.rule}" has no category`);
1556
+ return category;
1557
+ };
1476
1558
  const toJsonReport = (diagnostics, score, label, project) => ({
1477
1559
  diagnostics: diagnostics.map((d) => ({
1560
+ category: resolveCategory(d),
1478
1561
  column: d.column,
1479
1562
  file: d.file,
1480
1563
  help: d.help,
@@ -1485,11 +1568,11 @@ const toJsonReport = (diagnostics, score, label, project) => ({
1485
1568
  })),
1486
1569
  label,
1487
1570
  project,
1488
- schemaVersion: 2,
1571
+ schemaVersion: 3,
1489
1572
  score,
1490
1573
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
1491
1574
  });
1492
1575
 
1493
1576
  //#endregion
1494
1577
  export { runDockerfileRules as a, createComposeLocator as c, discoverProject as d, version as f, runComposeRules as i, parseCompose as l, calculateScore as n, allRules as o, loadConfig as r, findRule as s, toJsonReport as t, parseDockerfile as u };
1495
- //# sourceMappingURL=src-NqSwkGiS.mjs.map
1578
+ //# sourceMappingURL=src-BOGzkp17.mjs.map