@rtorcato/repo-tooling 3.1.0 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,7 +9,7 @@ import { checkGitIdentity } from '../../base/git-identity.js';
9
9
  import { LOCKFILE_VERSION, readLockfile } from '../utils/lockfile.js';
10
10
  import { declinedInLock, getFixTargetForCheck } from './fix-targets.js';
11
11
  import { checkAiSetup, checkCodeowners, checkCodeQL, checkCommunityHealth, checkCoverageUpload, checkDependabot, checkEditorConfig, checkFile, checkGitHooks, checkGitHubActions, checkGitLabCI, checkPrePushHook, checkReadmeBadges, COMMITLINT_FILE_CHECK, } from '../../base/checks.js';
12
- import { allDeps, checkAreTheTypesWrong, checkDocsSite, checkEnginesNode, checkKnip, checkLintStaged, checkNodeVersionConsistency, checkNodeVersionPin, checkPackageJson, checkPublint, checkSemanticRelease, checkSizeLimit, checkTailwind, checkPnpmWorkspace, checkTreeshakeSetup, checkTurborepo, checkTypedoc, checkVerifyScript, checkVscodeExtensions, evaluateNodeVersion, FILE_CHECKS, findDocsAppDir, jsBadgeAudience, jsGitHooksProfile, readPackageJson, } from '../../languages/js/checks.js';
12
+ import { allDeps, checkAreTheTypesWrong, checkConfigSchemaVersions, checkDocsSite, checkEnginesNode, checkGitDependencies, checkKnip, checkLintStaged, checkNodeVersionConsistency, checkNodeVersionPin, checkPackageJson, checkPublint, checkSemanticRelease, checkSizeLimit, checkTailwind, checkPnpmWorkspace, checkTreeshakeSetup, checkTurborepo, checkTypedoc, checkVerifyScript, checkVscodeExtensions, evaluateNodeVersion, FILE_CHECKS, findDocsAppDir, jsBadgeAudience, jsGitHooksProfile, readPackageJson, } from '../../languages/js/checks.js';
13
13
  export { evaluateNodeVersion };
14
14
  const PACKAGE = '@rtorcato/repo-tooling';
15
15
  // Detects the broken-release-on-protected-main footgun: a workflow that runs
@@ -218,6 +218,8 @@ export async function runDoctor(dir) {
218
218
  results.push(evaluateNodeVersion(process.version));
219
219
  results.push(checkPackageJson(pkg));
220
220
  results.push(checkEnginesNode(pkg));
221
+ results.push(await checkConfigSchemaVersions(targetDir, pkg));
222
+ results.push(checkGitDependencies(pkg));
221
223
  results.push(await checkVscodeExtensions(targetDir));
222
224
  results.push(await checkNodeVersionPin(targetDir));
223
225
  results.push(await checkNodeVersionConsistency(targetDir, pkg));
@@ -947,3 +947,161 @@ export async function checkTailwind(dir, pkg) {
947
947
  hint: 'Run `npx @rtorcato/repo-tooling fix tailwind` to scaffold the v4 PostCSS wiring',
948
948
  };
949
949
  }
