diff --git a/package-lock.json b/package-lock.json index 5a2575d73..8dea7dbc3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,8 +33,7 @@ "vscode-material-icons": "^0.1.1", "yaml": "^2.5.1", "yargs": "^17.7.2", - "zod": "^3.23.8", - "zod-validation-error": "^3.4.0" + "zod": "^4.0.5" }, "devDependencies": { "@beaussan/nx-knip": "^0.0.5-15", @@ -105,7 +104,7 @@ "verdaccio": "^5.32.2", "vite": "^5.4.8", "vitest": "1.3.1", - "zod2md": "^0.1.7" + "zod2md": "^0.2.4" }, "engines": { "node": ">=22.14" @@ -3011,9 +3010,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.2.tgz", - "integrity": "sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.6.tgz", + "integrity": "sha512-IjA+DcwoVpjEvyxZddDqBY+uJ2Snc6duLpjmkXm/v4xuS3H+3FkLZlDm9ZsAbF9rsfP3zeA0/ArNDORZgrxR/Q==", "cpu": [ "arm64" ], @@ -3074,6 +3073,22 @@ "node": ">=18" } }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.6.tgz", + "integrity": "sha512-+SqBcAWoB1fYKmpWoQP4pGtx+pUUC//RNYhFdbcSA16617cchuryuhOCRpPsjCblKukAckWsV+aQ3UKT/RMPcA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/sunos-x64": { "version": "0.23.1", "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.1.tgz", @@ -3123,9 +3138,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.2.tgz", - "integrity": "sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.6.tgz", + "integrity": "sha512-NgJPHHbEpLQgDH2MjQu90pzW/5vvXIZ7KOnPyNBm92A6WgZ/7b6fJyUBjoumLqeOQQGqY2QjQxRo97ah4Sj0cA==", "cpu": [ "x64" ], @@ -11600,6 +11615,15 @@ "devtools-protocol": "*" } }, + "node_modules/chromium-bidi/node_modules/zod": { + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/chromium/node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -13923,9 +13947,9 @@ } }, "node_modules/esbuild": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.2.tgz", - "integrity": "sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.6.tgz", + "integrity": "sha512-GVuzuUwtdsghE3ocJ9Bs8PNoF13HNQ5TXbEi2AhvVb8xU1Iwt9Fos9FEamfoee+u/TOsn7GUWc04lz46n2bbTg==", "hasInstallScript": true, "license": "MIT", "bin": { @@ -13935,37 +13959,38 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.2", - "@esbuild/android-arm": "0.25.2", - "@esbuild/android-arm64": "0.25.2", - "@esbuild/android-x64": "0.25.2", - "@esbuild/darwin-arm64": "0.25.2", - "@esbuild/darwin-x64": "0.25.2", - "@esbuild/freebsd-arm64": "0.25.2", - "@esbuild/freebsd-x64": "0.25.2", - "@esbuild/linux-arm": "0.25.2", - "@esbuild/linux-arm64": "0.25.2", - "@esbuild/linux-ia32": "0.25.2", - "@esbuild/linux-loong64": "0.25.2", - "@esbuild/linux-mips64el": "0.25.2", - "@esbuild/linux-ppc64": "0.25.2", - "@esbuild/linux-riscv64": "0.25.2", - "@esbuild/linux-s390x": "0.25.2", - "@esbuild/linux-x64": "0.25.2", - "@esbuild/netbsd-arm64": "0.25.2", - "@esbuild/netbsd-x64": "0.25.2", - "@esbuild/openbsd-arm64": "0.25.2", - "@esbuild/openbsd-x64": "0.25.2", - "@esbuild/sunos-x64": "0.25.2", - "@esbuild/win32-arm64": "0.25.2", - "@esbuild/win32-ia32": "0.25.2", - "@esbuild/win32-x64": "0.25.2" + "@esbuild/aix-ppc64": "0.25.6", + "@esbuild/android-arm": "0.25.6", + "@esbuild/android-arm64": "0.25.6", + "@esbuild/android-x64": "0.25.6", + "@esbuild/darwin-arm64": "0.25.6", + "@esbuild/darwin-x64": "0.25.6", + "@esbuild/freebsd-arm64": "0.25.6", + "@esbuild/freebsd-x64": "0.25.6", + "@esbuild/linux-arm": "0.25.6", + "@esbuild/linux-arm64": "0.25.6", + "@esbuild/linux-ia32": "0.25.6", + "@esbuild/linux-loong64": "0.25.6", + "@esbuild/linux-mips64el": "0.25.6", + "@esbuild/linux-ppc64": "0.25.6", + "@esbuild/linux-riscv64": "0.25.6", + "@esbuild/linux-s390x": "0.25.6", + "@esbuild/linux-x64": "0.25.6", + "@esbuild/netbsd-arm64": "0.25.6", + "@esbuild/netbsd-x64": "0.25.6", + "@esbuild/openbsd-arm64": "0.25.6", + "@esbuild/openbsd-x64": "0.25.6", + "@esbuild/openharmony-arm64": "0.25.6", + "@esbuild/sunos-x64": "0.25.6", + "@esbuild/win32-arm64": "0.25.6", + "@esbuild/win32-ia32": "0.25.6", + "@esbuild/win32-x64": "0.25.6" } }, "node_modules/esbuild/node_modules/@esbuild/aix-ppc64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.2.tgz", - "integrity": "sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.6.tgz", + "integrity": "sha512-ShbM/3XxwuxjFiuVBHA+d3j5dyac0aEVVq1oluIDf71hUw0aRF59dV/efUsIwFnR6m8JNM2FjZOzmaZ8yG61kw==", "cpu": [ "ppc64" ], @@ -13979,9 +14004,9 @@ } }, "node_modules/esbuild/node_modules/@esbuild/android-arm": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.2.tgz", - "integrity": "sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.6.tgz", + "integrity": "sha512-S8ToEOVfg++AU/bHwdksHNnyLyVM+eMVAOf6yRKFitnwnbwwPNqKr3srzFRe7nzV69RQKb5DgchIX5pt3L53xg==", "cpu": [ "arm" ], @@ -13995,9 +14020,9 @@ } }, "node_modules/esbuild/node_modules/@esbuild/android-arm64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.2.tgz", - "integrity": "sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.6.tgz", + "integrity": "sha512-hd5zdUarsK6strW+3Wxi5qWws+rJhCCbMiC9QZyzoxfk5uHRIE8T287giQxzVpEvCwuJ9Qjg6bEjcRJcgfLqoA==", "cpu": [ "arm64" ], @@ -14011,9 +14036,9 @@ } }, "node_modules/esbuild/node_modules/@esbuild/android-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.2.tgz", - "integrity": "sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.6.tgz", + "integrity": "sha512-0Z7KpHSr3VBIO9A/1wcT3NTy7EB4oNC4upJ5ye3R7taCc2GUdeynSLArnon5G8scPwaU866d3H4BCrE5xLW25A==", "cpu": [ "x64" ], @@ -14027,9 +14052,9 @@ } }, "node_modules/esbuild/node_modules/@esbuild/darwin-arm64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.2.tgz", - "integrity": "sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.6.tgz", + "integrity": "sha512-FFCssz3XBavjxcFxKsGy2DYK5VSvJqa6y5HXljKzhRZ87LvEi13brPrf/wdyl/BbpbMKJNOr1Sd0jtW4Ge1pAA==", "cpu": [ "arm64" ], @@ -14043,9 +14068,9 @@ } }, "node_modules/esbuild/node_modules/@esbuild/darwin-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.2.tgz", - "integrity": "sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.6.tgz", + "integrity": "sha512-GfXs5kry/TkGM2vKqK2oyiLFygJRqKVhawu3+DOCk7OxLy/6jYkWXhlHwOoTb0WqGnWGAS7sooxbZowy+pK9Yg==", "cpu": [ "x64" ], @@ -14059,9 +14084,9 @@ } }, "node_modules/esbuild/node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.2.tgz", - "integrity": "sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.6.tgz", + "integrity": "sha512-aoLF2c3OvDn2XDTRvn8hN6DRzVVpDlj2B/F66clWd/FHLiHaG3aVZjxQX2DYphA5y/evbdGvC6Us13tvyt4pWg==", "cpu": [ "arm64" ], @@ -14075,9 +14100,9 @@ } }, "node_modules/esbuild/node_modules/@esbuild/freebsd-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.2.tgz", - "integrity": "sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.6.tgz", + "integrity": "sha512-2SkqTjTSo2dYi/jzFbU9Plt1vk0+nNg8YC8rOXXea+iA3hfNJWebKYPs3xnOUf9+ZWhKAaxnQNUf2X9LOpeiMQ==", "cpu": [ "x64" ], @@ -14091,9 +14116,9 @@ } }, "node_modules/esbuild/node_modules/@esbuild/linux-arm": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.2.tgz", - "integrity": "sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.6.tgz", + "integrity": "sha512-SZHQlzvqv4Du5PrKE2faN0qlbsaW/3QQfUUc6yO2EjFcA83xnwm91UbEEVx4ApZ9Z5oG8Bxz4qPE+HFwtVcfyw==", "cpu": [ "arm" ], @@ -14107,9 +14132,9 @@ } }, "node_modules/esbuild/node_modules/@esbuild/linux-arm64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.2.tgz", - "integrity": "sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.6.tgz", + "integrity": "sha512-b967hU0gqKd9Drsh/UuAm21Khpoh6mPBSgz8mKRq4P5mVK8bpA+hQzmm/ZwGVULSNBzKdZPQBRT3+WuVavcWsQ==", "cpu": [ "arm64" ], @@ -14123,9 +14148,9 @@ } }, "node_modules/esbuild/node_modules/@esbuild/linux-ia32": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.2.tgz", - "integrity": "sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.6.tgz", + "integrity": "sha512-aHWdQ2AAltRkLPOsKdi3xv0mZ8fUGPdlKEjIEhxCPm5yKEThcUjHpWB1idN74lfXGnZ5SULQSgtr5Qos5B0bPw==", "cpu": [ "ia32" ], @@ -14139,9 +14164,9 @@ } }, "node_modules/esbuild/node_modules/@esbuild/linux-loong64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.2.tgz", - "integrity": "sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.6.tgz", + "integrity": "sha512-VgKCsHdXRSQ7E1+QXGdRPlQ/e08bN6WMQb27/TMfV+vPjjTImuT9PmLXupRlC90S1JeNNW5lzkAEO/McKeJ2yg==", "cpu": [ "loong64" ], @@ -14155,9 +14180,9 @@ } }, "node_modules/esbuild/node_modules/@esbuild/linux-mips64el": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.2.tgz", - "integrity": "sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.6.tgz", + "integrity": "sha512-WViNlpivRKT9/py3kCmkHnn44GkGXVdXfdc4drNmRl15zVQ2+D2uFwdlGh6IuK5AAnGTo2qPB1Djppj+t78rzw==", "cpu": [ "mips64el" ], @@ -14171,9 +14196,9 @@ } }, "node_modules/esbuild/node_modules/@esbuild/linux-ppc64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.2.tgz", - "integrity": "sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.6.tgz", + "integrity": "sha512-wyYKZ9NTdmAMb5730I38lBqVu6cKl4ZfYXIs31Baf8aoOtB4xSGi3THmDYt4BTFHk7/EcVixkOV2uZfwU3Q2Jw==", "cpu": [ "ppc64" ], @@ -14187,9 +14212,9 @@ } }, "node_modules/esbuild/node_modules/@esbuild/linux-riscv64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.2.tgz", - "integrity": "sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.6.tgz", + "integrity": "sha512-KZh7bAGGcrinEj4qzilJ4hqTY3Dg2U82c8bv+e1xqNqZCrCyc+TL9AUEn5WGKDzm3CfC5RODE/qc96OcbIe33w==", "cpu": [ "riscv64" ], @@ -14203,9 +14228,9 @@ } }, "node_modules/esbuild/node_modules/@esbuild/linux-s390x": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.2.tgz", - "integrity": "sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.6.tgz", + "integrity": "sha512-9N1LsTwAuE9oj6lHMyyAM+ucxGiVnEqUdp4v7IaMmrwb06ZTEVCIs3oPPplVsnjPfyjmxwHxHMF8b6vzUVAUGw==", "cpu": [ "s390x" ], @@ -14219,9 +14244,9 @@ } }, "node_modules/esbuild/node_modules/@esbuild/linux-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.2.tgz", - "integrity": "sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.6.tgz", + "integrity": "sha512-A6bJB41b4lKFWRKNrWoP2LHsjVzNiaurf7wyj/XtFNTsnPuxwEBWHLty+ZE0dWBKuSK1fvKgrKaNjBS7qbFKig==", "cpu": [ "x64" ], @@ -14235,9 +14260,9 @@ } }, "node_modules/esbuild/node_modules/@esbuild/netbsd-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.2.tgz", - "integrity": "sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.6.tgz", + "integrity": "sha512-dUXuZr5WenIDlMHdMkvDc1FAu4xdWixTCRgP7RQLBOkkGgwuuzaGSYcOpW4jFxzpzL1ejb8yF620UxAqnBrR9g==", "cpu": [ "x64" ], @@ -14251,9 +14276,9 @@ } }, "node_modules/esbuild/node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.2.tgz", - "integrity": "sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.6.tgz", + "integrity": "sha512-l8ZCvXP0tbTJ3iaqdNf3pjaOSd5ex/e6/omLIQCVBLmHTlfXW3zAxQ4fnDmPLOB1x9xrcSi/xtCWFwCZRIaEwg==", "cpu": [ "arm64" ], @@ -14267,9 +14292,9 @@ } }, "node_modules/esbuild/node_modules/@esbuild/openbsd-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.2.tgz", - "integrity": "sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.6.tgz", + "integrity": "sha512-hKrmDa0aOFOr71KQ/19JC7az1P0GWtCN1t2ahYAf4O007DHZt/dW8ym5+CUdJhQ/qkZmI1HAF8KkJbEFtCL7gw==", "cpu": [ "x64" ], @@ -14283,9 +14308,9 @@ } }, "node_modules/esbuild/node_modules/@esbuild/sunos-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.2.tgz", - "integrity": "sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.6.tgz", + "integrity": "sha512-dyCGxv1/Br7MiSC42qinGL8KkG4kX0pEsdb0+TKhmJZgCUDBGmyo1/ArCjNGiOLiIAgdbWgmWgib4HoCi5t7kA==", "cpu": [ "x64" ], @@ -14299,9 +14324,9 @@ } }, "node_modules/esbuild/node_modules/@esbuild/win32-arm64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.2.tgz", - "integrity": "sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.6.tgz", + "integrity": "sha512-42QOgcZeZOvXfsCBJF5Afw73t4veOId//XD3i+/9gSkhSV6Gk3VPlWncctI+JcOyERv85FUo7RxuxGy+z8A43Q==", "cpu": [ "arm64" ], @@ -14315,9 +14340,9 @@ } }, "node_modules/esbuild/node_modules/@esbuild/win32-ia32": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.2.tgz", - "integrity": "sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.6.tgz", + "integrity": "sha512-4AWhgXmDuYN7rJI6ORB+uU9DHLq/erBbuMoAuB4VWJTu5KtCgcKYPynF0YI1VkBNuEfjNlLrFr9KZPJzrtLkrQ==", "cpu": [ "ia32" ], @@ -20537,6 +20562,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/knip/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/koa": { "version": "2.15.3", "resolved": "https://registry.npmjs.org/koa/-/koa-2.15.3.tgz", @@ -28362,42 +28397,44 @@ } }, "node_modules/zod": { - "version": "3.23.8", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", - "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.0.5.tgz", + "integrity": "sha512-/5UuuRPStvHXu7RS+gmvRf4NXrNxpSllGwDnCBcJZtQsKrviYXm54yDGV2KYNLT5kq0lHGcl7lqWJLgSaG+tgA==", + "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" } }, "node_modules/zod-validation-error": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-3.4.0.tgz", - "integrity": "sha512-ZOPR9SVY6Pb2qqO5XHt+MkkTRxGXb4EVtnjc9JpXUOtUB1T9Ru7mZOT361AN3MsetVe7R0a1KZshJDZdgp9miQ==", + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-3.5.3.tgz", + "integrity": "sha512-OT5Y8lbUadqVZCsnyFaTQ4/O2mys4tj7PqhdbBCp7McPwvIEKfPtdA6QfPeFQK2/Rz5LgwmAXRJTugBNBi0btw==", + "dev": true, "license": "MIT", "engines": { "node": ">=18.0.0" }, "peerDependencies": { - "zod": "^3.18.0" + "zod": "^3.25.0 || ^4.0.0" } }, "node_modules/zod2md": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/zod2md/-/zod2md-0.1.7.tgz", - "integrity": "sha512-a/qcON8dBmNpABt0O/8aKiD6bORuYf/d5cSlMq65VGuv7prhF0KnoUacr0V2Y/PMkM2cmd8oKhXPNv/UZqSRgg==", + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/zod2md/-/zod2md-0.2.4.tgz", + "integrity": "sha512-P+12EKfWquooGSlkZ2RatVX9O8rrF7BvM8vlsrohOgvFM2NeGIS0VZQbkGNGLRI8sp82KTxxWG7H7JhRZhzHFg==", "dev": true, "license": "MIT", "dependencies": { "@commander-js/extra-typings": "^12.0.0", - "bundle-require": "^4.0.2", + "bundle-require": "^5.1.0", "commander": "^12.0.0", - "esbuild": "^0.19.11" + "esbuild": "^0.25.4" }, "bin": { "zod2md": "dist/bin.js" }, "peerDependencies": { - "zod": "^3.22.0" + "zod": "^3.25.0 || ^4.0.0" } }, "node_modules/zod2md/node_modules/@commander-js/extra-typings": { @@ -28409,396 +28446,6 @@ "commander": "~12.1.0" } }, - "node_modules/zod2md/node_modules/@esbuild/aix-ppc64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", - "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/zod2md/node_modules/@esbuild/android-arm": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", - "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/zod2md/node_modules/@esbuild/android-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", - "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/zod2md/node_modules/@esbuild/android-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", - "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/zod2md/node_modules/@esbuild/darwin-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", - "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/zod2md/node_modules/@esbuild/freebsd-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", - "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/zod2md/node_modules/@esbuild/freebsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", - "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/zod2md/node_modules/@esbuild/linux-arm": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", - "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/zod2md/node_modules/@esbuild/linux-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", - "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/zod2md/node_modules/@esbuild/linux-ia32": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", - "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/zod2md/node_modules/@esbuild/linux-loong64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", - "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/zod2md/node_modules/@esbuild/linux-mips64el": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", - "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/zod2md/node_modules/@esbuild/linux-ppc64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", - "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/zod2md/node_modules/@esbuild/linux-riscv64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", - "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/zod2md/node_modules/@esbuild/linux-s390x": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", - "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/zod2md/node_modules/@esbuild/linux-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", - "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/zod2md/node_modules/@esbuild/netbsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", - "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/zod2md/node_modules/@esbuild/openbsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", - "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/zod2md/node_modules/@esbuild/sunos-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", - "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/zod2md/node_modules/@esbuild/win32-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", - "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/zod2md/node_modules/@esbuild/win32-ia32": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", - "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/zod2md/node_modules/@esbuild/win32-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", - "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/zod2md/node_modules/bundle-require": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-4.2.1.tgz", - "integrity": "sha512-7Q/6vkyYAwOmQNRw75x+4yRtZCZJXUDmHHlFdkiV0wgv/reNjtJwpu1jPJ0w2kbEpIM0uoKI3S4/f39dU7AjSA==", - "dev": true, - "license": "MIT", - "dependencies": { - "load-tsconfig": "^0.2.3" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "peerDependencies": { - "esbuild": ">=0.17" - } - }, "node_modules/zod2md/node_modules/commander": { "version": "12.1.0", "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", @@ -28807,45 +28454,6 @@ "engines": { "node": ">=18" } - }, - "node_modules/zod2md/node_modules/esbuild": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", - "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.19.12", - "@esbuild/android-arm": "0.19.12", - "@esbuild/android-arm64": "0.19.12", - "@esbuild/android-x64": "0.19.12", - "@esbuild/darwin-arm64": "0.19.12", - "@esbuild/darwin-x64": "0.19.12", - "@esbuild/freebsd-arm64": "0.19.12", - "@esbuild/freebsd-x64": "0.19.12", - "@esbuild/linux-arm": "0.19.12", - "@esbuild/linux-arm64": "0.19.12", - "@esbuild/linux-ia32": "0.19.12", - "@esbuild/linux-loong64": "0.19.12", - "@esbuild/linux-mips64el": "0.19.12", - "@esbuild/linux-ppc64": "0.19.12", - "@esbuild/linux-riscv64": "0.19.12", - "@esbuild/linux-s390x": "0.19.12", - "@esbuild/linux-x64": "0.19.12", - "@esbuild/netbsd-x64": "0.19.12", - "@esbuild/openbsd-x64": "0.19.12", - "@esbuild/sunos-x64": "0.19.12", - "@esbuild/win32-arm64": "0.19.12", - "@esbuild/win32-ia32": "0.19.12", - "@esbuild/win32-x64": "0.19.12" - } } } } diff --git a/package.json b/package.json index 4d85151ca..b4e776c66 100644 --- a/package.json +++ b/package.json @@ -46,8 +46,7 @@ "vscode-material-icons": "^0.1.1", "yaml": "^2.5.1", "yargs": "^17.7.2", - "zod": "^3.23.8", - "zod-validation-error": "^3.4.0" + "zod": "^4.0.5" }, "devDependencies": { "@beaussan/nx-knip": "^0.0.5-15", @@ -118,7 +117,7 @@ "verdaccio": "^5.32.2", "vite": "^5.4.8", "vitest": "1.3.1", - "zod2md": "^0.1.7" + "zod2md": "^0.2.4" }, "optionalDependencies": { "@esbuild/darwin-arm64": "^0.19.12", diff --git a/packages/ci/package.json b/packages/ci/package.json index ccd168dd9..de2c7dd14 100644 --- a/packages/ci/package.json +++ b/packages/ci/package.json @@ -31,6 +31,6 @@ "glob": "^11.0.1", "simple-git": "^3.20.0", "yaml": "^2.5.1", - "zod": "^3.22.1" + "zod": "^4.0.5" } } diff --git a/packages/ci/src/lib/cli/persist.unit.test.ts b/packages/ci/src/lib/cli/persist.unit.test.ts index 222824134..874b78258 100644 --- a/packages/ci/src/lib/cli/persist.unit.test.ts +++ b/packages/ci/src/lib/cli/persist.unit.test.ts @@ -114,7 +114,7 @@ describe('parsePersistConfig', () => { await expect( parsePersistConfig({ persist: { format: ['json', 'html'] } }), ).rejects.toThrow( - /^Invalid persist config - ZodError:.*Invalid enum value. Expected 'json' \| 'md', received 'html'/s, + /^Invalid persist config - ZodError:.*Invalid option: expected one of \\"json\\"\|\\"md\\"/s, ); }); }); diff --git a/packages/cli/src/lib/yargs-cli.ts b/packages/cli/src/lib/yargs-cli.ts index 989ca8d5d..8caafa30c 100644 --- a/packages/cli/src/lib/yargs-cli.ts +++ b/packages/cli/src/lib/yargs-cli.ts @@ -155,9 +155,9 @@ function validatePersistFormat(persist: PersistConfig) { return true; } catch { throw new Error( - `Invalid persist.format option. Valid options are: ${Object.values( - formatSchema.Values, - ).join(', ')}`, + `Invalid persist.format option. Valid options are: ${formatSchema.options.join( + ', ', + )}`, ); } } diff --git a/packages/core/src/lib/implementation/execute-plugin.unit.test.ts b/packages/core/src/lib/implementation/execute-plugin.unit.test.ts index c74a9e509..cc63bf82c 100644 --- a/packages/core/src/lib/implementation/execute-plugin.unit.test.ts +++ b/packages/core/src/lib/implementation/execute-plugin.unit.test.ts @@ -134,21 +134,11 @@ describe('executePlugins', () => { ] satisfies PluginConfig[], { progress: false }, ), - ).rejects.toThrow(`Executing 1 plugin failed.\n\nError: - Plugin ${bold( - title, - )} (${bold(slug)}) produced the following error: - - Audit output is invalid: [ - { - "validation": "regex", - "code": "invalid_string", - "message": "The slug has to follow the pattern [0-9a-z] followed by multiple optional groups of -[0-9a-z]. e.g. my-slug", - "path": [ - 0, - "slug" - ] - } -] -`); + ).rejects.toThrow( + `Executing 1 plugin failed.\n\nError: - Plugin ${bold( + title, + )} (${bold(slug)}) produced the following error:\n - Audit output is invalid`, + ); }); it('should throw for one failing plugin', async () => { diff --git a/packages/core/src/lib/implementation/read-rc-file.int.test.ts b/packages/core/src/lib/implementation/read-rc-file.int.test.ts index b903ab41e..dae34e77a 100644 --- a/packages/core/src/lib/implementation/read-rc-file.int.test.ts +++ b/packages/core/src/lib/implementation/read-rc-file.int.test.ts @@ -69,12 +69,12 @@ describe('readRcByPath', () => { it('should throw if the configuration is empty', async () => { await expect( readRcByPath(path.join(configDirPath, 'code-pushup.empty.config.js')), - ).rejects.toThrow(/invalid_type/); + ).rejects.toThrow('Invalid input'); }); it('should throw if the configuration is invalid', async () => { await expect( readRcByPath(path.join(configDirPath, 'code-pushup.invalid.config.ts')), - ).rejects.toThrow(/refs are duplicates/); + ).rejects.toThrow('has duplicate references'); }); }); diff --git a/packages/models/docs/models-reference.md b/packages/models/docs/models-reference.md index 7bdd9d232..599b04b25 100644 --- a/packages/models/docs/models-reference.md +++ b/packages/models/docs/models-reference.md @@ -5,7 +5,7 @@ _Union of the following possible types:_ - `string` (_min length: 1_) -- _Object with properties:_ +- _Object with properties:_ ## AuditDetails @@ -13,11 +13,11 @@ Detailed information _Object containing the following properties:_ -| Property | Description | Type | -| :------- | :------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `issues` | List of findings | _Array of [Issue](#issue) items_ | -| `table` | Table of related findings | _Object with properties:_ _or_ _Object with properties:_ | -| `trees` | Findings in tree structure | _Array of [Tree](#tree) items_ | +| Property | Description | Type | +| :------- | :------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `issues` | List of findings | _Array of [Issue](#issue) items_ | +| `table` | Table of related findings | _Object with properties:_ _or_ _Object with properties:_ | +| `trees` | Findings in tree structure | _Array of [Tree](#tree) items_ | _All properties are optional._ @@ -25,15 +25,15 @@ _All properties are optional._ _Object containing the following properties:_ -| Property | Description | Type | -| :----------------------- | :----------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **`slug`** (\*) | Unique ID (human-readable, URL-safe) | `string` (_regex: `/^[a-z\d]+(?:-[a-z\d]+)*$/`, max length: 128_) | -| **`title`** (\*) | Descriptive name | `string` (_max length: 256_) | -| `docsUrl` | Documentation site | `string` (_url_) (_optional_) _or_ `''` | -| **`scores`** (\*) | Score comparison | _Object with properties:_ | -| **`plugin`** (\*) | Plugin which defines it | _Object with properties:_ | -| **`values`** (\*) | Audit `value` comparison | _Object with properties:_ | -| **`displayValues`** (\*) | Audit `displayValue` comparison | _Object with properties:_ | +| Property | Description | Type | +| :----------------------- | :----------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **`slug`** (\*) | Unique ID (human-readable, URL-safe) | `string` (_regex: `/^[a-z\d]+(?:-[a-z\d]+)*$/`, max length: 128_) | +| **`title`** (\*) | Descriptive name | `string` (_max length: 256_) | +| `docsUrl` | Documentation site | `string` (_url_) (_optional_) _or_ `''` | +| **`scores`** (\*) | Score comparison | _Object with properties:_ | +| **`plugin`** (\*) | Plugin which defines it | _Object with properties:_ | +| **`values`** (\*) | Audit `value` comparison | _Object with properties:_ | +| **`displayValues`** (\*) | Audit `displayValue` comparison | _Object with properties:_ | _(\*) Required._ @@ -55,6 +55,8 @@ _(\*) Required._ ## AuditOutputs +List of JSON formatted audit output emitted by the runner process of a plugin + _Array of [AuditOutput](#auditoutput) items._ ## AuditReport @@ -79,15 +81,15 @@ _(\*) Required._ _Object containing the following properties:_ -| Property | Description | Type | -| :---------------- | :--------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **`slug`** (\*) | Unique ID (human-readable, URL-safe) | `string` (_regex: `/^[a-z\d]+(?:-[a-z\d]+)*$/`, max length: 128_) | -| **`title`** (\*) | Descriptive name | `string` (_max length: 256_) | -| `docsUrl` | Documentation site | `string` (_url_) (_optional_) _or_ `''` | -| **`plugin`** (\*) | Plugin which defines it | _Object with properties:_ | -| **`score`** (\*) | Value between 0 and 1 | `number` (_≥0, ≤1_) | -| **`value`** (\*) | Raw numeric value | `number` (_≥0_) | -| `displayValue` | Formatted value (e.g. '0.9 s', '2.1 MB') | `string` | +| Property | Description | Type | +| :---------------- | :--------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **`slug`** (\*) | Unique ID (human-readable, URL-safe) | `string` (_regex: `/^[a-z\d]+(?:-[a-z\d]+)*$/`, max length: 128_) | +| **`title`** (\*) | Descriptive name | `string` (_max length: 256_) | +| `docsUrl` | Documentation site | `string` (_url_) (_optional_) _or_ `''` | +| **`plugin`** (\*) | Plugin which defines it | _Object with properties:_ | +| **`score`** (\*) | Value between 0 and 1 | `number` (_≥0, ≤1_) | +| **`value`** (\*) | Raw numeric value | `number` (_≥0_) | +| `displayValue` | Formatted value (e.g. '0.9 s', '2.1 MB') | `string` | _(\*) Required._ @@ -151,12 +153,12 @@ _(\*) Required._ _Object containing the following properties:_ -| Property | Description | Type | -| :---------------- | :----------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **`slug`** (\*) | Unique ID (human-readable, URL-safe) | `string` (_regex: `/^[a-z\d]+(?:-[a-z\d]+)*$/`, max length: 128_) | -| **`title`** (\*) | Descriptive name | `string` (_max length: 256_) | -| `docsUrl` | Documentation site | `string` (_url_) (_optional_) _or_ `''` | -| **`scores`** (\*) | Score comparison | _Object with properties:_ | +| Property | Description | Type | +| :---------------- | :----------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| **`slug`** (\*) | Unique ID (human-readable, URL-safe) | `string` (_regex: `/^[a-z\d]+(?:-[a-z\d]+)*$/`, max length: 128_) | +| **`title`** (\*) | Descriptive name | `string` (_max length: 256_) | +| `docsUrl` | Documentation site | `string` (_url_) (_optional_) _or_ `''` | +| **`scores`** (\*) | Score comparison | _Object with properties:_ | _(\*) Required._ @@ -196,7 +198,7 @@ _Object containing the following properties:_ | :----------------- | :------------------------------------- | :------------------------------------ | | **`hash`** (\*) | Commit SHA (full) | `string` (_regex: `/^[\da-f]{40}$/`_) | | **`message`** (\*) | Commit message | `string` | -| **`date`** (\*) | Date and time when commit was authored | `Date` (_nullable_) | +| **`date`** (\*) | Date and time when commit was authored | `Date` | | **`author`** (\*) | Commit author name | `string` | _(\*) Required._ @@ -210,7 +212,7 @@ _Object containing the following properties:_ | **`plugins`** (\*) | List of plugins to be used (official, community-provided, or custom) | _Array of at least 1 [PluginConfig](#pluginconfig) items_ | | `persist` | | [PersistConfig](#persistconfig) | | `upload` | | [UploadConfig](#uploadconfig) | -| `categories` | | _Array of [CategoryConfig](#categoryconfig) items_ | +| `categories` | Categorization of individual audits | _Array of [CategoryConfig](#categoryconfig) items_ | _(\*) Required._ @@ -235,11 +237,11 @@ _(\*) Required._ _Object containing the following properties:_ -| Property | Description | Type | -| :---------------- | :-------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **`name`** (\*) | File or folder name | `string` (_min length: 1_) | -| **`values`** (\*) | Coverage metrics for file/folder | _Object with properties:_ | -| `children` | Files and folders contained in this folder (omit if file) | _Array of [CoverageTreeNode](#coveragetreenode) items_ | +| Property | Description | Type | +| :---------------- | :-------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **`name`** (\*) | File or folder name | `string` (_min length: 1_) | +| **`values`** (\*) | Coverage metrics for file/folder | _Object with properties:_ | +| `children` | Files and folders contained in this folder (omit if file) | _Array of [CoverageTreeNode](#coveragetreenode) items_ | _(\*) Required._ @@ -267,7 +269,7 @@ _String which has a minimum length of 1._ ## Format -_Enum string, one of the following possible values:_ +_Enum, one of the following possible values:_ - `'json'` - `'md'` @@ -276,13 +278,13 @@ _Enum string, one of the following possible values:_ _Object containing the following properties:_ -| Property | Description | Type | -| :---------------- | :----------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **`slug`** (\*) | Unique ID (human-readable, URL-safe) | `string` (_regex: `/^[a-z\d]+(?:-[a-z\d]+)*$/`, max length: 128_) | -| **`title`** (\*) | Descriptive name | `string` (_max length: 256_) | -| `docsUrl` | Documentation site | `string` (_url_) (_optional_) _or_ `''` | -| **`scores`** (\*) | Score comparison | _Object with properties:_ | -| **`plugin`** (\*) | Plugin which defines it | _Object with properties:_ | +| Property | Description | Type | +| :---------------- | :----------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **`slug`** (\*) | Unique ID (human-readable, URL-safe) | `string` (_regex: `/^[a-z\d]+(?:-[a-z\d]+)*$/`, max length: 128_) | +| **`title`** (\*) | Descriptive name | `string` (_max length: 256_) | +| `docsUrl` | Documentation site | `string` (_url_) (_optional_) _or_ `''` | +| **`scores`** (\*) | Score comparison | _Object with properties:_ | +| **`plugin`** (\*) | Plugin which defines it | _Object with properties:_ | _(\*) Required._ @@ -303,13 +305,13 @@ _(\*) Required._ _Object containing the following properties:_ -| Property | Description | Type | -| :---------------- | :----------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **`slug`** (\*) | Unique ID (human-readable, URL-safe) | `string` (_regex: `/^[a-z\d]+(?:-[a-z\d]+)*$/`, max length: 128_) | -| **`title`** (\*) | Descriptive name | `string` (_max length: 256_) | -| `docsUrl` | Documentation site | `string` (_url_) (_optional_) _or_ `''` | -| **`plugin`** (\*) | Plugin which defines it | _Object with properties:_ | -| **`score`** (\*) | Value between 0 and 1 | `number` (_≥0, ≤1_) | +| Property | Description | Type | +| :---------------- | :----------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **`slug`** (\*) | Unique ID (human-readable, URL-safe) | `string` (_regex: `/^[a-z\d]+(?:-[a-z\d]+)*$/`, max length: 128_) | +| **`title`** (\*) | Descriptive name | `string` (_max length: 256_) | +| `docsUrl` | Documentation site | `string` (_url_) (_optional_) _or_ `''` | +| **`plugin`** (\*) | Plugin which defines it | _Object with properties:_ | +| **`score`** (\*) | Value between 0 and 1 | `number` (_≥0, ≤1_) | _(\*) Required._ @@ -346,7 +348,7 @@ _(\*) Required._ Severity level -_Enum string, one of the following possible values:_ +_Enum, one of the following possible values:_ - `'info'` - `'warning'` @@ -356,7 +358,7 @@ _Enum string, one of the following possible values:_ Icon from VSCode Material Icons extension -_Enum string, one of the following possible values:_ +_Enum, one of the following possible values:_
Expand for full list of 858 values @@ -1260,11 +1262,22 @@ _Object containing the following properties:_ | **`slug`** (\*) | Unique plugin slug within core config | `string` (_regex: `/^[a-z\d]+(?:-[a-z\d]+)*$/`, max length: 128_) | | **`icon`** (\*) | Icon from VSCode Material Icons extension | [MaterialIcon](#materialicon) | | **`runner`** (\*) | | [RunnerConfig](#runnerconfig) _or_ [RunnerFunction](#runnerfunction) | -| **`audits`** (\*) | | _Array of at least 1 [Audit](#audit) items_ | -| `groups` | | _Array of [Group](#group) items_ | +| **`audits`** (\*) | List of audits maintained in a plugin | _Array of at least 1 [Audit](#audit) items_ | +| `groups` | List of groups | _Array of [Group](#group) items_ | +| `context` | Plugin-specific context data for helpers | [PluginContext](#plugincontext) | _(\*) Required._ +## PluginContext + +Plugin-specific context data for helpers + +_Object record with dynamic keys:_ + +- _keys of type_ `string` +- _values of type_ `unknown` + (_optional_) + ## PluginMeta _Object containing the following properties:_ @@ -1305,17 +1318,19 @@ _(\*) Required._ ## Report +Collect output data + _Object containing the following properties:_ -| Property | Description | Type | -| :--------------------- | :---------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **`packageName`** (\*) | NPM package name | `string` | -| **`version`** (\*) | NPM version of the CLI | `string` | -| **`date`** (\*) | Start date and time of the collect run | `string` | -| **`duration`** (\*) | Duration of the collect run in ms | `number` | -| **`plugins`** (\*) | | _Array of at least 1 [PluginReport](#pluginreport) items_ | -| `categories` | | _Array of [CategoryConfig](#categoryconfig) items_ | -| **`commit`** (\*) | Git commit for which report was collected | _Object with properties:_ (_nullable_) | +| Property | Description | Type | +| :--------------------- | :---------------------------------------- | :-------------------------------------------------------- | +| **`packageName`** (\*) | NPM package name | `string` | +| **`version`** (\*) | NPM version of the CLI | `string` | +| **`date`** (\*) | Start date and time of the collect run | `string` | +| **`duration`** (\*) | Duration of the collect run in ms | `number` | +| **`plugins`** (\*) | | _Array of at least 1 [PluginReport](#pluginreport) items_ | +| `categories` | | _Array of [CategoryConfig](#categoryconfig) items_ | +| **`commit`** (\*) | Git commit for which report was collected | [Commit](#commit) (_nullable_) | _(\*) Required._ @@ -1323,18 +1338,18 @@ _(\*) Required._ _Object containing the following properties:_ -| Property | Description | Type | -| :--------------------- | :---------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **`commits`** (\*) | Commits identifying compared reports | _Object with properties:_ (_nullable_) | -| `portalUrl` | Link to comparison page in Code PushUp portal | `string` (_url_) | -| `label` | Label (e.g. project name) | `string` | -| **`categories`** (\*) | Changes affecting categories | _Object with properties:_ | -| **`groups`** (\*) | Changes affecting groups | _Object with properties:_ | -| **`audits`** (\*) | Changes affecting audits | _Object with properties:_ | -| **`packageName`** (\*) | NPM package name | `string` | -| **`version`** (\*) | NPM version of the CLI (when `compare` was run) | `string` | -| **`date`** (\*) | Start date and time of the compare run | `string` | -| **`duration`** (\*) | Duration of the compare run in ms | `number` | +| Property | Description | Type | +| :--------------------- | :---------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **`commits`** (\*) | Commits identifying compared reports | _Object with properties:_ (_nullable_) | +| `portalUrl` | Link to comparison page in Code PushUp portal | `string` (_url_) | +| `label` | Label (e.g. project name) | `string` | +| **`categories`** (\*) | Changes affecting categories | _Object with properties:_ | +| **`groups`** (\*) | Changes affecting groups | _Object with properties:_ | +| **`audits`** (\*) | Changes affecting audits | _Object with properties:_ | +| **`packageName`** (\*) | NPM package name | `string` | +| **`version`** (\*) | NPM version of the CLI (when `compare` was run) | `string` | +| **`date`** (\*) | Start date and time of the compare run | `string` | +| **`duration`** (\*) | Duration of the compare run in ms | `number` | _(\*) Required._ @@ -1344,13 +1359,13 @@ How to execute runner _Object containing the following properties:_ -| Property | Description | Type | -| :-------------------- | :----------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **`command`** (\*) | Shell command to execute | `string` | -| `args` | | `Array` | -| **`outputFile`** (\*) | Runner output path | [FilePath](#filepath) | -| `outputTransform` | | _Function:_
| -| `configFile` | Runner config path | [FilePath](#filepath) | +| Property | Description | Type | +| :-------------------- | :----------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **`command`** (\*) | Shell command to execute | `string` | +| `args` | Command arguments | `Array` | +| **`outputFile`** (\*) | Runner output path | [FilePath](#filepath) | +| `outputTransform` | | _Function:_
  • _parameters:_
    1. `unknown`
  • _returns:_ [AuditOutputs](#auditoutputs) _or_ _Promise of_ [AuditOutputs](#auditoutputs)
| +| `configFile` | Runner config path | [FilePath](#filepath) | _(\*) Required._ @@ -1383,10 +1398,10 @@ Source file location _Object containing the following properties:_ -| Property | Description | Type | -| :-------------- | :--------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **`file`** (\*) | Relative path to source file in Git repo | [FilePath](#filepath) | -| `position` | Location in file | _Object with properties:_
  • `startLine`: `number` (_int, >0_) - Start line
  • `startColumn`: `number` (_int, >0_) - Start column
  • `endLine`: `number` (_int, >0_) - End line
  • `endColumn`: `number` (_int, >0_) - End column
| +| Property | Description | Type | +| :-------------- | :--------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **`file`** (\*) | Relative path to source file in Git repo | [FilePath](#filepath) | +| `position` | Location in file | _Object with properties:_
  • **`startLine`** (\*): `number` (_int, >0_) - Start line
  • `startColumn`: `number` (_int, >0_) - Start column
  • `endLine`: `number` (_int, >0_) - End line
  • `endColumn`: `number` (_int, >0_) - End column
| _(\*) Required._ @@ -1394,7 +1409,7 @@ _(\*) Required._ Cell alignment -_Enum string, one of the following possible values:_ +_Enum, one of the following possible values:_ - `'left'` - `'center'` @@ -1407,8 +1422,7 @@ _Union of the following possible types:_ - `string` - `number` - `boolean` -- `null` (_nullable_) - (_optional & nullable_) +- `null` _Default value:_ `null` @@ -1428,7 +1442,7 @@ _(\*) Required._ Cell alignment -_Enum string, one of the following possible values:_ +_Enum, one of the following possible values:_ - `'left'` - `'center'` @@ -1441,13 +1455,13 @@ Object row _Object record with dynamic keys:_ - _keys of type_ `string` -- _values of type_ [TableCellValue](#tablecellvalue) (_optional & nullable_) +- _values of type_ [TableCellValue](#tablecellvalue) ## TableRowPrimitive Primitive row -_Array of [TableCellValue](#tablecellvalue) (\_optional & nullable_) items.\_ +_Array of [TableCellValue](#tablecellvalue) items._ ## Tree diff --git a/packages/models/package.json b/packages/models/package.json index 82b81e2f8..77dfeea93 100644 --- a/packages/models/package.json +++ b/packages/models/package.json @@ -27,7 +27,7 @@ }, "type": "module", "dependencies": { - "zod": "^3.22.1", + "zod": "^4.0.5", "vscode-material-icons": "^0.1.0" } } diff --git a/packages/models/src/lib/audit-output.ts b/packages/models/src/lib/audit-output.ts index 7c9b611ff..a5b431f15 100644 --- a/packages/models/src/lib/audit-output.ts +++ b/packages/models/src/lib/audit-output.ts @@ -1,10 +1,10 @@ import { z } from 'zod'; +import { createDuplicateSlugsCheck } from './implementation/checks.js'; import { nonnegativeNumberSchema, scoreSchema, slugSchema, } from './implementation/schemas.js'; -import { errorItems, hasDuplicateStrings } from './implementation/utils.js'; import { issueSchema } from './issue.js'; import { tableSchema } from './table.js'; import { treeSchema } from './tree.js'; @@ -12,56 +12,38 @@ import { treeSchema } from './tree.js'; export const auditValueSchema = nonnegativeNumberSchema.describe('Raw numeric value'); export const auditDisplayValueSchema = z - .string({ description: "Formatted value (e.g. '0.9 s', '2.1 MB')" }) - .optional(); + .string() + .optional() + .describe("Formatted value (e.g. '0.9 s', '2.1 MB')"); -export const auditDetailsSchema = z.object( - { - issues: z - .array(issueSchema, { description: 'List of findings' }) - .optional(), +export const auditDetailsSchema = z + .object({ + issues: z.array(issueSchema).describe('List of findings').optional(), table: tableSchema('Table of related findings').optional(), trees: z - .array(treeSchema, { description: 'Findings in tree structure' }) + .array(treeSchema) + .describe('Findings in tree structure') .optional(), - }, - { description: 'Detailed information' }, -); + }) + .describe('Detailed information'); export type AuditDetails = z.infer; -export const auditOutputSchema = z.object( - { +export const auditOutputSchema = z + .object({ slug: slugSchema.describe('Reference to audit'), displayValue: auditDisplayValueSchema, value: auditValueSchema, score: scoreSchema, details: auditDetailsSchema.optional(), - }, - { description: 'Audit information' }, -); + }) + .describe('Audit information'); export type AuditOutput = z.infer; export const auditOutputsSchema = z - .array(auditOutputSchema, { - description: - 'List of JSON formatted audit output emitted by the runner process of a plugin', - }) - // audit slugs are unique - .refine( - audits => !getDuplicateSlugsInAudits(audits), - audits => ({ message: duplicateSlugsInAuditsErrorMsg(audits) }), + .array(auditOutputSchema) + .check(createDuplicateSlugsCheck('Audit')) + .describe( + 'List of JSON formatted audit output emitted by the runner process of a plugin', ); export type AuditOutputs = z.infer; - -// helper for validator: audit slugs are unique -function duplicateSlugsInAuditsErrorMsg(audits: AuditOutput[]) { - const duplicateRefs = getDuplicateSlugsInAudits(audits); - return `In plugin audits the slugs are not unique: ${errorItems( - duplicateRefs, - )}`; -} - -function getDuplicateSlugsInAudits(audits: AuditOutput[]) { - return hasDuplicateStrings(audits.map(({ slug }) => slug)); -} diff --git a/packages/models/src/lib/audit-output.unit.test.ts b/packages/models/src/lib/audit-output.unit.test.ts index 64160ceaa..7a9515ded 100644 --- a/packages/models/src/lib/audit-output.unit.test.ts +++ b/packages/models/src/lib/audit-output.unit.test.ts @@ -179,6 +179,8 @@ describe('auditOutputsSchema', () => { score: 0.75, }, ] satisfies AuditOutputs), - ).toThrow('slugs are not unique: total-blocking-time'); + ).toThrow( + String.raw`Audit slugs must be unique, but received duplicates: \"total-blocking-time\"`, + ); }); }); diff --git a/packages/models/src/lib/audit.ts b/packages/models/src/lib/audit.ts index e28b9ef61..695ad5ef9 100644 --- a/packages/models/src/lib/audit.ts +++ b/packages/models/src/lib/audit.ts @@ -1,6 +1,6 @@ import { z } from 'zod'; +import { createDuplicateSlugsCheck } from './implementation/checks.js'; import { metaSchema, slugSchema } from './implementation/schemas.js'; -import { errorItems, hasDuplicateStrings } from './implementation/utils.js'; export const auditSchema = z .object({ @@ -18,28 +18,7 @@ export const auditSchema = z export type Audit = z.infer; export const pluginAuditsSchema = z - .array(auditSchema, { - description: 'List of audits maintained in a plugin', - }) + .array(auditSchema) .min(1) - // audit slugs are unique - .refine( - auditMetadata => !getDuplicateSlugsInAudits(auditMetadata), - auditMetadata => ({ - message: duplicateSlugsInAuditsErrorMsg(auditMetadata), - }), - ); - -// ======================= - -// helper for validator: audit slugs are unique -function duplicateSlugsInAuditsErrorMsg(audits: Audit[]) { - const duplicateRefs = getDuplicateSlugsInAudits(audits); - return `In plugin audits the following slugs are not unique: ${errorItems( - duplicateRefs, - )}`; -} - -function getDuplicateSlugsInAudits(audits: Audit[]) { - return hasDuplicateStrings(audits.map(({ slug }) => slug)); -} + .check(createDuplicateSlugsCheck('Audit')) + .describe('List of audits maintained in a plugin'); diff --git a/packages/models/src/lib/audit.unit.test.ts b/packages/models/src/lib/audit.unit.test.ts index 0c637ca5d..a681f6041 100644 --- a/packages/models/src/lib/audit.unit.test.ts +++ b/packages/models/src/lib/audit.unit.test.ts @@ -74,6 +74,8 @@ describe('pluginAuditsSchema', () => { title: 'Jest unit tests results.', }, ] satisfies Audit[]), - ).toThrow('slugs are not unique: jest-unit-test-results'); + ).toThrow( + String.raw`Audit slugs must be unique, but received duplicates: \"jest-unit-test-results\"`, + ); }); }); diff --git a/packages/models/src/lib/category-config.ts b/packages/models/src/lib/category-config.ts index 16dd04aa9..c634afced 100644 --- a/packages/models/src/lib/category-config.ts +++ b/packages/models/src/lib/category-config.ts @@ -1,21 +1,26 @@ import { z } from 'zod'; +import { + createDuplicateSlugsCheck, + createDuplicatesCheck, +} from './implementation/checks.js'; import { metaSchema, scorableSchema, slugSchema, weightedRefSchema, } from './implementation/schemas.js'; -import { errorItems, hasDuplicateStrings } from './implementation/utils.js'; +import { formatRef } from './implementation/utils.js'; export const categoryRefSchema = weightedRefSchema( 'Weighted references to audits and/or groups for the category', 'Slug of an audit or group (depending on `type`)', ).merge( z.object({ - type: z.enum(['audit', 'group'], { - description: + type: z + .enum(['audit', 'group']) + .describe( 'Discriminant for reference kind, affects where `slug` is looked up', - }), + ), plugin: slugSchema.describe( 'Plugin slug (plugin should contain referenced audit or group)', ), @@ -26,8 +31,11 @@ export type CategoryRef = z.infer; export const categoryConfigSchema = scorableSchema( 'Category with a score calculated from audits and groups from various plugins', categoryRefSchema, - getDuplicateRefsInCategoryMetrics, - duplicateRefsInCategoryMetricsErrorMsg, + createDuplicatesCheck( + serializeCategoryRefTarget, + duplicates => + `Category has duplicate references: ${formatSerializedCategoryRefTargets(duplicates)}`, + ), ) .merge( metaSchema({ @@ -40,49 +48,36 @@ export const categoryConfigSchema = scorableSchema( .merge( z.object({ isBinary: z - .boolean({ - description: - 'Is this a binary category (i.e. only a perfect score considered a "pass")?', - }) + .boolean() + .describe( + 'Is this a binary category (i.e. only a perfect score considered a "pass")?', + ) .optional(), }), ); export type CategoryConfig = z.infer; -// helper for validator: categories have unique refs to audits or groups -export function duplicateRefsInCategoryMetricsErrorMsg(metrics: CategoryRef[]) { - const duplicateRefs = getDuplicateRefsInCategoryMetrics(metrics); - return `In the categories, the following audit or group refs are duplicates: ${errorItems( - duplicateRefs, - )}`; -} +const CATEGORY_REF_SEP = '||'; -function getDuplicateRefsInCategoryMetrics(metrics: CategoryRef[]) { - return hasDuplicateStrings( - metrics.map(({ slug, type, plugin }) => `${type} :: ${plugin} / ${slug}`), - ); +function serializeCategoryRefTarget(ref: CategoryRef): string { + return [ref.type, ref.plugin, ref.slug].join(CATEGORY_REF_SEP); } -export const categoriesSchema = z - .array(categoryConfigSchema, { - description: 'Categorization of individual audits', - }) - .refine( - categoryCfg => !getDuplicateSlugCategories(categoryCfg), - categoryCfg => ({ - message: duplicateSlugCategoriesErrorMsg(categoryCfg), - }), - ); - -// helper for validator: categories slugs are unique -function duplicateSlugCategoriesErrorMsg(categories: CategoryConfig[]) { - const duplicateStringSlugs = getDuplicateSlugCategories(categories); - return `In the categories, the following slugs are duplicated: ${errorItems( - duplicateStringSlugs, - )}`; +function formatSerializedCategoryRefTargets(keys: string[]): string { + return keys + .map(key => { + const [type, plugin, slug] = key.split(CATEGORY_REF_SEP) as [ + 'group' | 'audit', + string, + string, + ]; + return formatRef({ type, plugin, slug }); + }) + .join(', '); } -function getDuplicateSlugCategories(categories: CategoryConfig[]) { - return hasDuplicateStrings(categories.map(({ slug }) => slug)); -} +export const categoriesSchema = z + .array(categoryConfigSchema) + .check(createDuplicateSlugsCheck('Category')) + .describe('Categorization of individual audits'); diff --git a/packages/models/src/lib/category-config.unit.test.ts b/packages/models/src/lib/category-config.unit.test.ts index d234c8292..bc717bb30 100644 --- a/packages/models/src/lib/category-config.unit.test.ts +++ b/packages/models/src/lib/category-config.unit.test.ts @@ -49,7 +49,7 @@ describe('categoryRefSchema', () => { type: 'audit', weight: -2, } satisfies CategoryRef), - ).toThrow('Number must be greater than or equal to 0'); + ).toThrow('Too small: expected number to be >=0'); }); it('should throw for an invalid reference type', () => { @@ -60,7 +60,7 @@ describe('categoryRefSchema', () => { type: 'issue', weight: 1, }), - ).toThrow('Invalid enum value'); + ).toThrow(String.raw`Invalid option: expected one of \"audit\"|\"group\"`); }); it('should throw for a missing weight', () => { @@ -152,7 +152,9 @@ describe('categoryConfigSchema', () => { }, ], } satisfies CategoryConfig), - ).toThrow('audit or group refs are duplicates'); + ).toThrow( + String.raw`Category has duplicate references: audit \"jest-unit-tests\" (plugin \"jest\")`, + ); }); it('should throw for a category with only zero-weight references', () => { @@ -175,9 +177,7 @@ describe('categoryConfigSchema', () => { }, ], } satisfies CategoryConfig), - ).toThrow( - 'In a category, there has to be at least one ref with weight > 0. Affected refs: functional/immutable-data, lighthouse-experimental', - ); + ).toThrow('A category must have at least 1 ref with weight > 0.'); }); }); @@ -246,7 +246,7 @@ describe('categoriesSchema', () => { }, ] satisfies CategoryConfig[]), ).toThrow( - 'In the categories, the following slugs are duplicated: bug-prevention', + String.raw`Category slugs must be unique, but received duplicates: \"bug-prevention\"`, ); }); }); diff --git a/packages/models/src/lib/commit.ts b/packages/models/src/lib/commit.ts index 8442b8030..c92609ef2 100644 --- a/packages/models/src/lib/commit.ts +++ b/packages/models/src/lib/commit.ts @@ -1,24 +1,18 @@ import { z } from 'zod'; -export const commitSchema = z.object( - { +export const commitSchema = z + .object({ hash: z - .string({ description: 'Commit SHA (full)' }) + .string() .regex( /^[\da-f]{40}$/, 'Commit SHA should be a 40-character hexadecimal string', - ), - message: z.string({ description: 'Commit message' }), - date: z.coerce.date({ - description: 'Date and time when commit was authored', - }), - author: z - .string({ - description: 'Commit author name', - }) - .trim(), - }, - { description: 'Git commit' }, -); + ) + .describe('Commit SHA (full)'), + message: z.string().describe('Commit message'), + date: z.coerce.date().describe('Date and time when commit was authored'), + author: z.string().trim().describe('Commit author name'), + }) + .describe('Git commit'); export type Commit = z.infer; diff --git a/packages/models/src/lib/configuration.ts b/packages/models/src/lib/configuration.ts index 72eca414d..e3512d2f5 100644 --- a/packages/models/src/lib/configuration.ts +++ b/packages/models/src/lib/configuration.ts @@ -4,9 +4,9 @@ import { z } from 'zod'; * Generic schema for a tool command configuration, reusable across plugins. */ export const artifactGenerationCommandSchema = z.union([ - z.string({ description: 'Generate artifact files' }).min(1), + z.string().min(1).describe('Generate artifact files'), z.object({ - command: z.string({ description: 'Generate artifact files' }).min(1), + command: z.string().min(1).describe('Generate artifact files'), args: z.array(z.string()).optional(), }), ]); diff --git a/packages/models/src/lib/core-config.ts b/packages/models/src/lib/core-config.ts index 4c5307557..a4e6469f7 100644 --- a/packages/models/src/lib/core-config.ts +++ b/packages/models/src/lib/core-config.ts @@ -1,20 +1,18 @@ import { z } from 'zod'; import { categoriesSchema } from './category-config.js'; -import { - getMissingRefsForCategories, - missingRefsForCategoriesErrorMsg, -} from './implementation/utils.js'; +import { createCheck } from './implementation/checks.js'; +import { findMissingSlugsInCategoryRefs } from './implementation/utils.js'; import { persistConfigSchema } from './persist-config.js'; import { pluginConfigSchema } from './plugin-config.js'; import { uploadConfigSchema } from './upload-config.js'; export const unrefinedCoreConfigSchema = z.object({ plugins: z - .array(pluginConfigSchema, { - description: - 'List of plugins to be used (official, community-provided, or custom)', - }) - .min(1), + .array(pluginConfigSchema) + .min(1) + .describe( + 'List of plugins to be used (official, community-provided, or custom)', + ), /** portal configuration for persisting results */ persist: persistConfigSchema.optional(), /** portal configuration for uploading results */ @@ -31,13 +29,7 @@ export const coreConfigSchema = refineCoreConfig(unrefinedCoreConfigSchema); */ export function refineCoreConfig(schema: typeof unrefinedCoreConfigSchema) { // categories point to existing audit or group refs - return schema.refine( - ({ categories, plugins }) => - !getMissingRefsForCategories(categories, plugins), - ({ categories, plugins }) => ({ - message: missingRefsForCategoriesErrorMsg(categories, plugins), - }), - ); + return schema.check(createCheck(findMissingSlugsInCategoryRefs)); } export type CoreConfig = z.infer; diff --git a/packages/models/src/lib/core-config.unit.test.ts b/packages/models/src/lib/core-config.unit.test.ts index edfb6fa73..ddc0e0072 100644 --- a/packages/models/src/lib/core-config.unit.test.ts +++ b/packages/models/src/lib/core-config.unit.test.ts @@ -94,7 +94,7 @@ describe('coreConfigSchema', () => { ], } satisfies CoreConfig), ).toThrow( - 'category references need to point to an audit or group: vitest/unit-tests', + String.raw`Category references audits or groups which don't exist: audit \"unit-tests\" (plugin \"vitest\")`, ); }); @@ -133,7 +133,7 @@ describe('coreConfigSchema', () => { ], } satisfies CoreConfig), ).toThrow( - 'category references need to point to an audit or group: eslint#eslint-errors (group)', + String.raw`Category references audits or groups which don't exist: group \"eslint-errors\" (plugin \"eslint\")`, ); }); @@ -189,7 +189,7 @@ describe('coreConfigSchema', () => { } satisfies CoreConfig; expect(() => coreConfigSchema.parse(config)).toThrow( - 'In a category, there has to be at least one ref with weight > 0. Affected refs: csp-xss', + 'A category must have at least 1 ref with weight > 0.', ); }); }); diff --git a/packages/models/src/lib/group.ts b/packages/models/src/lib/group.ts index faaa55b4e..a7b16eec0 100644 --- a/packages/models/src/lib/group.ts +++ b/packages/models/src/lib/group.ts @@ -1,15 +1,14 @@ import { z } from 'zod'; import { - type WeightedRef, + createDuplicateSlugsCheck, + createDuplicatesCheck, +} from './implementation/checks.js'; +import { metaSchema, scorableSchema, weightedRefSchema, } from './implementation/schemas.js'; -import { - errorItems, - exists, - hasDuplicateStrings, -} from './implementation/utils.js'; +import { formatSlugsList } from './implementation/utils.js'; export const groupRefSchema = weightedRefSchema( 'Weighted reference to a group', @@ -30,48 +29,17 @@ export const groupSchema = scorableSchema( 'A group aggregates a set of audits into a single score which can be referenced from a category. ' + 'E.g. the group slug "performance" groups audits and can be referenced in a category', groupRefSchema, - getDuplicateRefsInGroups, - duplicateRefsInGroupsErrorMsg, + createDuplicatesCheck( + ({ slug }) => slug, + duplicates => + `Group has duplicate references to audits: ${formatSlugsList(duplicates)}`, + ), ).merge(groupMetaSchema); export type Group = z.infer; export const groupsSchema = z - .array(groupSchema, { - description: 'List of groups', - }) + .array(groupSchema) + .check(createDuplicateSlugsCheck('Group')) .optional() - .refine( - groups => !getDuplicateSlugsInGroups(groups), - groups => ({ - message: duplicateSlugsInGroupsErrorMsg(groups), - }), - ); - -// ============ - -// helper for validator: group refs are unique -function duplicateRefsInGroupsErrorMsg(groups: WeightedRef[]) { - const duplicateRefs = getDuplicateRefsInGroups(groups); - return `In plugin groups the following references are not unique: ${errorItems( - duplicateRefs, - )}`; -} - -function getDuplicateRefsInGroups(groups: WeightedRef[]) { - return hasDuplicateStrings(groups.map(({ slug: ref }) => ref).filter(exists)); -} - -// helper for validator: group refs are unique -function duplicateSlugsInGroupsErrorMsg(groups: Group[] | undefined) { - const duplicateRefs = getDuplicateSlugsInGroups(groups); - return `In groups the following slugs are not unique: ${errorItems( - duplicateRefs, - )}`; -} - -function getDuplicateSlugsInGroups(groups: Group[] | undefined) { - return Array.isArray(groups) - ? hasDuplicateStrings(groups.map(({ slug }) => slug)) - : false; -} + .describe('List of groups'); diff --git a/packages/models/src/lib/group.unit.test.ts b/packages/models/src/lib/group.unit.test.ts index e0f9b5149..baef00fd1 100644 --- a/packages/models/src/lib/group.unit.test.ts +++ b/packages/models/src/lib/group.unit.test.ts @@ -82,7 +82,9 @@ describe('groupSchema', () => { { slug: 'lighthouse-bug-prevention', weight: 2 }, ], } satisfies Group), - ).toThrow('following references are not unique: lighthouse-bug-prevention'); + ).toThrow( + String.raw`Group has duplicate references to audits: \"lighthouse-bug-prevention\"`, + ); }); }); @@ -127,6 +129,8 @@ describe('groupsSchema', () => { refs: [{ slug: 'jest-unit-tests', weight: 2 }], }, ] satisfies Group[]), - ).toThrow('slugs are not unique: lighthouse'); + ).toThrow( + String.raw`Group slugs must be unique, but received duplicates: \"lighthouse\"`, + ); }); }); diff --git a/packages/models/src/lib/implementation/checks.ts b/packages/models/src/lib/implementation/checks.ts new file mode 100644 index 000000000..a3f12341f --- /dev/null +++ b/packages/models/src/lib/implementation/checks.ts @@ -0,0 +1,42 @@ +import type { z } from 'zod'; +import { hasDuplicateStrings } from './utils.js'; + +export function createCheck( + findErrorFn: (value: T) => false | { message: string }, +): z.core.CheckFn { + return ctx => { + const error = findErrorFn(ctx.value); + if (error) { + // eslint-disable-next-line functional/immutable-data, no-param-reassign + ctx.issues = [ + ...ctx.issues, + { + code: 'custom', + message: error.message, + input: ctx.value, + }, + ]; + } + }; +} + +export function createDuplicatesCheck( + keyFn: (item: T) => string, + errorMsgFn: (duplicates: string[]) => string, +): z.core.CheckFn { + return createCheck(items => { + const keys = items.map(keyFn); + const duplicates = hasDuplicateStrings(keys); + return duplicates && { message: errorMsgFn(duplicates) }; + }); +} + +export function createDuplicateSlugsCheck( + name: 'Audit' | 'Plugin' | 'Category' | 'Group', +): z.core.CheckFn { + return createDuplicatesCheck( + ({ slug }) => slug, + duplicates => + `${name} slugs must be unique, but received duplicates: ${duplicates.map(slug => JSON.stringify(slug)).join(', ')}`, + ); +} diff --git a/packages/models/src/lib/implementation/checks.unit.test.ts b/packages/models/src/lib/implementation/checks.unit.test.ts new file mode 100644 index 000000000..802f88351 --- /dev/null +++ b/packages/models/src/lib/implementation/checks.unit.test.ts @@ -0,0 +1,146 @@ +import type { z } from 'zod'; +import type { Audit } from '../audit.js'; +import type { CategoryRef } from '../category-config.js'; +import { + createCheck, + createDuplicateSlugsCheck, + createDuplicatesCheck, +} from './checks.js'; + +describe('createCheck', () => { + it('should add issue if callback finds an error', () => { + const findErrorFn = vi + .fn<[string]>() + .mockReturnValue({ message: 'Something went wrong' }); + + const check = createCheck(findErrorFn); + + expect(findErrorFn).not.toHaveBeenCalled(); + + const ctx: z.core.ParsePayload = { value: 'XYZ', issues: [] }; + check(ctx); + + expect(findErrorFn).toHaveBeenCalledWith('XYZ'); + expect(ctx.issues).toEqual([ + { + code: 'custom', + message: 'Something went wrong', + input: 'XYZ', + }, + ]); + }); + + it('should NOT add issue if callback finds no error', () => { + const findErrorFn = vi.fn<[string]>().mockReturnValue(false); + + const check = createCheck(findErrorFn); + + expect(findErrorFn).not.toHaveBeenCalled(); + + const ctx: z.core.ParsePayload = { value: 'XYZ', issues: [] }; + check(ctx); + + expect(findErrorFn).toHaveBeenCalledWith('XYZ'); + expect(ctx.issues).toEqual([]); + }); +}); + +describe('createDuplicatesCheck', () => { + const keyFn = vi.fn( + ({ type, plugin, slug }: CategoryRef) => `${type} ${plugin}/${slug}`, + ); + const errorMsgFn = vi.fn( + (duplicates: string[]) => `Duplicate refs found: ${duplicates.join(', ')}`, + ); + + it('add issue with custom message if there are duplicate keys', () => { + const check = createDuplicatesCheck(keyFn, errorMsgFn); + + expect(keyFn).not.toHaveBeenCalled(); + expect(errorMsgFn).not.toHaveBeenCalled(); + + const ctx: z.core.ParsePayload = { + value: [ + { type: 'audit', plugin: 'coverage', slug: 'coverage', weight: 2 }, + { type: 'audit', plugin: 'jsdocs', slug: 'coverage', weight: 1 }, + { type: 'audit', plugin: 'coverage', slug: 'coverage', weight: 1 }, + ], + issues: [], + }; + check(ctx); + + expect(keyFn).toHaveBeenCalledTimes(3); + expect(errorMsgFn).toHaveBeenCalledWith(['audit coverage/coverage']); + expect(ctx.issues).toEqual([ + { + code: 'custom', + message: 'Duplicate refs found: audit coverage/coverage', + input: ctx.value, + }, + ]); + }); + + it('add NOT add issue if all keys are unique', () => { + const check = createDuplicatesCheck(keyFn, errorMsgFn); + + expect(keyFn).not.toHaveBeenCalled(); + expect(errorMsgFn).not.toHaveBeenCalled(); + + const ctx: z.core.ParsePayload = { + value: [ + { type: 'group', plugin: 'eslint', slug: 'errors', weight: 1 }, + { type: 'group', plugin: 'eslint', slug: 'warnings', weight: 1 }, + { type: 'group', plugin: 'typescript', slug: 'errors', weight: 1 }, + ], + issues: [], + }; + check(ctx); + + expect(keyFn).toHaveBeenCalledTimes(3); + expect(errorMsgFn).not.toHaveBeenCalled(); + expect(ctx.issues).toEqual([]); + }); +}); + +describe('createDuplicateSlugsCheck', () => { + it('should add issue if there are duplicate slugs', () => { + const check = createDuplicateSlugsCheck('Audit'); + + const ctx: z.core.ParsePayload = { + value: [ + { slug: 'lcp', title: 'Largest Contentful Paint' }, + { slug: 'cls', title: 'Cumulative Layout Shift' }, + { slug: 'fcp', title: 'First Contentful Paint' }, + { slug: 'lcp', title: 'LCP' }, + { slug: 'fcp', title: 'FCP' }, + ], + issues: [], + }; + check(ctx); + + expect(ctx.issues).toEqual([ + { + code: 'custom', + message: + 'Audit slugs must be unique, but received duplicates: "fcp", "lcp"', + input: ctx.value, + }, + ]); + }); + + it('should NOT add issue if all slugs are unique', () => { + const check = createDuplicateSlugsCheck('Audit'); + + const ctx: z.core.ParsePayload = { + value: [ + { slug: 'lcp', title: 'Largest Contentful Paint' }, + { slug: 'cls', title: 'Cumulative Layout Shift' }, + { slug: 'fcp', title: 'First Contentful Paint' }, + ], + issues: [], + }; + check(ctx); + + expect(ctx.issues).toEqual([]); + }); +}); diff --git a/packages/models/src/lib/implementation/function.ts b/packages/models/src/lib/implementation/function.ts new file mode 100644 index 000000000..af5101499 --- /dev/null +++ b/packages/models/src/lib/implementation/function.ts @@ -0,0 +1,60 @@ +import { z } from 'zod/v4'; +import type { $ZodFunction } from 'zod/v4/core'; + +// https://zod.dev/v4/changelog?id=zfunction +// https://github.com/colinhacks/zod/issues/4143#issuecomment-2931729793 +// https://github.com/matejchalk/zod2md?tab=readme-ov-file#function-schemas + +/** + * Converts Zod v4 function factory (returned by `z.function`) to Zod schema. + * + * Supports asynchronous functions. For synchronous functions, you can use {@link convertSyncZodFunctionToSchema}. + * + * @param factory `z.function({ input: [...], output: ... })` + * @returns Zod schema with compile-time and runtime validations. + */ +export function convertAsyncZodFunctionToSchema( + factory: T, +) { + return z + .custom() + .transform((arg, ctx) => { + if (typeof arg !== 'function') { + ctx.addIssue(`Expected function, received ${typeof arg}`); + return z.NEVER; + } + return factory.implementAsync(arg as Parameters[0]); + }) + .meta({ + // enables zod2md to include function signature in docs + $ZodFunction: factory, + }); +} + +/** + * Converts Zod v4 function factory (returned by `z.function`) to Zod schema. + * + * **IMPORTANT!** Use for synchronous functions only. For asynchronous functions use {@link convertAsyncZodFunctionToSchema}. + * + * @throws `Encountered Promise during synchronous parse. Use .parseAsync() instead.` if used with async functions. + * + * @param factory `z.function({ input: [...], output: ... })` + * @returns Zod schema with compile-time and runtime validations. + */ +export function convertSyncZodFunctionToSchema( + factory: T, +) { + return z + .custom() + .transform((arg, ctx) => { + if (typeof arg !== 'function') { + ctx.addIssue(`Expected function, received ${typeof arg}`); + return z.NEVER; + } + return factory.implement(arg as Parameters[0]); + }) + .meta({ + // enables zod2md to include function signature in docs + $ZodFunction: factory, + }); +} diff --git a/packages/models/src/lib/implementation/function.unit.test.ts b/packages/models/src/lib/implementation/function.unit.test.ts new file mode 100644 index 000000000..c04d4c8be --- /dev/null +++ b/packages/models/src/lib/implementation/function.unit.test.ts @@ -0,0 +1,137 @@ +import { z } from 'zod/v4'; +import { + convertAsyncZodFunctionToSchema, + convertSyncZodFunctionToSchema, +} from './function.js'; + +describe('convertAsyncZodFunctionToSchema', () => { + it('should create a Zod schema', () => { + expect( + convertAsyncZodFunctionToSchema( + z.function({ output: z.promise(z.url()) }), + ), + ).toBeInstanceOf(z.ZodType); + }); + + it('should accept a valid function', async () => { + const schema = convertAsyncZodFunctionToSchema( + z.function({ input: [z.string()], output: z.promise(z.int()) }), + ); + + const fn = (input: string) => Promise.resolve(input.length); + + expect(() => schema.parse(fn)).not.toThrow(); + await expect(schema.parse(fn)('')).resolves.toBe(0); + }); + + it('should reject a non-function value', () => { + const schema = convertAsyncZodFunctionToSchema( + z.function({ input: [z.string()], output: z.promise(z.int()) }), + ); + + expect(() => schema.parse(123)).toThrow( + 'Expected function, received number', + ); + }); + + it('should validate function arguments at runtime', async () => { + const schema = convertAsyncZodFunctionToSchema( + z.function({ input: [z.string()], output: z.promise(z.int()) }), + ); + + await expect( + schema.parse((input: string) => Promise.resolve(input.length))( + // @ts-expect-error testing invalid argument type + null, + ), + ).rejects.toThrow('expected string, received null'); + }); + + it('should validate function return type at runtime', async () => { + const schema = convertAsyncZodFunctionToSchema( + z.function({ input: [z.string()], output: z.promise(z.int()) }), + ); + + await expect( + schema.parse(() => Promise.resolve(Math.random()))(''), + ).rejects.toThrow('expected int, received number'); + }); + + it('should add $ZodFunction metadata for zod2md', () => { + const factory = z.function({ + input: [z.string()], + output: z.promise(z.int()), + }); + const schema = convertAsyncZodFunctionToSchema(factory); + + expect(z.globalRegistry.get(schema)).toHaveProperty( + '$ZodFunction', + factory, + ); + }); +}); + +describe('convertSyncZodFunctionToSchema', () => { + it('should create a Zod schema', () => { + expect( + convertSyncZodFunctionToSchema(z.function({ output: z.url() })), + ).toBeInstanceOf(z.ZodType); + }); + + it('should accept a valid function', () => { + const schema = convertSyncZodFunctionToSchema( + z.function({ input: [z.string()], output: z.int() }), + ); + + const fn = (input: string) => input.length; + + expect(() => schema.parse(fn)).not.toThrow(); + expect(schema.parse(fn)('')).toBe(0); + }); + + it('should reject a non-function value', () => { + const schema = convertSyncZodFunctionToSchema( + z.function({ input: [z.string()], output: z.int() }), + ); + + expect(() => schema.parse(123)).toThrow( + 'Expected function, received number', + ); + }); + + it('should validate function arguments at runtime', () => { + const schema = convertSyncZodFunctionToSchema( + z.function({ input: [z.string()], output: z.int() }), + ); + + expect(() => + schema.parse((input: string) => input.length)( + // @ts-expect-error testing invalid argument type + null, + ), + ).toThrow('expected string, received null'); + }); + + it('should validate function return type at runtime', () => { + const schema = convertSyncZodFunctionToSchema( + z.function({ input: [z.string()], output: z.int() }), + ); + + expect(() => schema.parse(() => Math.random())('')).toThrow( + 'expected int, received number', + ); + }); + + it('should add $ZodFunction metadata for zod2md', () => { + const factory = z.function({ + input: [z.string()], + output: z.int(), + }); + const schema = convertSyncZodFunctionToSchema(factory); + + expect(z.globalRegistry.get(schema)).toHaveProperty( + '$ZodFunction', + factory, + ); + }); +}); diff --git a/packages/models/src/lib/implementation/schemas.ts b/packages/models/src/lib/implementation/schemas.ts index 5e2a7b1e4..720dec5c4 100644 --- a/packages/models/src/lib/implementation/schemas.ts +++ b/packages/models/src/lib/implementation/schemas.ts @@ -1,5 +1,12 @@ import { MATERIAL_ICONS } from 'vscode-material-icons'; -import { type ZodObject, type ZodOptional, type ZodString, z } from 'zod'; +import { + ZodError, + type ZodIssue, + type ZodObject, + type ZodOptional, + type ZodString, + z, +} from 'zod'; import { MAX_DESCRIPTION_LENGTH, MAX_SLUG_LENGTH, @@ -25,26 +32,28 @@ export function executionMetaSchema( }, ) { return z.object({ - date: z.string({ description: options.descriptionDate }), - duration: z.number({ description: options.descriptionDuration }), + date: z.string().describe(options.descriptionDate), + duration: z.number().describe(options.descriptionDuration), }); } /** Schema for a slug of a categories, plugins or audits. */ export const slugSchema = z - .string({ description: 'Unique ID (human-readable, URL-safe)' }) + .string() .regex(slugRegex, { message: 'The slug has to follow the pattern [0-9a-z] followed by multiple optional groups of -[0-9a-z]. e.g. my-slug', }) .max(MAX_SLUG_LENGTH, { message: `The slug can be max ${MAX_SLUG_LENGTH} characters long`, - }); + }) + .describe('Unique ID (human-readable, URL-safe)'); /** Schema for a general description property */ export const descriptionSchema = z - .string({ description: 'Description (markdown)' }) + .string() .max(MAX_DESCRIPTION_LENGTH) + .describe('Description (markdown)') .optional(); /* Schema for a URL */ @@ -58,29 +67,32 @@ export const docsUrlSchema = urlSchema .catch(ctx => { // if only URL validation fails, supress error since this metadata is optional anyway if ( - ctx.error.errors.length === 1 && - ctx.error.errors[0]?.code === 'invalid_string' && - ctx.error.errors[0].validation === 'url' + ctx.issues.length === 1 && + (ctx.issues[0]?.errors as ZodIssue[][]) + .flat() + .some( + error => error.code === 'invalid_format' && error.format === 'url', + ) ) { - console.warn(`Ignoring invalid docsUrl: ${ctx.input}`); + console.warn(`Ignoring invalid docsUrl: ${ctx.value}`); return ''; } - throw ctx.error; + throw new ZodError(ctx.error.issues); }) .describe('Documentation site'); /** Schema for a title of a plugin, category and audit */ export const titleSchema = z - .string({ description: 'Descriptive name' }) - .max(MAX_TITLE_LENGTH); + .string() + .max(MAX_TITLE_LENGTH) + .describe('Descriptive name'); /** Schema for score of audit, category or group */ export const scoreSchema = z - .number({ - description: 'Value between 0 and 1', - }) + .number() .min(0) - .max(1); + .max(1) + .describe('Value between 0 and 1'); /** Schema for a property indicating whether an entity is filtered out */ export const isSkippedSchema = z.boolean().optional(); @@ -103,23 +115,21 @@ export function metaSchema(options?: { description, isSkippedDescription, } = options ?? {}; - return z.object( - { - title: titleDescription - ? titleSchema.describe(titleDescription) - : titleSchema, - description: descriptionDescription - ? descriptionSchema.describe(descriptionDescription) - : descriptionSchema, - docsUrl: docsUrlDescription - ? docsUrlSchema.describe(docsUrlDescription) - : docsUrlSchema, - isSkipped: isSkippedDescription - ? isSkippedSchema.describe(isSkippedDescription) - : isSkippedSchema, - }, - { description }, - ); + const meta = z.object({ + title: titleDescription + ? titleSchema.describe(titleDescription) + : titleSchema, + description: descriptionDescription + ? descriptionSchema.describe(descriptionDescription) + : descriptionSchema, + docsUrl: docsUrlDescription + ? docsUrlSchema.describe(docsUrlDescription) + : docsUrlSchema, + isSkipped: isSkippedDescription + ? isSkippedSchema.describe(isSkippedDescription) + : isSkippedSchema, + }); + return description ? meta.describe(description) : meta; } /** Schema for a generalFilePath */ @@ -142,21 +152,21 @@ export const positiveIntSchema = z.number().int().positive(); export const nonnegativeNumberSchema = z.number().nonnegative(); -export function packageVersionSchema(options?: { - versionDescription?: string; - required?: TRequired; -}) { +export function packageVersionSchema< + TRequired extends boolean = false, +>(options?: { versionDescription?: string; required?: TRequired }) { const { versionDescription = 'NPM version of the package', required } = options ?? {}; - const packageSchema = z.string({ description: 'NPM package name' }); - const versionSchema = z.string({ description: versionDescription }); - return z.object( - { + const packageSchema = z.string().describe('NPM package name'); + const versionSchema = z.string().describe(versionDescription); + return z + .object({ packageName: required ? packageSchema : packageSchema.optional(), version: required ? versionSchema : versionSchema.optional(), - }, - { description: 'NPM package name and version of a published package' }, - ) as ZodObject<{ + }) + .describe( + 'NPM package name and version of a published package', + ) as ZodObject<{ packageName: TRequired extends true ? ZodString : ZodOptional; version: TRequired extends true ? ZodString : ZodOptional; }>; @@ -171,13 +181,12 @@ export function weightedRefSchema( description: string, slugDescription: string, ) { - return z.object( - { + return z + .object({ slug: slugSchema.describe(slugDescription), weight: weightSchema.describe('Weight used to calculate score'), - }, - { description }, - ); + }) + .describe(description); } export type WeightedRef = z.infer>; @@ -185,37 +194,27 @@ export type WeightedRef = z.infer>; export function scorableSchema>( description: string, refSchema: T, - duplicateCheckFn: (metrics: z.infer[]) => false | string[], - duplicateMessageFn: (metrics: z.infer[]) => string, + duplicateCheckFn: z.core.CheckFn[]>, ) { - return z.object( - { + return z + .object({ slug: slugSchema.describe('Human-readable unique ID, e.g. "performance"'), refs: z .array(refSchema) .min(1, { message: 'In a category, there has to be at least one ref' }) // refs are unique - .refine( - refs => !duplicateCheckFn(refs), - refs => ({ - message: duplicateMessageFn(refs), - }), - ) + .check(duplicateCheckFn) // category weights are correct - .refine(hasNonZeroWeightedRef, refs => { - const affectedRefs = refs.map(ref => ref.slug).join(', '); - return { - message: `In a category, there has to be at least one ref with weight > 0. Affected refs: ${affectedRefs}`, - }; + .refine(hasNonZeroWeightedRef, { + error: 'A category must have at least 1 ref with weight > 0.', }), - }, - { description }, - ); + }) + .describe(description); } -export const materialIconSchema = z.enum(MATERIAL_ICONS, { - description: 'Icon from VSCode Material Icons extension', -}); +export const materialIconSchema = z + .enum(MATERIAL_ICONS) + .describe('Icon from VSCode Material Icons extension'); export type MaterialIcon = z.infer; type Ref = { weight: number }; @@ -224,12 +223,11 @@ function hasNonZeroWeightedRef(refs: Ref[]) { return refs.reduce((acc, { weight }) => weight + acc, 0) !== 0; } -export const filePositionSchema = z.object( - { +export const filePositionSchema = z + .object({ startLine: positiveIntSchema.describe('Start line'), startColumn: positiveIntSchema.describe('Start column').optional(), endLine: positiveIntSchema.describe('End line').optional(), endColumn: positiveIntSchema.describe('End column').optional(), - }, - { description: 'Location in file' }, -); + }) + .describe('Location in file'); diff --git a/packages/models/src/lib/implementation/schemas.unit.test.ts b/packages/models/src/lib/implementation/schemas.unit.test.ts index 4370cc963..a72640350 100644 --- a/packages/models/src/lib/implementation/schemas.unit.test.ts +++ b/packages/models/src/lib/implementation/schemas.unit.test.ts @@ -61,6 +61,8 @@ describe('docsUrlSchema', () => { }); it('should throw if not a string', () => { - expect(() => docsUrlSchema.parse(false)).toThrow('invalid_type'); + expect(() => docsUrlSchema.parse(false)).toThrow( + 'Invalid input: expected string, received boolean', + ); }); }); diff --git a/packages/models/src/lib/implementation/utils.ts b/packages/models/src/lib/implementation/utils.ts index 2bd6142e9..250558516 100644 --- a/packages/models/src/lib/implementation/utils.ts +++ b/packages/models/src/lib/implementation/utils.ts @@ -43,11 +43,15 @@ export function hasMissingStrings( return nonExisting.length === 0 ? false : nonExisting; } +export function formatSlugsList(slugs: string[]): string { + return slugs.map(slug => `"${slug}"`).join(', '); +} + /** * helper for error items */ export function errorItems( - items: string[] | false, + items: string[], transform: (itemArr: string[]) => string = itemArr => itemArr.join(', '), ): string { return transform(items || []); @@ -72,12 +76,10 @@ export function getMissingRefsForCategories( } const auditRefsFromCategory = categories.flatMap(({ refs }) => - refs - .filter(({ type }) => type === 'audit') - .map(({ plugin, slug }) => `${plugin}/${slug}`), + refs.filter(({ type }) => type === 'audit').map(formatRef), ); - const auditRefsFromPlugins = plugins.flatMap(({ audits, slug: pluginSlug }) => - audits.map(({ slug }) => `${pluginSlug}/${slug}`), + const auditRefsFromPlugins = plugins.flatMap(({ audits, slug: plugin }) => + audits.map(({ slug }) => formatRef({ type: 'audit', plugin, slug })), ); const missingAuditRefs = hasMissingStrings( auditRefsFromCategory, @@ -85,15 +87,12 @@ export function getMissingRefsForCategories( ); const groupRefsFromCategory = categories.flatMap(({ refs }) => - refs - .filter(({ type }) => type === 'group') - .map(({ plugin, slug }) => `${plugin}#${slug} (group)`), + refs.filter(({ type }) => type === 'group').map(formatRef), ); - const groupRefsFromPlugins = plugins.flatMap( - ({ groups, slug: pluginSlug }) => - Array.isArray(groups) - ? groups.map(({ slug }) => `${pluginSlug}#${slug} (group)`) - : [], + const groupRefsFromPlugins = plugins.flatMap(({ groups, slug: plugin }) => + Array.isArray(groups) + ? groups.map(({ slug }) => formatRef({ type: 'group', plugin, slug })) + : [], ); const missingGroupRefs = hasMissingStrings( groupRefsFromCategory, @@ -107,12 +106,27 @@ export function getMissingRefsForCategories( return missingRefs.length > 0 ? missingRefs : false; } -export function missingRefsForCategoriesErrorMsg( - categories: CategoryConfig[] | undefined, - plugins: PluginConfig[] | PluginReport[], -) { +export function findMissingSlugsInCategoryRefs({ + categories, + plugins, +}: { + categories?: CategoryConfig[]; + plugins: PluginConfig[] | PluginReport[]; +}) { const missingRefs = getMissingRefsForCategories(categories, plugins); - return `The following category references need to point to an audit or group: ${errorItems( - missingRefs, - )}`; + return ( + missingRefs && { + message: `Category references audits or groups which don't exist: ${missingRefs.join( + ', ', + )}`, + } + ); +} + +export function formatRef(ref: { + type: 'audit' | 'group'; + plugin: string; + slug: string; +}): string { + return `${ref.type} "${ref.slug}" (plugin "${ref.plugin}")`; } diff --git a/packages/models/src/lib/issue.ts b/packages/models/src/lib/issue.ts index c7184f140..378db354e 100644 --- a/packages/models/src/lib/issue.ts +++ b/packages/models/src/lib/issue.ts @@ -2,18 +2,19 @@ import { z } from 'zod'; import { MAX_ISSUE_MESSAGE_LENGTH } from './implementation/limits.js'; import { sourceFileLocationSchema } from './source.js'; -export const issueSeveritySchema = z.enum(['info', 'warning', 'error'], { - description: 'Severity level', -}); +export const issueSeveritySchema = z + .enum(['info', 'warning', 'error']) + .describe('Severity level'); export type IssueSeverity = z.infer; -export const issueSchema = z.object( - { + +export const issueSchema = z + .object({ message: z - .string({ description: 'Descriptive error message' }) - .max(MAX_ISSUE_MESSAGE_LENGTH), + .string() + .max(MAX_ISSUE_MESSAGE_LENGTH) + .describe('Descriptive error message'), severity: issueSeveritySchema, source: sourceFileLocationSchema.optional(), - }, - { description: 'Issue information' }, -); + }) + .describe('Issue information'); export type Issue = z.infer; diff --git a/packages/models/src/lib/issue.unit.test.ts b/packages/models/src/lib/issue.unit.test.ts index 0ffcf9683..4a71f9250 100644 --- a/packages/models/src/lib/issue.unit.test.ts +++ b/packages/models/src/lib/issue.unit.test.ts @@ -39,7 +39,9 @@ describe('issueSchema', () => { message: 'Use const instead of let.', severity: 'critical', }), - ).toThrow('Invalid enum value'); + ).toThrow( + String.raw`Invalid option: expected one of \"info\"|\"warning\"|\"error\"`, + ); }); it('should throw for invalid file position', () => { @@ -52,6 +54,6 @@ describe('issueSchema', () => { position: { startLine: 0, endLine: 3 }, }, } satisfies Issue), - ).toThrow('Number must be greater than 0'); + ).toThrow('Too small: expected number to be >0'); }); }); diff --git a/packages/models/src/lib/persist-config.unit.test.ts b/packages/models/src/lib/persist-config.unit.test.ts index 9693217bc..77ca8157b 100644 --- a/packages/models/src/lib/persist-config.unit.test.ts +++ b/packages/models/src/lib/persist-config.unit.test.ts @@ -34,7 +34,7 @@ describe('persistConfigSchema', () => { it('should throw for an invalid format', () => { expect(() => persistConfigSchema.parse({ format: ['html'] })).toThrow( - 'Invalid enum value', + String.raw`Invalid option: expected one of \"json\"|\"md\"`, ); }); }); diff --git a/packages/models/src/lib/plugin-config.ts b/packages/models/src/lib/plugin-config.ts index b977e7ec4..3697ed4cf 100644 --- a/packages/models/src/lib/plugin-config.ts +++ b/packages/models/src/lib/plugin-config.ts @@ -1,17 +1,18 @@ import { z } from 'zod'; -import { pluginAuditsSchema } from './audit.js'; -import { groupsSchema } from './group.js'; +import { type Audit, pluginAuditsSchema } from './audit.js'; +import { type Group, groupsSchema } from './group.js'; +import { createCheck } from './implementation/checks.js'; import { materialIconSchema, metaSchema, packageVersionSchema, slugSchema, } from './implementation/schemas.js'; -import { errorItems, hasMissingStrings } from './implementation/utils.js'; +import { formatSlugsList, hasMissingStrings } from './implementation/utils.js'; import { runnerConfigSchema, runnerFunctionSchema } from './runner-config.js'; export const pluginContextSchema = z - .record(z.unknown()) + .record(z.string(), z.unknown()) .optional() .describe('Plugin-specific context data for helpers'); export type PluginContext = z.infer; @@ -39,33 +40,30 @@ export const pluginDataSchema = z.object({ groups: groupsSchema, context: pluginContextSchema, }); -type PluginData = z.infer; export const pluginConfigSchema = pluginMetaSchema .merge(pluginDataSchema) - // every listed group ref points to an audit within the plugin - .refine( - pluginCfg => !getMissingRefsFromGroups(pluginCfg), - pluginCfg => ({ - message: missingRefsFromGroupsErrorMsg(pluginCfg), - }), - ); + .check(createCheck(findMissingSlugsInGroupRefs)); export type PluginConfig = z.infer; -// helper for validator: every listed group ref points to an audit within the plugin -function missingRefsFromGroupsErrorMsg(pluginCfg: PluginData) { - const missingRefs = getMissingRefsFromGroups(pluginCfg); - return `The following group references need to point to an existing audit in this plugin config: ${errorItems( - missingRefs, - )}`; +// every listed group ref points to an audit within the plugin +export function findMissingSlugsInGroupRefs< + T extends { audits: Audit[]; groups?: Group[] }, +>({ audits, groups = [] }: T) { + const missingSlugs = getAuditSlugsFromGroups(audits, groups); + return ( + missingSlugs && { + message: `Group references audits which don't exist in this plugin: ${formatSlugsList( + missingSlugs, + )}`, + } + ); } -function getMissingRefsFromGroups(pluginCfg: PluginData) { +function getAuditSlugsFromGroups(audits: Audit[], groups: Group[]) { return hasMissingStrings( - pluginCfg.groups?.flatMap(({ refs: audits }) => - audits.map(({ slug: ref }) => ref), - ) ?? [], - pluginCfg.audits.map(({ slug }) => slug), + groups.flatMap(({ refs }) => refs.map(({ slug }) => slug)), + audits.map(({ slug }) => slug), ); } diff --git a/packages/models/src/lib/plugin-config.unit.test.ts b/packages/models/src/lib/plugin-config.unit.test.ts index 56c0ddec5..f7409c92a 100644 --- a/packages/models/src/lib/plugin-config.unit.test.ts +++ b/packages/models/src/lib/plugin-config.unit.test.ts @@ -69,7 +69,7 @@ describe('pluginConfigSchema', () => { ], } satisfies PluginConfig), ).toThrow( - 'group references need to point to an existing audit in this plugin config: cyct', + String.raw`Group references audits which don't exist in this plugin: \"cyct\"`, ); }); @@ -90,7 +90,7 @@ describe('pluginConfigSchema', () => { ], } satisfies PluginConfig), ).toThrow( - 'group references need to point to an existing audit in this plugin config: cyct', + String.raw`Group references audits which don't exist in this plugin: \"cyct\"`, ); }); diff --git a/packages/models/src/lib/report.ts b/packages/models/src/lib/report.ts index 72da71453..6042d2481 100644 --- a/packages/models/src/lib/report.ts +++ b/packages/models/src/lib/report.ts @@ -3,18 +3,17 @@ import { auditOutputSchema } from './audit-output.js'; import { auditSchema } from './audit.js'; import { categoryConfigSchema } from './category-config.js'; import { commitSchema } from './commit.js'; -import { type Group, groupSchema } from './group.js'; +import { groupSchema } from './group.js'; +import { createCheck } from './implementation/checks.js'; import { executionMetaSchema, packageVersionSchema, } from './implementation/schemas.js'; +import { findMissingSlugsInCategoryRefs } from './implementation/utils.js'; import { - errorItems, - getMissingRefsForCategories, - hasMissingStrings, - missingRefsForCategoriesErrorMsg, -} from './implementation/utils.js'; -import { pluginMetaSchema } from './plugin-config.js'; + findMissingSlugsInGroupRefs, + pluginMetaSchema, +} from './plugin-config.js'; export const auditReportSchema = auditSchema.merge(auditOutputSchema); export type AuditReport = z.infer; @@ -32,36 +31,10 @@ export const pluginReportSchema = pluginMetaSchema groups: z.array(groupSchema).optional(), }), ) - .refine( - pluginReport => - !getMissingRefsFromGroups(pluginReport.audits, pluginReport.groups ?? []), - pluginReport => ({ - message: missingRefsFromGroupsErrorMsg( - pluginReport.audits, - pluginReport.groups ?? [], - ), - }), - ); + .check(createCheck(findMissingSlugsInGroupRefs)); export type PluginReport = z.infer; -// every listed group ref points to an audit within the plugin report -function missingRefsFromGroupsErrorMsg(audits: AuditReport[], groups: Group[]) { - const missingRefs = getMissingRefsFromGroups(audits, groups); - return `group references need to point to an existing audit in this plugin report: ${errorItems( - missingRefs, - )}`; -} - -function getMissingRefsFromGroups(audits: AuditReport[], groups: Group[]) { - return hasMissingStrings( - groups.flatMap(({ refs: auditRefs }) => - auditRefs.map(({ slug: ref }) => ref), - ), - audits.map(({ slug }) => slug), - ); -} - export const reportSchema = packageVersionSchema({ versionDescription: 'NPM version of the CLI', required: true, @@ -73,23 +46,15 @@ export const reportSchema = packageVersionSchema({ }), ) .merge( - z.object( - { - plugins: z.array(pluginReportSchema).min(1), - categories: z.array(categoryConfigSchema).optional(), - commit: commitSchema - .describe('Git commit for which report was collected') - .nullable(), - }, - { description: 'Collect output data' }, - ), - ) - .refine( - ({ categories, plugins }) => - !getMissingRefsForCategories(categories, plugins), - ({ categories, plugins }) => ({ - message: missingRefsForCategoriesErrorMsg(categories, plugins), + z.object({ + plugins: z.array(pluginReportSchema).min(1), + categories: z.array(categoryConfigSchema).optional(), + commit: commitSchema + .describe('Git commit for which report was collected') + .nullable(), }), - ); + ) + .check(createCheck(findMissingSlugsInCategoryRefs)) + .describe('Collect output data'); export type Report = z.infer; diff --git a/packages/models/src/lib/report.unit.test.ts b/packages/models/src/lib/report.unit.test.ts index 900532aad..e1ab2f916 100644 --- a/packages/models/src/lib/report.unit.test.ts +++ b/packages/models/src/lib/report.unit.test.ts @@ -160,7 +160,7 @@ describe('pluginReportSchema', () => { ], } satisfies PluginReport), ).toThrow( - 'group references need to point to an existing audit in this plugin report: perf-lighthouse', + String.raw`Group references audits which don't exist in this plugin: \"perf-lighthouse\"`, ); }); }); @@ -266,7 +266,7 @@ describe('reportSchema', () => { version: '1.0.1', } satisfies Report), ).toThrow( - 'category references need to point to an audit or group: vitest/vitest-unit-test', + String.raw`Category references audits or groups which don't exist: audit \"vitest-unit-test\" (plugin \"vitest\")`, ); }); }); diff --git a/packages/models/src/lib/reports-diff.ts b/packages/models/src/lib/reports-diff.ts index b02f61264..bc4802169 100644 --- a/packages/models/src/lib/reports-diff.ts +++ b/packages/models/src/lib/reports-diff.ts @@ -28,15 +28,14 @@ function makeArraysComparisonSchema< TDiff extends typeof scorableDiffSchema, TResult extends ZodTypeAny, >(diffSchema: TDiff, resultSchema: TResult, description: string) { - return z.object( - { + return z + .object({ changed: z.array(diffSchema), unchanged: z.array(resultSchema), added: z.array(resultSchema), removed: z.array(resultSchema), - }, - { description }, - ); + }) + .describe(description); } const scorableMetaSchema = z.object({ diff --git a/packages/models/src/lib/runner-config.ts b/packages/models/src/lib/runner-config.ts index 799da8063..a40a0b983 100644 --- a/packages/models/src/lib/runner-config.ts +++ b/packages/models/src/lib/runner-config.ts @@ -1,34 +1,33 @@ -import { z } from 'zod'; +import { z } from 'zod/v4'; import { auditOutputsSchema } from './audit-output.js'; +import { convertAsyncZodFunctionToSchema } from './implementation/function.js'; import { filePathSchema } from './implementation/schemas.js'; -export const outputTransformSchema = z - .function() - .args(z.unknown()) - .returns(z.union([auditOutputsSchema, z.promise(auditOutputsSchema)])); - +export const outputTransformSchema = convertAsyncZodFunctionToSchema( + z.function({ + input: [z.unknown()], + output: z.union([auditOutputsSchema, z.promise(auditOutputsSchema)]), + }), +); export type OutputTransform = z.infer; -export const runnerConfigSchema = z.object( - { - command: z.string({ - description: 'Shell command to execute', - }), - args: z.array(z.string({ description: 'Command arguments' })).optional(), +export const runnerConfigSchema = z + .object({ + command: z.string().describe('Shell command to execute'), + args: z.array(z.string()).describe('Command arguments').optional(), outputFile: filePathSchema.describe('Runner output path'), outputTransform: outputTransformSchema.optional(), configFile: filePathSchema.describe('Runner config path').optional(), - }, - { - description: 'How to execute runner', - }, -); + }) + .describe('How to execute runner'); export type RunnerConfig = z.infer; -export const runnerFunctionSchema = z - .function() - .returns(z.union([auditOutputsSchema, z.promise(auditOutputsSchema)])); +export const runnerFunctionSchema = convertAsyncZodFunctionToSchema( + z.function({ + output: z.union([auditOutputsSchema, z.promise(auditOutputsSchema)]), + }), +); export type RunnerFunction = z.infer; diff --git a/packages/models/src/lib/runner-config.unit.test.ts b/packages/models/src/lib/runner-config.unit.test.ts index 0be4b3406..747236042 100644 --- a/packages/models/src/lib/runner-config.unit.test.ts +++ b/packages/models/src/lib/runner-config.unit.test.ts @@ -64,7 +64,7 @@ describe('runnerFunctionSchema', () => { it('should throw for a non-function argument', () => { expect(() => runnerFunctionSchema.parse({ slug: 'configuration' })).toThrow( - `Expected function,`, + 'Expected function, received object', ); }); }); @@ -86,7 +86,7 @@ describe('outputTransformSchema', () => { it('should throw for a non-function argument', () => { expect(() => outputTransformSchema.parse('configuration')).toThrow( - 'Expected function', + 'Expected function, received string', ); }); }); diff --git a/packages/models/src/lib/source.ts b/packages/models/src/lib/source.ts index 3b69e90ef..e1ca11e6a 100644 --- a/packages/models/src/lib/source.ts +++ b/packages/models/src/lib/source.ts @@ -4,12 +4,11 @@ import { filePositionSchema, } from './implementation/schemas.js'; -export const sourceFileLocationSchema = z.object( - { +export const sourceFileLocationSchema = z + .object({ file: filePathSchema.describe('Relative path to source file in Git repo'), position: filePositionSchema.optional(), - }, - { description: 'Source file location' }, -); + }) + .describe('Source file location'); export type SourceFileLocation = z.infer; diff --git a/packages/models/src/lib/table.ts b/packages/models/src/lib/table.ts index 53d7cd1e4..80b36f3c8 100644 --- a/packages/models/src/lib/table.ts +++ b/packages/models/src/lib/table.ts @@ -1,9 +1,9 @@ import { z } from 'zod'; import { tableCellValueSchema } from './implementation/schemas.js'; -export const tableAlignmentSchema = z.enum(['left', 'center', 'right'], { - description: 'Cell alignment', -}); +export const tableAlignmentSchema = z + .enum(['left', 'center', 'right']) + .describe('Cell alignment'); export type TableAlignment = z.infer; export const tableColumnPrimitiveSchema = tableAlignmentSchema; @@ -16,31 +16,30 @@ export const tableColumnObjectSchema = z.object({ }); export type TableColumnObject = z.infer; -export const tableRowObjectSchema = z.record(tableCellValueSchema, { - description: 'Object row', -}); +export const tableRowObjectSchema = z + .record(z.string(), tableCellValueSchema) + .describe('Object row'); export type TableRowObject = z.infer; -export const tableRowPrimitiveSchema = z.array(tableCellValueSchema, { - description: 'Primitive row', -}); +export const tableRowPrimitiveSchema = z + .array(tableCellValueSchema) + .describe('Primitive row'); export type TableRowPrimitive = z.infer; const tableSharedSchema = z.object({ title: z.string().optional().describe('Display title for table'), }); -const tablePrimitiveSchema = tableSharedSchema.merge( - z.object( - { +const tablePrimitiveSchema = tableSharedSchema + .merge( + z.object({ columns: z.array(tableAlignmentSchema).optional(), rows: z.array(tableRowPrimitiveSchema), - }, - { description: 'Table with primitive rows and optional alignment columns' }, - ), -); -const tableObjectSchema = tableSharedSchema.merge( - z.object( - { + }), + ) + .describe('Table with primitive rows and optional alignment columns'); +const tableObjectSchema = tableSharedSchema + .merge( + z.object({ columns: z .union([ z.array(tableAlignmentSchema), @@ -48,14 +47,10 @@ const tableObjectSchema = tableSharedSchema.merge( ]) .optional(), rows: z.array(tableRowObjectSchema), - }, - { - description: - 'Table with object rows and optional alignment or object columns', - }, - ), -); + }), + ) + .describe('Table with object rows and optional alignment or object columns'); export const tableSchema = (description = 'Table information') => - z.union([tablePrimitiveSchema, tableObjectSchema], { description }); + z.union([tablePrimitiveSchema, tableObjectSchema]).describe(description); export type Table = z.infer>; diff --git a/packages/models/src/lib/table.unit.test.ts b/packages/models/src/lib/table.unit.test.ts index 1e49a40d6..53974888f 100644 --- a/packages/models/src/lib/table.unit.test.ts +++ b/packages/models/src/lib/table.unit.test.ts @@ -23,7 +23,7 @@ describe('tableAlignmentSchema', () => { it('should throw for a invalid enum', () => { const alignment = 'crooked'; expect(() => tableAlignmentSchema.parse(alignment)).toThrow( - 'invalid_enum_value', + String.raw`Invalid option: expected one of \"left\"|\"center\"|\"right\"`, ); }); }); @@ -37,7 +37,7 @@ describe('tableColumnPrimitiveSchema', () => { it('should throw for a invalid enum', () => { const column = 'crooked'; expect(() => tableColumnPrimitiveSchema.parse(column)).toThrow( - 'invalid_enum_value', + String.raw`Invalid option: expected one of \"left\"|\"center\"|\"right\"`, ); }); }); @@ -63,7 +63,7 @@ describe('tableRowPrimitiveSchema', () => { it('should throw for a invalid array', () => { const row = [{}]; expect(() => tableRowPrimitiveSchema.parse(row)).toThrow( - 'Expected string, received object', + 'Invalid input: expected string, received object', ); }); }); @@ -115,7 +115,7 @@ describe('tableSchema', () => { rows: [[[] as unknown as string]], }; expect(() => tableSchema().parse(table)).toThrow( - 'Expected string, received array', + 'Invalid input: expected string, received array', ); }); @@ -124,7 +124,7 @@ describe('tableSchema', () => { rows: [['1', { prop1: '2' }]], }; expect(() => tableSchema().parse(table)).toThrow( - 'Expected string, received object', + 'Invalid input: expected string, received object', ); }); diff --git a/packages/models/src/lib/tree.ts b/packages/models/src/lib/tree.ts index 45477b554..645193320 100644 --- a/packages/models/src/lib/tree.ts +++ b/packages/models/src/lib/tree.ts @@ -1,7 +1,10 @@ import { z } from 'zod'; import { filePositionSchema } from './implementation/schemas.js'; -const basicTreeNodeValuesSchema = z.record(z.union([z.number(), z.string()])); +const basicTreeNodeValuesSchema = z.record( + z.string(), + z.union([z.number(), z.string()]), +); const basicTreeNodeDataSchema = z.object({ name: z.string().min(1).describe('Text label for node'), values: basicTreeNodeValuesSchema @@ -11,9 +14,12 @@ const basicTreeNodeDataSchema = z.object({ export const basicTreeNodeSchema: z.ZodType = basicTreeNodeDataSchema.extend({ - children: z - .lazy(() => z.array(basicTreeNodeSchema).optional()) - .describe('Direct descendants of this node (omit if leaf)'), + get children() { + return z + .array(basicTreeNodeSchema) + .optional() + .describe('Direct descendants of this node (omit if leaf)'); + }, }); export type BasicTreeNode = z.infer & { children?: BasicTreeNode[]; @@ -47,9 +53,12 @@ const coverageTreeNodeDataSchema = z.object({ export const coverageTreeNodeSchema: z.ZodType = coverageTreeNodeDataSchema.extend({ - children: z - .lazy(() => z.array(coverageTreeNodeSchema).optional()) - .describe('Files and folders contained in this folder (omit if file)'), + get children() { + return z + .array(coverageTreeNodeSchema) + .optional() + .describe('Files and folders contained in this folder (omit if file)'); + }, }); export type CoverageTreeNode = z.infer & { children?: CoverageTreeNode[]; diff --git a/packages/models/src/lib/upload-config.ts b/packages/models/src/lib/upload-config.ts index cc36ac0d6..bc68d00db 100644 --- a/packages/models/src/lib/upload-config.ts +++ b/packages/models/src/lib/upload-config.ts @@ -3,19 +3,21 @@ import { slugSchema, urlSchema } from './implementation/schemas.js'; export const uploadConfigSchema = z.object({ server: urlSchema.describe('URL of deployed portal API'), - apiKey: z.string({ - description: + apiKey: z + .string() + .describe( 'API key with write access to portal (use `process.env` for security)', - }), + ), organization: slugSchema.describe( 'Organization slug from Code PushUp portal', ), project: slugSchema.describe('Project slug from Code PushUp portal'), timeout: z - .number({ description: 'Request timeout in minutes (default is 5)' }) + .number() .positive() .int() - .optional(), + .optional() + .describe('Request timeout in minutes (default is 5)'), }); export type UploadConfig = z.infer; diff --git a/packages/models/src/lib/upload-config.unit.test.ts b/packages/models/src/lib/upload-config.unit.test.ts index 422ea67ac..38fd95cb5 100644 --- a/packages/models/src/lib/upload-config.unit.test.ts +++ b/packages/models/src/lib/upload-config.unit.test.ts @@ -21,7 +21,7 @@ describe('uploadConfigSchema', () => { project: 'cli', server: '-invalid-/url', } satisfies UploadConfig), - ).toThrow('Invalid url'); + ).toThrow('Invalid URL'); }); it('should throw for a PascalCase organization name', () => { diff --git a/packages/nx-plugin/package.json b/packages/nx-plugin/package.json index 58385cd41..4e7626a3a 100644 --- a/packages/nx-plugin/package.json +++ b/packages/nx-plugin/package.json @@ -37,6 +37,6 @@ "@nx/devkit": "^17.0.0 || ^18.0.0 || ^19.0.0", "ansis": "^3.3.0", "nx": "^17.0.0 || ^18.0.0 || ^19.0.0", - "zod": "^3.22.4" + "zod": "^4.0.5" } } diff --git a/packages/nx-plugin/src/executors/internal/env.unit.test.ts b/packages/nx-plugin/src/executors/internal/env.unit.test.ts index 548ef1e3b..4f5af94ab 100644 --- a/packages/nx-plugin/src/executors/internal/env.unit.test.ts +++ b/packages/nx-plugin/src/executors/internal/env.unit.test.ts @@ -31,10 +31,10 @@ describe('parseEnv', () => { }); it('should throw for process.env.CP_TIMEOUT option < 0', () => { - expect(() => parseEnv({ CP_TIMEOUT: '-1' })).toThrow('Invalid'); + expect(() => parseEnv({ CP_TIMEOUT: '-1' })).toThrow('Invalid string'); }); it('should throw for invalid URL in process.env.CP_SERVER option', () => { - expect(() => parseEnv({ CP_SERVER: 'httptpt' })).toThrow('Invalid url'); + expect(() => parseEnv({ CP_SERVER: 'httptpt' })).toThrow('Invalid URL'); }); }); diff --git a/packages/plugin-coverage/package.json b/packages/plugin-coverage/package.json index 71a23e216..7c80be6e0 100644 --- a/packages/plugin-coverage/package.json +++ b/packages/plugin-coverage/package.json @@ -39,7 +39,7 @@ "ansis": "^3.3.0", "parse-lcov": "^1.0.4", "yargs": "^17.7.2", - "zod": "^3.22.4" + "zod": "^4.0.5" }, "peerDependencies": { "@nx/devkit": ">=17.0.0", diff --git a/packages/plugin-coverage/src/lib/config.ts b/packages/plugin-coverage/src/lib/config.ts index 6719939c4..4db726ed0 100644 --- a/packages/plugin-coverage/src/lib/config.ts +++ b/packages/plugin-coverage/src/lib/config.ts @@ -6,63 +6,57 @@ export type CoverageType = z.infer; export const coverageResultSchema = z.union([ z.object({ resultsPath: z - .string({ - description: 'Path to coverage results for Nx setup.', - }) - .includes('lcov'), + .string() + .includes('lcov') + .describe('Path to coverage results for Nx setup.'), pathToProject: z - .string({ - description: - 'Path from workspace root to project root. Necessary for LCOV reports which provide a relative path.', - }) + .string() + .describe( + 'Path from workspace root to project root. Necessary for LCOV reports which provide a relative path.', + ) .optional(), }), z - .string({ - description: 'Path to coverage results for a single project setup.', - }) - .includes('lcov'), + .string() + .includes('lcov') + .describe('Path to coverage results for a single project setup.'), ]); export type CoverageResult = z.infer; export const coveragePluginConfigSchema = z.object({ coverageToolCommand: z .object({ - command: z - .string({ description: 'Command to run coverage tool.' }) - .min(1), + command: z.string().min(1).describe('Command to run coverage tool.'), args: z - .array(z.string(), { - description: 'Arguments to be passed to the coverage tool.', - }) - .optional(), + .array(z.string()) + .optional() + .describe('Arguments to be passed to the coverage tool.'), }) .optional(), continueOnCommandFail: z - .boolean({ - description: - 'Continue on coverage tool command failure or error. Defaults to true.', - }) - .default(true), + .boolean() + .default(true) + .describe( + 'Continue on coverage tool command failure or error. Defaults to true.', + ), coverageTypes: z - .array(coverageTypeSchema, { - description: 'Coverage types measured. Defaults to all available types.', - }) + .array(coverageTypeSchema) .min(1) - .default(['function', 'branch', 'line']), + .default(['function', 'branch', 'line']) + .describe('Coverage types measured. Defaults to all available types.'), reports: z - .array(coverageResultSchema, { - description: - 'Path to all code coverage report files. Only LCOV format is supported for now.', - }) - .min(1), + .array(coverageResultSchema) + .min(1) + .describe( + 'Path to all code coverage report files. Only LCOV format is supported for now.', + ), perfectScoreThreshold: z - .number({ - description: - 'Score will be 1 (perfect) for this coverage and above. Score range is 0 - 1.', - }) + .number() .gt(0) .max(1) + .describe( + 'Score will be 1 (perfect) for this coverage and above. Score range is 0 - 1.', + ) .optional(), }); export type CoveragePluginConfig = z.input; diff --git a/packages/plugin-coverage/src/lib/config.unit.test.ts b/packages/plugin-coverage/src/lib/config.unit.test.ts index a5938fa19..517f8231a 100644 --- a/packages/plugin-coverage/src/lib/config.unit.test.ts +++ b/packages/plugin-coverage/src/lib/config.unit.test.ts @@ -71,7 +71,7 @@ describe('coveragePluginConfigSchema', () => { coverageTypes: ['line'], reports: ['coverage/cli/coverage-final.json'], } satisfies CoveragePluginConfig), - ).toThrow(/Invalid input: must include.+lcov/); + ).toThrow(String.raw`Invalid string: must include \"lcov\"`); }); it('throws for missing command', () => { diff --git a/packages/plugin-eslint/package.json b/packages/plugin-eslint/package.json index a7aebf096..b53a77686 100644 --- a/packages/plugin-eslint/package.json +++ b/packages/plugin-eslint/package.json @@ -41,7 +41,7 @@ "@code-pushup/utils": "0.69.5", "@code-pushup/models": "0.69.5", "yargs": "^17.7.2", - "zod": "^3.22.4" + "zod": "^4.0.5" }, "peerDependencies": { "@nx/devkit": ">=17.0.0", diff --git a/packages/plugin-eslint/src/lib/config.ts b/packages/plugin-eslint/src/lib/config.ts index 4a3133d2c..5f7cabeee 100644 --- a/packages/plugin-eslint/src/lib/config.ts +++ b/packages/plugin-eslint/src/lib/config.ts @@ -1,12 +1,13 @@ import { z } from 'zod'; import { toArray } from '@code-pushup/utils'; -const patternsSchema = z.union([z.string(), z.array(z.string()).min(1)], { - description: +const patternsSchema = z + .union([z.string(), z.array(z.string()).min(1)]) + .describe( 'Lint target files. May contain file paths, directory paths or glob patterns', -}); + ); -const eslintrcSchema = z.string({ description: 'Path to ESLint config file' }); +const eslintrcSchema = z.string().describe('Path to ESLint config file'); const eslintTargetObjectSchema = z.object({ eslintrc: eslintrcSchema.optional(), @@ -34,30 +35,26 @@ export type ESLintPluginRunnerConfig = { slugs: string[]; }; -const customGroupRulesSchema = z.union( - [ +const customGroupRulesSchema = z + .union([ z .array(z.string()) .min(1, 'Custom group rules must contain at least 1 element'), - z.record(z.string(), z.number()).refine( - schema => Object.keys(schema).length > 0, - () => ({ - code: 'too_small', - message: 'Custom group rules must contain at least 1 element', + z + .record(z.string(), z.number()) + .refine(schema => Object.keys(schema).length > 0, { + error: 'Custom group rules must contain at least 1 element', }), - ), - ], - { - description: - 'Array of rule IDs with equal weights or object mapping rule IDs to specific weights', - }, -); + ]) + .describe( + 'Array of rule IDs with equal weights or object mapping rule IDs to specific weights', + ); const customGroupSchema = z.object({ - slug: z.string({ description: 'Unique group identifier' }), - title: z.string({ description: 'Group display title' }), - description: z.string({ description: 'Group metadata' }).optional(), - docsUrl: z.string({ description: 'Group documentation site' }).optional(), + slug: z.string().describe('Unique group identifier'), + title: z.string().describe('Group display title'), + description: z.string().describe('Group metadata').optional(), + docsUrl: z.string().describe('Group documentation site').optional(), rules: customGroupRulesSchema, }); export type CustomGroup = z.infer; diff --git a/packages/plugin-eslint/src/lib/eslint-plugin.int.test.ts b/packages/plugin-eslint/src/lib/eslint-plugin.int.test.ts index 245611811..7fee0ef8e 100644 --- a/packages/plugin-eslint/src/lib/eslint-plugin.int.test.ts +++ b/packages/plugin-eslint/src/lib/eslint-plugin.int.test.ts @@ -134,7 +134,7 @@ describe('eslintPlugin', () => { groups: [{ slug: 'type-safety', title: 'Type safety', rules: [] }], }, ), - ).rejects.toThrow(/Custom group rules must contain at least 1 element/); + ).rejects.toThrow('Invalid input'); await expect( eslintPlugin( { @@ -145,14 +145,14 @@ describe('eslintPlugin', () => { groups: [{ slug: 'type-safety', title: 'Type safety', rules: {} }], }, ), - ).rejects.toThrow(/Custom group rules must contain at least 1 element/); + ).rejects.toThrow('Invalid input'); }); it('should throw when invalid parameters provided', async () => { await expect( // @ts-expect-error simulating invalid non-TS config eslintPlugin({ eslintrc: '.eslintrc.json' }), - ).rejects.toThrow(/Invalid input/); + ).rejects.toThrow('Invalid input'); }); it("should throw if eslintrc file doesn't exist", async () => { diff --git a/packages/plugin-js-packages/package.json b/packages/plugin-js-packages/package.json index db7c84e53..45428c1cb 100644 --- a/packages/plugin-js-packages/package.json +++ b/packages/plugin-js-packages/package.json @@ -42,6 +42,6 @@ "build-md": "^0.4.1", "semver": "^7.6.0", "yargs": "^17.7.2", - "zod": "^3.22.4" + "zod": "^4.0.5" } } diff --git a/packages/plugin-js-packages/src/lib/config.ts b/packages/plugin-js-packages/src/lib/config.ts index 75d6c247b..37f79b687 100644 --- a/packages/plugin-js-packages/src/lib/config.ts +++ b/packages/plugin-js-packages/src/lib/config.ts @@ -53,12 +53,12 @@ export function fillAuditLevelMapping( export const jsPackagesPluginConfigSchema = z.object({ checks: z - .array(packageCommandSchema, { - description: - 'Package manager commands to be run. Defaults to both audit and outdated.', - }) + .array(packageCommandSchema) .min(1) - .default(['audit', 'outdated']), + .default(['audit', 'outdated']) + .describe( + 'Package manager commands to be run. Defaults to both audit and outdated.', + ), packageManager: packageManagerIdSchema .describe('Package manager to be used.') .optional(), @@ -67,12 +67,12 @@ export const jsPackagesPluginConfigSchema = z.object({ .min(1) .default(['prod', 'dev']), auditLevelMapping: z - .record(packageAuditLevelSchema, issueSeveritySchema, { - description: - 'Mapping of audit levels to issue severity. Custom mapping or overrides may be entered manually, otherwise has a default preset.', - }) + .partialRecord(packageAuditLevelSchema, issueSeveritySchema) .default(defaultAuditLevelMapping) - .transform(fillAuditLevelMapping), + .transform(fillAuditLevelMapping) + .describe( + 'Mapping of audit levels to issue severity. Custom mapping or overrides may be entered manually, otherwise has a default preset.', + ), packageJsonPath: packageJsonPathSchema, }); diff --git a/packages/plugin-jsdocs/package.json b/packages/plugin-jsdocs/package.json index 2a7a3fdad..9506691af 100644 --- a/packages/plugin-jsdocs/package.json +++ b/packages/plugin-jsdocs/package.json @@ -37,7 +37,7 @@ "dependencies": { "@code-pushup/models": "0.69.5", "@code-pushup/utils": "0.69.5", - "zod": "^3.22.4", + "zod": "^4.0.5", "ts-morph": "^24.0.0" } } diff --git a/packages/plugin-jsdocs/src/lib/config.ts b/packages/plugin-jsdocs/src/lib/config.ts index 2b793591d..cd139c50c 100644 --- a/packages/plugin-jsdocs/src/lib/config.ts +++ b/packages/plugin-jsdocs/src/lib/config.ts @@ -1,8 +1,8 @@ import { z } from 'zod'; -const patternsSchema = z.union([z.string(), z.array(z.string()).min(1)], { - description: 'Glob pattern to match source files to evaluate.', -}); +const patternsSchema = z + .union([z.string(), z.array(z.string()).min(1)]) + .describe('Glob pattern to match source files to evaluate.'); const jsDocsTargetObjectSchema = z .object({ diff --git a/packages/plugin-jsdocs/src/lib/config.unit.test.ts b/packages/plugin-jsdocs/src/lib/config.unit.test.ts index 38c3ddc42..d77dd19ff 100644 --- a/packages/plugin-jsdocs/src/lib/config.unit.test.ts +++ b/packages/plugin-jsdocs/src/lib/config.unit.test.ts @@ -49,7 +49,7 @@ describe('JsDocsPlugin Configuration', () => { jsDocsPluginConfigSchema.parse({ patterns: 123, }), - ).toThrow('Expected array'); + ).toThrow('Invalid input'); }); }); @@ -85,7 +85,7 @@ describe('JsDocsPlugin Configuration', () => { onlyAudits: 'functions-coverage', patterns: ['src/**/*.ts'], }), - ).toThrow('Expected array'); + ).toThrow('Invalid input'); }); it('throws for array with non-string elements', () => { @@ -94,7 +94,7 @@ describe('JsDocsPlugin Configuration', () => { onlyAudits: [123, true], patterns: ['src/**/*.ts'], }), - ).toThrow('Expected string, received number'); + ).toThrow('Invalid input'); }); }); @@ -130,7 +130,7 @@ describe('JsDocsPlugin Configuration', () => { skipAudits: 'functions-coverage', patterns: ['src/**/*.ts'], }), - ).toThrow('Expected array'); + ).toThrow('Invalid input'); }); it('throws for array with non-string elements', () => { @@ -139,7 +139,7 @@ describe('JsDocsPlugin Configuration', () => { skipAudits: [123, true], patterns: ['src/**/*.ts'], }), - ).toThrow('Expected string'); + ).toThrow('Invalid input'); }); }); }); diff --git a/packages/plugin-jsdocs/src/lib/jsdocs-plugin.unit.test.ts b/packages/plugin-jsdocs/src/lib/jsdocs-plugin.unit.test.ts index 1be2c11fb..e6ec0b74f 100644 --- a/packages/plugin-jsdocs/src/lib/jsdocs-plugin.unit.test.ts +++ b/packages/plugin-jsdocs/src/lib/jsdocs-plugin.unit.test.ts @@ -43,11 +43,9 @@ describe('jsDocsPlugin', () => { it('should throw for invalid plugin options', () => { expect(() => - jsDocsPlugin({ - // @ts-expect-error testing invalid config - patterns: 123, - }), - ).toThrow('Expected array, received number'); + // @ts-expect-error testing invalid config + jsDocsPlugin({ patterns: 123 }), + ).toThrow('Invalid input'); }); it('should filter groups', () => { diff --git a/packages/plugin-typescript/package.json b/packages/plugin-typescript/package.json index 323c16a4e..4ccf5c0aa 100644 --- a/packages/plugin-typescript/package.json +++ b/packages/plugin-typescript/package.json @@ -25,7 +25,7 @@ "dependencies": { "@code-pushup/models": "0.69.5", "@code-pushup/utils": "0.69.5", - "zod": "^3.23.8" + "zod": "^4.0.5" }, "peerDependencies": { "typescript": ">=4.0.0" diff --git a/packages/plugin-typescript/src/lib/schema.ts b/packages/plugin-typescript/src/lib/schema.ts index db8a3a395..ceb9c8959 100644 --- a/packages/plugin-typescript/src/lib/schema.ts +++ b/packages/plugin-typescript/src/lib/schema.ts @@ -8,14 +8,12 @@ const auditSlugs = AUDITS.map(({ slug }) => slug) as [ ]; export const typescriptPluginConfigSchema = z.object({ tsconfig: z - .string({ - description: 'Path to a tsconfig file (default is tsconfig.json)', - }) - .default(DEFAULT_TS_CONFIG), + .string() + .default(DEFAULT_TS_CONFIG) + .describe(`Path to a tsconfig file (default is ${DEFAULT_TS_CONFIG})`), onlyAudits: z - .array(z.enum(auditSlugs), { - description: 'Filters TypeScript compiler errors by diagnostic codes', - }) + .array(z.enum(auditSlugs)) + .describe('Filters TypeScript compiler errors by diagnostic codes') .optional(), }); diff --git a/packages/plugin-typescript/src/lib/schema.unit.test.ts b/packages/plugin-typescript/src/lib/schema.unit.test.ts index b23fe6a89..c16f8fed9 100644 --- a/packages/plugin-typescript/src/lib/schema.unit.test.ts +++ b/packages/plugin-typescript/src/lib/schema.unit.test.ts @@ -55,7 +55,9 @@ describe('typescriptPluginConfigSchema', () => { tsconfig, onlyAudits: [123, true], }), - ).toThrow('invalid_type'); + ).toThrow( + String.raw`Invalid option: expected one of \"syntax-errors\"|\"semantic-errors\"|`, + ); }); it('throws for unknown audit slug', () => { @@ -64,6 +66,8 @@ describe('typescriptPluginConfigSchema', () => { tsconfig, onlyAudits: ['unknown-audit'], }), - ).toThrow(/unknown-audit/); + ).toThrow( + String.raw`Invalid option: expected one of \"syntax-errors\"|\"semantic-errors\"|`, + ); }); }); diff --git a/packages/utils/package.json b/packages/utils/package.json index 10af41a00..f6d266c03 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -37,7 +37,6 @@ "multi-progress-bars": "^5.0.3", "semver": "^7.6.0", "simple-git": "^3.20.0", - "zod": "^3.23.8", - "zod-validation-error": "^3.4.0" + "zod": "^4.0.5" } } diff --git a/packages/utils/src/lib/zod-validation.ts b/packages/utils/src/lib/zod-validation.ts index 57a15c638..dec02d5d9 100644 --- a/packages/utils/src/lib/zod-validation.ts +++ b/packages/utils/src/lib/zod-validation.ts @@ -1,11 +1,6 @@ -import { bold, red } from 'ansis'; +import { bold } from 'ansis'; import path from 'node:path'; -import type { z } from 'zod'; -import { - type MessageBuilder, - fromError, - isZodErrorLike, -} from 'zod-validation-error'; +import { ZodError, z } from 'zod'; type SchemaValidationContext = { schemaType: string; @@ -15,43 +10,16 @@ type SchemaValidationContext = { export class SchemaValidationError extends Error { constructor( { schemaType, sourcePath }: SchemaValidationContext, - error: Error, + error: ZodError, ) { - const validationError = fromError(error, { - messageBuilder: zodErrorMessageBuilder, - }); + const formattedError = z.prettifyError(error); const pathDetails = sourcePath ? ` in ${bold(path.relative(process.cwd(), sourcePath))}` : ''; - super( - `Failed parsing ${schemaType}${pathDetails}.\n\n${validationError.message}`, - ); + super(`Failed parsing ${schemaType}${pathDetails}.\n\n${formattedError}`); } } -export function formatErrorPath(errorPath: (string | number)[]): string { - return errorPath - .map((key, index) => { - if (typeof key === 'number') { - return `[${key}]`; - } - return index > 0 ? `.${key}` : key; - }) - .join(''); -} - -const zodErrorMessageBuilder: MessageBuilder = issues => - issues - .map(issue => { - const formattedMessage = red(`${bold(issue.code)}: ${issue.message}`); - const formattedPath = formatErrorPath(issue.path); - if (formattedPath) { - return `Validation error at ${bold(formattedPath)}\n${formattedMessage}\n`; - } - return `${formattedMessage}\n`; - }) - .join('\n'); - export function parseSchema( schema: T, data: z.input, @@ -60,7 +28,7 @@ export function parseSchema( try { return schema.parse(data); } catch (error) { - if (isZodErrorLike(error)) { + if (error instanceof ZodError) { throw new SchemaValidationError({ schemaType, sourcePath }, error); } throw error; diff --git a/packages/utils/src/lib/zod-validation.unit.test.ts b/packages/utils/src/lib/zod-validation.unit.test.ts deleted file mode 100644 index 2c9725a4d..000000000 --- a/packages/utils/src/lib/zod-validation.unit.test.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { formatErrorPath } from './zod-validation.js'; - -describe('formatErrorPath', () => { - it.each([ - [['categories', 1, 'slug'], 'categories[1].slug'], - [['plugins', 2, 'groups', 0, 'refs'], 'plugins[2].groups[0].refs'], - [['refs', 0, 'slug'], 'refs[0].slug'], - [['categories'], 'categories'], - [[], ''], - [['path', 5], 'path[5]'], - ])('should format error path %j as %j', (input, expected) => { - expect(formatErrorPath(input)).toBe(expected); - }); -});