@docker-doctor/cli 0.5.3 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/cli.cjs +66 -13
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.mjs +66 -13
- package/dist/cli.mjs.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +7 -1
- package/dist/index.d.mts +7 -1
- package/dist/index.mjs +1 -1
- package/dist/{src-CklEIXTX.mjs → src-BLeXr_hR.mjs} +165 -32
- package/dist/src-BLeXr_hR.mjs.map +1 -0
- package/dist/{src-D4rVJg36.cjs → src-BpAv-H0Z.cjs} +165 -32
- package/dist/src-BpAv-H0Z.cjs.map +1 -0
- package/package.json +2 -1
- package/dist/src-CklEIXTX.mjs.map +0 -1
- package/dist/src-D4rVJg36.cjs.map +0 -1
|
@@ -33,7 +33,7 @@ node_path = __toESM(node_path, 1);
|
|
|
33
33
|
let yaml = require("yaml");
|
|
34
34
|
|
|
35
35
|
//#region package.json
|
|
36
|
-
var version = "0.
|
|
36
|
+
var version = "0.6.0";
|
|
37
37
|
|
|
38
38
|
//#endregion
|
|
39
39
|
//#region ../core/src/project-info/ignore.ts
|
|
@@ -89,20 +89,66 @@ const createIgnoreMatcher = (patterns) => {
|
|
|
89
89
|
|
|
90
90
|
//#endregion
|
|
91
91
|
//#region ../core/src/project-info/discover.ts
|
|
92
|
-
const
|
|
93
|
-
|
|
92
|
+
const PRUNED_DIRECTORIES = [
|
|
93
|
+
".git",
|
|
94
|
+
".hg",
|
|
95
|
+
".svn",
|
|
96
|
+
"node_modules",
|
|
97
|
+
"bower_components",
|
|
98
|
+
".pnpm-store",
|
|
99
|
+
".yarn",
|
|
100
|
+
".venv",
|
|
101
|
+
"venv",
|
|
102
|
+
"__pycache__",
|
|
103
|
+
".tox",
|
|
104
|
+
".mypy_cache",
|
|
105
|
+
".pytest_cache",
|
|
106
|
+
".ruff_cache",
|
|
107
|
+
".gradle",
|
|
108
|
+
".turbo",
|
|
109
|
+
".next",
|
|
110
|
+
".nuxt",
|
|
111
|
+
".svelte-kit",
|
|
112
|
+
".parcel-cache",
|
|
113
|
+
".cache",
|
|
114
|
+
"coverage",
|
|
115
|
+
"dist"
|
|
116
|
+
];
|
|
117
|
+
const PRUNED = new Set(PRUNED_DIRECTORIES);
|
|
118
|
+
const toPosixRelative = (rootDir, file) => node_path.default.relative(rootDir, file).split(node_path.default.sep).join("/");
|
|
119
|
+
const SUBTREE_SUFFIX = "/**";
|
|
120
|
+
const createPrunedDirectoryMatcher = (patterns = []) => createIgnoreMatcher(patterns.filter((pattern) => pattern.endsWith(SUBTREE_SUFFIX)).map((pattern) => pattern.slice(0, -3)));
|
|
121
|
+
const isPermissionError = (error) => error instanceof Error && "code" in error && (error.code === "EACCES" || error.code === "EPERM");
|
|
122
|
+
const walk = async (dir, options, fileList = []) => {
|
|
123
|
+
let files;
|
|
124
|
+
try {
|
|
125
|
+
files = await node_fs_promises.default.readdir(dir, { withFileTypes: true });
|
|
126
|
+
} catch (error) {
|
|
127
|
+
if (dir !== options.rootDir && isPermissionError(error)) {
|
|
128
|
+
options.onSkippedDirectory?.(toPosixRelative(options.rootDir, dir));
|
|
129
|
+
return fileList;
|
|
130
|
+
}
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
94
133
|
await Promise.all(files.map(async (file) => {
|
|
95
134
|
const filePath = node_path.default.join(dir, file.name);
|
|
135
|
+
if (file.isSymbolicLink()) {
|
|
136
|
+
fileList.push(filePath);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
96
139
|
if (file.isDirectory()) {
|
|
97
|
-
if (
|
|
98
|
-
await walk(filePath, fileList);
|
|
140
|
+
if (PRUNED.has(file.name) || options.isPrunedByConfig(toPosixRelative(options.rootDir, filePath))) return;
|
|
141
|
+
await walk(filePath, options, fileList);
|
|
99
142
|
} else fileList.push(filePath);
|
|
100
143
|
}));
|
|
101
144
|
return fileList;
|
|
102
145
|
};
|
|
103
|
-
const toPosixRelative = (rootDir, file) => node_path.default.relative(rootDir, file).split(node_path.default.sep).join("/");
|
|
104
146
|
const discoverProject = async (rootDir, options) => {
|
|
105
|
-
const allFiles = await walk(rootDir
|
|
147
|
+
const allFiles = await walk(rootDir, {
|
|
148
|
+
isPrunedByConfig: createPrunedDirectoryMatcher(options?.ignoreFiles),
|
|
149
|
+
onSkippedDirectory: options?.onSkippedDirectory,
|
|
150
|
+
rootDir
|
|
151
|
+
});
|
|
106
152
|
const dockerfiles = [];
|
|
107
153
|
const composeFiles = [];
|
|
108
154
|
const dockerignores = [];
|
|
@@ -287,7 +333,7 @@ const parseDockerfile = (content, file = "Dockerfile") => {
|
|
|
287
333
|
const lineNum = i + 1;
|
|
288
334
|
const insideHeredoc = state.heredocQueue.length > 0;
|
|
289
335
|
if (!insideHeredoc && trimmed.startsWith("#")) continue;
|
|
290
|
-
if (!
|
|
336
|
+
if (!insideHeredoc && trimmed === "") continue;
|
|
291
337
|
state.rawAccumulator.push(rawLine);
|
|
292
338
|
if (insideHeredoc) processHeredocLine(state, trimmed);
|
|
293
339
|
else processInstructionLine(state, trimmed, lineNum);
|
|
@@ -458,6 +504,13 @@ const createDiagnostic = (file, ruleKey, severity, message, help, line) => ({
|
|
|
458
504
|
severity
|
|
459
505
|
});
|
|
460
506
|
|
|
507
|
+
//#endregion
|
|
508
|
+
//#region ../core/src/rules/package-managers.ts
|
|
509
|
+
const APT_GET_INSTALL_RE = /\bapt-get(?:\s+-\S+)*\s+install\b/u;
|
|
510
|
+
const APT_GET_UPDATE_RE = /\bapt-get(?:\s+-\S+)*\s+update\b/u;
|
|
511
|
+
const hasAptGetInstall = (text) => APT_GET_INSTALL_RE.test(text);
|
|
512
|
+
const hasAptGetUpdate = (text) => APT_GET_UPDATE_RE.test(text);
|
|
513
|
+
|
|
461
514
|
//#endregion
|
|
462
515
|
//#region ../core/src/rules/best-practices.ts
|
|
463
516
|
const requireHealthcheck = {
|
|
@@ -473,6 +526,16 @@ const requireHealthcheck = {
|
|
|
473
526
|
key: "docker-doctor/require-healthcheck",
|
|
474
527
|
message: "Add a HEALTHCHECK instruction"
|
|
475
528
|
};
|
|
529
|
+
const AUTO_EXTRACT_SUFFIXES = [
|
|
530
|
+
".tar",
|
|
531
|
+
".tar.gz",
|
|
532
|
+
".tgz",
|
|
533
|
+
".tar.bz2",
|
|
534
|
+
".tbz2",
|
|
535
|
+
".tbz",
|
|
536
|
+
".tar.xz",
|
|
537
|
+
".txz"
|
|
538
|
+
];
|
|
476
539
|
const preferCopyOverAdd = {
|
|
477
540
|
category: "Best Practices",
|
|
478
541
|
check(instructions, file) {
|
|
@@ -481,7 +544,7 @@ const preferCopyOverAdd = {
|
|
|
481
544
|
const src = inst.args.split(/\s+/u).find((p) => !p.startsWith("--"));
|
|
482
545
|
if (!src) continue;
|
|
483
546
|
const isRemote = src.startsWith("http://") || src.startsWith("https://");
|
|
484
|
-
const isArchive =
|
|
547
|
+
const isArchive = AUTO_EXTRACT_SUFFIXES.some((suffix) => src.toLowerCase().endsWith(suffix));
|
|
485
548
|
if (!isRemote && !isArchive) diagnostics.push(createDiagnostic(file, this.key, this.defaultSeverity, `ADD instruction used for regular files: '${inst.args}'. COPY is simpler and less prone to magic side effects.`, this.help, inst.line));
|
|
486
549
|
}
|
|
487
550
|
return diagnostics;
|
|
@@ -519,8 +582,8 @@ const combineAptUpdateInstall = {
|
|
|
519
582
|
check(instructions, file) {
|
|
520
583
|
const diagnostics = [];
|
|
521
584
|
for (const inst of instructions) if (inst.instruction === "RUN") {
|
|
522
|
-
const hasUpdate = inst.args
|
|
523
|
-
const hasInstall = inst.args
|
|
585
|
+
const hasUpdate = hasAptGetUpdate(inst.args);
|
|
586
|
+
const hasInstall = hasAptGetInstall(inst.args);
|
|
524
587
|
if (hasUpdate && !hasInstall) diagnostics.push(createDiagnostic(file, this.key, this.defaultSeverity, "RUN apt-get update used without apt-get install in the same instruction. This can cause caching issues and build failures.", this.help, inst.line));
|
|
525
588
|
else if (hasInstall && !hasUpdate) diagnostics.push(createDiagnostic(file, this.key, this.defaultSeverity, "RUN apt-get install used without apt-get update in the same instruction. Always combine them to ensure up-to-date package installation.", this.help, inst.line));
|
|
526
589
|
}
|
|
@@ -600,18 +663,32 @@ const avoidRunCd = {
|
|
|
600
663
|
key: "docker-doctor/avoid-run-cd",
|
|
601
664
|
message: "Avoid changing directories with cd in RUN"
|
|
602
665
|
};
|
|
666
|
+
const PACKAGE_LIST_START_RE = /\b(?:apt-get(?:\s+-\S+)*\s+install|apk\s+add|yum\s+install|dnf\s+install)\b/u;
|
|
667
|
+
const SHELL_SEPARATOR_RE$1 = /[;|]|&&/u;
|
|
668
|
+
const WHITESPACE_RE$1 = /\s+/u;
|
|
669
|
+
const collectPackageList = (raw) => {
|
|
670
|
+
const start = PACKAGE_LIST_START_RE.exec(raw);
|
|
671
|
+
if (!start) return [];
|
|
672
|
+
const packages = [];
|
|
673
|
+
const tokens = raw.slice(start.index + start[0].length).split(WHITESPACE_RE$1);
|
|
674
|
+
for (const token of tokens) {
|
|
675
|
+
if (SHELL_SEPARATOR_RE$1.test(token)) break;
|
|
676
|
+
if (token !== "" && token !== "\\" && !token.startsWith("-")) packages.push(token);
|
|
677
|
+
}
|
|
678
|
+
return packages;
|
|
679
|
+
};
|
|
603
680
|
const sortMultilineArgs = {
|
|
604
681
|
category: "Best Practices",
|
|
605
682
|
check(instructions, file) {
|
|
606
683
|
const diagnostics = [];
|
|
607
684
|
for (const inst of instructions) if (inst.instruction === "RUN") {
|
|
608
685
|
const { raw } = inst;
|
|
609
|
-
const isPackageInstall = raw
|
|
686
|
+
const isPackageInstall = hasAptGetInstall(raw) || raw.includes("apk add") || raw.includes("yum install") || raw.includes("dnf install");
|
|
610
687
|
const hasContinuation = raw.includes("\\\n") || raw.includes("\\\r\n");
|
|
611
688
|
if (isPackageInstall && hasContinuation) {
|
|
612
|
-
const packages = raw
|
|
689
|
+
const packages = collectPackageList(raw);
|
|
613
690
|
if (packages.length > 1) {
|
|
614
|
-
const sorted = packages.toSorted(
|
|
691
|
+
const sorted = packages.toSorted();
|
|
615
692
|
if (!packages.every((val, idx) => val === sorted[idx])) diagnostics.push(createDiagnostic(file, this.key, this.defaultSeverity, "Multi-line package arguments are not sorted alphanumerically. Keeping them sorted makes maintenance easier and prevents duplicates.", this.help, inst.line));
|
|
616
693
|
}
|
|
617
694
|
}
|
|
@@ -873,7 +950,10 @@ const composeModelRules = [undefinedModelReference, pinModelVersion];
|
|
|
873
950
|
const INTERPOLATION_WITH_DEFAULT = /\$\{[^}:?-]+:?-(?<fallback>[^}]*)\}/gu;
|
|
874
951
|
const INTERPOLATION_WITHOUT_DEFAULT = /\$\{[^}]*\}|\$[A-Za-z_][A-Za-z0-9_]*/gu;
|
|
875
952
|
const HOME_INTERPOLATION = /\$\{HOME\}|\$HOME(?![A-Za-z0-9_])/gu;
|
|
876
|
-
const
|
|
953
|
+
const PWD_INTERPOLATION = /\$\{PWD\}|\$PWD(?![A-Za-z0-9_])/gu;
|
|
954
|
+
const LEADING_INTERPOLATION_RE = /^\$(?:\{[^}]*\}|[A-Za-z_][A-Za-z0-9_]*)/u;
|
|
955
|
+
const applyFoldsAndDefaults = (value) => value.replace(HOME_INTERPOLATION, "~").replace(PWD_INTERPOLATION, ".").replace(INTERPOLATION_WITH_DEFAULT, (_match, fallback) => fallback);
|
|
956
|
+
const resolveInterpolationDefaults = (value) => applyFoldsAndDefaults(value).replace(INTERPOLATION_WITHOUT_DEFAULT, "");
|
|
877
957
|
const PATH_SHAPED = /^(?:[/.~]|[A-Za-z]:)/u;
|
|
878
958
|
const splitShortSyntax = (spec) => {
|
|
879
959
|
const parts = [];
|
|
@@ -924,12 +1004,16 @@ const volumeMount = (volume) => {
|
|
|
924
1004
|
* The host path a bind mount reads from, with interpolation defaults
|
|
925
1005
|
* applied and Windows separators folded to `/`. Undefined when the entry is
|
|
926
1006
|
* not a bind mount (named volume, tmpfs, anonymous volume) or when the
|
|
927
|
-
* source
|
|
1007
|
+
* source starts with an interpolation that has no default. A source such as
|
|
1008
|
+
* `${DATA_DIR}/pg` names no known host location, so no rule can say whether
|
|
1009
|
+
* it is inside the project.
|
|
928
1010
|
*/
|
|
929
1011
|
const bindMountSource = (mount) => {
|
|
930
1012
|
if (mount.type !== void 0 && mount.type !== "bind") return;
|
|
931
1013
|
if (!mount.source) return;
|
|
932
|
-
const
|
|
1014
|
+
const withDefaults = applyFoldsAndDefaults(mount.source);
|
|
1015
|
+
if (LEADING_INTERPOLATION_RE.test(withDefaults)) return;
|
|
1016
|
+
const resolved = withDefaults.replace(INTERPOLATION_WITHOUT_DEFAULT, "").replaceAll("\\", "/");
|
|
933
1017
|
if (resolved === "" || !PATH_SHAPED.test(resolved)) return;
|
|
934
1018
|
return resolved;
|
|
935
1019
|
};
|
|
@@ -937,15 +1021,16 @@ const isDockerSocketPath = (hostPath) => hostPath.endsWith("/docker.sock") || ho
|
|
|
937
1021
|
const DOCKER_SOCKET_TARGET = "/var/run/docker.sock";
|
|
938
1022
|
/**
|
|
939
1023
|
* Whether the entry bind-mounts the Docker socket, under any of its host
|
|
940
|
-
* spellings. A
|
|
941
|
-
* still
|
|
1024
|
+
* spellings. A source that starts with `${VAR}` names no known host path,
|
|
1025
|
+
* but a `/docker.sock` suffix or a socket target still identifies it.
|
|
942
1026
|
*/
|
|
943
1027
|
const mountsDockerSocket = (mount) => {
|
|
944
1028
|
const source = bindMountSource(mount);
|
|
945
1029
|
if (source !== void 0) return isDockerSocketPath(source);
|
|
946
1030
|
if (mount.type !== void 0 && mount.type !== "bind") return false;
|
|
947
1031
|
if (!mount.source) return false;
|
|
948
|
-
|
|
1032
|
+
const remainder = resolveInterpolationDefaults(mount.source);
|
|
1033
|
+
return isDockerSocketPath(remainder) || remainder === "" && resolveInterpolationDefaults(mount.target ?? "") === DOCKER_SOCKET_TARGET;
|
|
949
1034
|
};
|
|
950
1035
|
const WINDOWS_DRIVE_ROOT = /^[A-Za-z]:\/?$/u;
|
|
951
1036
|
const WINDOWS_DRIVE_PREFIX = /^[A-Za-z]:/u;
|
|
@@ -995,7 +1080,11 @@ const SECRET_KEY_PATTERNS = [
|
|
|
995
1080
|
];
|
|
996
1081
|
const isSecretKey = (key) => SECRET_KEY_PATTERNS.some((regex) => regex.test(key));
|
|
997
1082
|
const CREDENTIAL_FREE_URL = /^[a-z][a-z0-9+.-]*:\/\/[^@/\s]*(?:\/|$)/iu;
|
|
998
|
-
const
|
|
1083
|
+
const SURROUNDING_QUOTES_RE = /^(?<quote>["'])(?<inner>.*)\k<quote>$/su;
|
|
1084
|
+
const isLiteralSecretValue = (value) => {
|
|
1085
|
+
const unquoted = value.match(SURROUNDING_QUOTES_RE)?.groups?.inner ?? value;
|
|
1086
|
+
return unquoted.length > 0 && !unquoted.startsWith("$") && !CREDENTIAL_FREE_URL.test(unquoted);
|
|
1087
|
+
};
|
|
999
1088
|
|
|
1000
1089
|
//#endregion
|
|
1001
1090
|
//#region ../core/src/rules/compose-security.ts
|
|
@@ -1102,6 +1191,10 @@ const preferReadOnlyBindMount = {
|
|
|
1102
1191
|
key: "docker-doctor/prefer-read-only-bind-mount",
|
|
1103
1192
|
message: "Bind mounts outside the project should be read-only"
|
|
1104
1193
|
};
|
|
1194
|
+
const literalValue = (value) => {
|
|
1195
|
+
if (typeof value === "string") return value;
|
|
1196
|
+
if (typeof value === "number") return String(value);
|
|
1197
|
+
};
|
|
1105
1198
|
const noPlaintextSecrets = {
|
|
1106
1199
|
category: "Compose",
|
|
1107
1200
|
check(composeContent, file, context) {
|
|
@@ -1117,15 +1210,16 @@ const noPlaintextSecrets = {
|
|
|
1117
1210
|
if (eqIndex <= 0) continue;
|
|
1118
1211
|
const key = entry.slice(0, eqIndex);
|
|
1119
1212
|
const value = entry.slice(eqIndex + 1);
|
|
1120
|
-
if (isSecretKey(key) &&
|
|
1213
|
+
if (isSecretKey(key) && isLiteralSecretValue(value)) flag(name, key, context?.locate?.([
|
|
1121
1214
|
"services",
|
|
1122
1215
|
name,
|
|
1123
1216
|
"environment",
|
|
1124
1217
|
index
|
|
1125
1218
|
]));
|
|
1126
1219
|
}
|
|
1127
|
-
else if (environment && typeof environment === "object") {
|
|
1128
|
-
|
|
1220
|
+
else if (environment && typeof environment === "object") for (const [key, value] of Object.entries(environment)) {
|
|
1221
|
+
const literal = literalValue(value);
|
|
1222
|
+
if (literal !== void 0 && isSecretKey(key) && isLiteralSecretValue(literal)) flag(name, key, context?.locate?.([
|
|
1129
1223
|
"services",
|
|
1130
1224
|
name,
|
|
1131
1225
|
"environment",
|
|
@@ -1185,7 +1279,7 @@ const cleanPackageCache = {
|
|
|
1185
1279
|
const diagnostics = [];
|
|
1186
1280
|
for (const inst of instructions) if (inst.instruction === "RUN") {
|
|
1187
1281
|
const { args } = inst;
|
|
1188
|
-
if (args
|
|
1282
|
+
if (hasAptGetInstall(args) && !args.includes("rm -rf /var/lib/apt/lists") && !hasCacheMountFor(args, APT_CACHE_DIRS)) diagnostics.push(createDiagnostic(file, this.key, this.defaultSeverity, `Running 'apt-get install' without removing package lists afterwards. This keeps metadata caches inside the image layer.`, this.help, inst.line));
|
|
1189
1283
|
if (args.includes("apk add") && !args.includes("--no-cache") && !args.includes("rm -rf /var/cache/apk") && !hasCacheMountFor(args, APK_CACHE_DIRS)) diagnostics.push(createDiagnostic(file, this.key, this.defaultSeverity, `Running 'apk add' without '--no-cache' or cleaning the apk cache. This increases layer size.`, this.help, inst.line));
|
|
1190
1284
|
}
|
|
1191
1285
|
return diagnostics;
|
|
@@ -1195,7 +1289,40 @@ const cleanPackageCache = {
|
|
|
1195
1289
|
key: "docker-doctor/clean-package-cache",
|
|
1196
1290
|
message: "Clean up package manager cache in the same RUN layer"
|
|
1197
1291
|
};
|
|
1198
|
-
const
|
|
1292
|
+
const SHELL_SEPARATOR_RE = /\s*(?:&&|\|\||;|\|)\s*/u;
|
|
1293
|
+
const WHITESPACE_RE = /\s+/u;
|
|
1294
|
+
const ENV_ASSIGNMENT_RE = /^[A-Za-z_][A-Za-z0-9_]*=/u;
|
|
1295
|
+
const NODE_PACKAGE_MANAGERS = /* @__PURE__ */ new Set([
|
|
1296
|
+
"npm",
|
|
1297
|
+
"yarn",
|
|
1298
|
+
"pnpm",
|
|
1299
|
+
"bun"
|
|
1300
|
+
]);
|
|
1301
|
+
const INSTALL_VERBS = /* @__PURE__ */ new Set([
|
|
1302
|
+
"install",
|
|
1303
|
+
"ci",
|
|
1304
|
+
"i"
|
|
1305
|
+
]);
|
|
1306
|
+
const NON_DEV_INSTALL_FLAGS = /* @__PURE__ */ new Set([
|
|
1307
|
+
"-g",
|
|
1308
|
+
"--global",
|
|
1309
|
+
"--production",
|
|
1310
|
+
"--omit=dev",
|
|
1311
|
+
"--only=prod",
|
|
1312
|
+
"--only=production",
|
|
1313
|
+
"--prod",
|
|
1314
|
+
"-P",
|
|
1315
|
+
"-p"
|
|
1316
|
+
]);
|
|
1317
|
+
const segmentInstallsDevDependencies = (segment) => {
|
|
1318
|
+
const tokens = segment.split(WHITESPACE_RE).filter(Boolean);
|
|
1319
|
+
const commandStart = tokens.findIndex((token) => !ENV_ASSIGNMENT_RE.test(token));
|
|
1320
|
+
if (commandStart === -1) return false;
|
|
1321
|
+
const [manager, verb, ...options] = tokens.slice(commandStart);
|
|
1322
|
+
if (!(NODE_PACKAGE_MANAGERS.has(manager) && INSTALL_VERBS.has(verb))) return false;
|
|
1323
|
+
return !tokens.slice(0, commandStart).includes("NODE_ENV=production") && options.every((option) => option.startsWith("-") && !NON_DEV_INSTALL_FLAGS.has(option));
|
|
1324
|
+
};
|
|
1325
|
+
const installsDevDependencies = (args) => !args.includes("prune") && maskQuotedText(args).split(SHELL_SEPARATOR_RE).some(segmentInstallsDevDependencies);
|
|
1199
1326
|
const avoidDevDependencies = {
|
|
1200
1327
|
category: "Image Size",
|
|
1201
1328
|
check(instructions, file) {
|
|
@@ -1241,11 +1368,12 @@ const imageSizeRules = [
|
|
|
1241
1368
|
|
|
1242
1369
|
//#endregion
|
|
1243
1370
|
//#region ../core/src/rules/performance.ts
|
|
1371
|
+
const BUILD_TOOL_COMMAND_RE = /(?:^|&&|\|\||;|\||\(|`|\$\()\s*(?:make|cmake)(?=\s|$)/u;
|
|
1244
1372
|
const useMultiStage = {
|
|
1245
1373
|
category: "Performance",
|
|
1246
1374
|
check(instructions, file) {
|
|
1247
1375
|
if (instructions.filter((inst) => inst.instruction === "FROM").length === 1) {
|
|
1248
|
-
if (instructions.some((inst) => inst.instruction === "RUN" && (inst.args.includes("npm run build") || inst.args.includes("yarn build") || inst.args.includes("bun run build") || inst.args.includes("cargo build") || inst.args
|
|
1376
|
+
if (instructions.some((inst) => inst.instruction === "RUN" && (inst.args.includes("npm run build") || inst.args.includes("yarn build") || inst.args.includes("bun run build") || inst.args.includes("cargo build") || BUILD_TOOL_COMMAND_RE.test(maskQuotedText(inst.args))))) return [createDiagnostic(file, this.key, this.defaultSeverity, "Only one build stage (FROM) was detected, but build instructions were found. Multi-stage builds can significantly reduce final image size.", this.help, instructions.find((inst) => inst.instruction === "FROM")?.line || 1)];
|
|
1249
1377
|
}
|
|
1250
1378
|
return [];
|
|
1251
1379
|
},
|
|
@@ -1304,7 +1432,8 @@ const hasDockerignoreFor = (dockerfilePath, projectFiles) => {
|
|
|
1304
1432
|
const lastSlash = dockerfilePath.lastIndexOf("/");
|
|
1305
1433
|
const dir = lastSlash === -1 ? "" : dockerfilePath.slice(0, lastSlash);
|
|
1306
1434
|
const adjacent = dir === "" ? ".dockerignore" : `${dir}/.dockerignore`;
|
|
1307
|
-
|
|
1435
|
+
const perDockerfile = `${dockerfilePath}.dockerignore`;
|
|
1436
|
+
return projectFiles.some((f) => f === adjacent || f === perDockerfile || f === ".dockerignore");
|
|
1308
1437
|
};
|
|
1309
1438
|
const useDockerignore = {
|
|
1310
1439
|
category: "Performance",
|
|
@@ -1716,13 +1845,13 @@ const calculateScore = (diagnostics) => {
|
|
|
1716
1845
|
|
|
1717
1846
|
//#endregion
|
|
1718
1847
|
//#region ../core/src/report.ts
|
|
1719
|
-
const REPORT_SCHEMA_VERSION =
|
|
1848
|
+
const REPORT_SCHEMA_VERSION = 4;
|
|
1720
1849
|
const resolveCategory = (diagnostic) => {
|
|
1721
1850
|
const category = diagnostic.category ?? findRule(diagnostic.rule)?.category;
|
|
1722
1851
|
if (!category) throw new Error(`Diagnostic for unknown rule "${diagnostic.rule}" has no category`);
|
|
1723
1852
|
return category;
|
|
1724
1853
|
};
|
|
1725
|
-
const toJsonReport = (diagnostics, score, label, project) => ({
|
|
1854
|
+
const toJsonReport = (diagnostics, score, label, project, failures = []) => ({
|
|
1726
1855
|
diagnostics: diagnostics.map((d) => ({
|
|
1727
1856
|
category: resolveCategory(d),
|
|
1728
1857
|
column: d.column,
|
|
@@ -1733,9 +1862,13 @@ const toJsonReport = (diagnostics, score, label, project) => ({
|
|
|
1733
1862
|
rule: d.rule,
|
|
1734
1863
|
severity: d.severity
|
|
1735
1864
|
})),
|
|
1865
|
+
failures: failures.map(({ file, message }) => ({
|
|
1866
|
+
file,
|
|
1867
|
+
message
|
|
1868
|
+
})),
|
|
1736
1869
|
label,
|
|
1737
1870
|
project,
|
|
1738
|
-
schemaVersion:
|
|
1871
|
+
schemaVersion: 4,
|
|
1739
1872
|
score,
|
|
1740
1873
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1741
1874
|
});
|
|
@@ -1819,4 +1952,4 @@ Object.defineProperty(exports, 'version', {
|
|
|
1819
1952
|
return version;
|
|
1820
1953
|
}
|
|
1821
1954
|
});
|
|
1822
|
-
//# sourceMappingURL=src-
|
|
1955
|
+
//# sourceMappingURL=src-BpAv-H0Z.cjs.map
|