950
+ /**
951
+ * Both checks below read `package.json` alone — no network, no lockfile. They
952
+ * report `optional-missing` rather than `drift`: each is a policy call a repo
953
+ * can legitimately decide against, so they surface in doctor's output and can
954
+ * be declined in `.repo-tooling.json`, but neither fails a build.
955
+ */
956
+ /** `[major, minor, patch]` floor of a range, or null when it hasn't got one. */
957
+ export function rangeFloor(range) {
958
+ const m = /^[\s^~>=<v]*(\d+)\.(\d+)\.(\d+)/.exec(range);
959
+ return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
960
+ }
961
+ /**
962
+ * Major.minor only — patch is deliberately ignored. New config keys and new
963
+ * behaviour arrive in minors; a dev floor five patches above the peer floor
964
+ * (`^2.5.0` vs `^2.5.5`) is just a routine bump and flagging it would make
965
+ * this check fire on almost every package.
966
+ */
967
+ function compareFloor(a, b) {
968
+ return a[0] - b[0] || a[1] - b[1];
969
+ }
970
+ /**
971
+ * Config files whose `$schema` URL carries the tool version the config is
972
+ * written for, and the package that reads them. One entry per tool; adding
973
+ * another is a line here.
974
+ */
975
+ const SCHEMA_CONFIGS = [
976
+ { files: ['biome.json', 'biome.jsonc'], host: 'biomejs.dev', pkg: '@biomejs/biome' },
977
+ ];
978
+ /** The version out of `https://biomejs.dev/schemas/2.5.0/schema.json`. */
979
+ export function schemaUrlVersion(url) {
980
+ const m = /\/(\d+\.\d+\.\d+)\//.exec(url);
981
+ return m?.[1] ? rangeFloor(m[1]) : null;
982
+ }
983
+ /**
984
+ * `"$schema": "..."` without parsing the file. Biome configs may be JSONC, and
985
+ * a comment is enough to break `JSON.parse` — the one field this needs is
986
+ * cheaper and safer to read directly.
987
+ */
988
+ function readSchemaUrl(contents) {
989
+ return /"\$schema"\s*:\s*"([^"]+)"/.exec(contents)?.[1] ?? null;
990
+ }
991
+ /**
992
+ * A shipped config written for a newer tool version than the package claims to
993
+ * support (#330). The `$schema` URL is an explicit, machine-readable statement
994
+ * of which version the config targets, so comparing it against the declared
995
+ * dependency floor is exact — no heuristics, no false positives.
996
+ *
997
+ * This is the #330 defect precisely: `tooling/biome/biome.json` carries
998
+ * `$schema` 2.5.0 and uses `linter.rules.preset`, a 2.5 key, while
999
+ * `peerDependencies` advertised `@biomejs/biome: ^2.0.0`. Consumers on 2.0–2.4
1000
+ * got `Found an unknown key \`preset\`` with nothing pointing at the range.
1001
+ *
1002
+ * It reads the same way in a consuming repo: a `biome.json` targeting 2.5.0
1003
+ * with `@biomejs/biome: ^2.3.0` in devDependencies is the identical failure,
1004
+ * one level down.
1005
+ *
1006
+ * The floor is what matters, not the ceiling — `^2.0.0` resolves to the newest
1007
+ * 2.x today, so the repo's own install works and only consumers pinned lower
1008
+ * break. That is exactly why this goes unnoticed.
1009
+ */
1010
+ export async function checkConfigSchemaVersions(dir, pkg) {
1011
+ const check = 'Config schema versions';
1012
+ // peerDependencies is the contract a publisher offers; devDependencies is
1013
+ // what a leaf repo actually installs. Prefer the former when present.
1014
+ const declared = {
1015
+ ...(pkg?.dependencies ?? {}),
1016
+ ...(pkg?.devDependencies ?? {}),
1017
+ ...(pkg?.peerDependencies ?? {}),
1018
+ };
1019
+ const mismatches = [];
1020
+ let checked = 0;
1021
+ for (const spec of SCHEMA_CONFIGS) {
1022
+ for (const file of spec.files) {
1023
+ const filepath = path.join(dir, file);
1024
+ if (!(await fs.pathExists(filepath)))
1025
+ continue;
1026
+ const url = readSchemaUrl(await fs.readFile(filepath, 'utf8'));
1027
+ if (!url?.includes(spec.host))
1028
+ continue;
1029
+ const schemaVersion = schemaUrlVersion(url);
1030
+ const range = declared[spec.pkg];
1031
+ if (!schemaVersion || !range)
1032
+ continue;
1033
+ const floor = rangeFloor(range);
1034
+ if (!floor)
1035
+ continue;
1036
+ checked++;
1037
+ if (compareFloor(schemaVersion, floor) > 0) {
1038
+ const v = schemaVersion.join('.');
1039
+ mismatches.push(`${file} targets ${spec.pkg} ${v} but the range is ${range}`);
1040
+ }
1041
+ }
1042
+ }
1043
+ if (checked === 0) {
1044
+ return { check, status: 'ok', detail: 'no versioned config schemas to compare' };
1045
+ }
1046
+ if (mismatches.length === 0) {
1047
+ return {
1048
+ check,
1049
+ status: 'ok',
1050
+ detail: `${checked} config schema${checked === 1 ? '' : 's'} within the declared version range`,
1051
+ };
1052
+ }
1053
+ return {
1054
+ check,
1055
+ status: 'optional-missing',
1056
+ detail: `${mismatches.length} config${mismatches.length === 1 ? '' : 's'} written for a newer tool than declared: ${mismatches.join('; ')}`,
1057
+ hint: 'Raise the dependency floor to the version the config targets, or rewrite the config for the oldest version supported. Anyone resolving below the schema version gets a config-parse error that never mentions the version range.',
1058
+ };
1059
+ }
1060
+ /** npm git specifiers, including the bare `owner/repo` GitHub shorthand. */
1061
+ const GIT_PROTOCOL = /^(?:github|gitlab|bitbucket|gist):|^git\+|^git:\/\//;
1062
+ const GITHUB_SHORTHAND = /^[A-Za-z0-9][\w.-]*\/[A-Za-z0-9][\w.-]*(?:#.*)?$/;
1063
+ export function isGitSpecifier(spec) {
1064
+ return GIT_PROTOCOL.test(spec) || GITHUB_SHORTHAND.test(spec);
1065
+ }
1066
+ /**
1067
+ * A git dependency with no `#ref` (#332). The package manager resolves it once
1068
+ * and pins the commit in the lockfile, so it never moves again until somebody
1069
+ * re-resolves by hand — with no version mismatch, no Dependabot PR, and no
1070
+ * signal of any kind that it has gone stale.
1071
+ *
1072
+ * This is how the docs homepage kept advertising `@rtorcato/js-tooling` for
1073
+ * days after the rename: `github:rtorcato/shared-docs` had been pinned to a
1074
+ * pre-rename commit and nothing could tell.
1075
+ */
1076
+ export function checkGitDependencies(pkg) {
1077
+ const check = 'Git dependencies';
1078
+ const fields = ['dependencies', 'devDependencies', 'optionalDependencies'];
1079
+ const refless = [];
1080
+ let total = 0;
1081
+ for (const field of fields) {
1082
+ const deps = pkg?.[field] ?? {};
1083
+ for (const [name, spec] of Object.entries(deps)) {
1084
+ if (typeof spec !== 'string' || !isGitSpecifier(spec))
1085
+ continue;
1086
+ total++;
1087
+ if (!spec.includes('#'))
1088
+ refless.push(`${name} (${spec})`);
1089
+ }
1090
+ }
1091
+ if (total === 0) {
1092
+ return { check, status: 'ok', detail: 'no git dependencies' };
1093
+ }
1094
+ if (refless.length === 0) {
1095
+ return {
1096
+ check,
1097
+ status: 'ok',
1098
+ detail: `${total} git dependenc${total === 1 ? 'y' : 'ies'}, all with an explicit ref`,
1099
+ };
1100
+ }
1101
+ return {
1102
+ check,
1103
+ status: 'optional-missing',
1104
+ detail: `${refless.length} git dependenc${refless.length === 1 ? 'y' : 'ies'} with no ref — pinned to whatever commit was current at install time: ${refless.join('; ')}`,
1105
+ hint: 'Prefer publishing to a registry and depending on a semver range. Failing that, add an explicit ref — `#semver:^1.2.0` is strongest, `#main` at least makes the intent legible and `pnpm update` meaningful.',
1106
+ };
1107
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rtorcato/repo-tooling",
3
- "version": "3.1.0",
3
+ "version": "3.2.0",
4
4
  "description": "One CLI to scaffold, audit and fix your repo's whole toolchain — linting, tests, commits, releases & CI.",
5
5
  "type": "module",
6
6
  "keywords": [