@rtorcato/repo-tooling 3.38.0 → 3.38.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/base/checks.js +4 -2
- package/dist/cli/commands/doctor.js +6 -2
- package/dist/cli/commands/fix.js +10 -6
- package/dist/cli/generators/badges.js +23 -3
- package/dist/cli/utils/detect-language.js +11 -0
- package/dist/languages/js/checks.js +4 -1
- package/dist/languages/js/fixers.js +5 -0
- package/package.json +1 -1
package/dist/base/checks.js
CHANGED
|
@@ -720,7 +720,9 @@ export async function checkPrePushHook(dir, p) {
|
|
|
720
720
|
}
|
|
721
721
|
export async function checkReadmeBadges(dir, audience,
|
|
722
722
|
/** `fix` target that rebuilds the badge block, or null when the language has none. */
|
|
723
|
-
fixTarget
|
|
723
|
+
fixTarget,
|
|
724
|
+
/** The repo's own package name — only its badges can 404; others' resolve fine (#634). */
|
|
725
|
+
name) {
|
|
724
726
|
const check = 'README badges';
|
|
725
727
|
const hint = fixTarget
|
|
726
728
|
? `Run \`npx @rtorcato/repo-tooling fix ${fixTarget}\` to add CI/npm/coverage/license badges`
|
|
@@ -729,7 +731,7 @@ fixTarget) {
|
|
|
729
731
|
const readme = (await fs.pathExists(readmePath)) ? await fs.readFile(readmePath, 'utf8') : '';
|
|
730
732
|
if (audience === 'private') {
|
|
731
733
|
// Only a problem if a private/app repo carries badges that would 404.
|
|
732
|
-
if (readme && hasPublicOnlyBadges(readme)) {
|
|
734
|
+
if (readme && hasPublicOnlyBadges(readme, name)) {
|
|
733
735
|
return {
|
|
734
736
|
check,
|
|
735
737
|
status: 'drift',
|
|
@@ -244,7 +244,7 @@ async function runBaseChecks(dir, lock, opts) {
|
|
|
244
244
|
if (lock?.rules?.mcp?.recommended?.length) {
|
|
245
245
|
results.push(await checkRecommendedMcp(dir, lock.rules.mcp.recommended));
|
|
246
246
|
}
|
|
247
|
-
results.push(await checkReadmeBadges(dir, opts.badges.audience, opts.badges.fixTarget));
|
|
247
|
+
results.push(await checkReadmeBadges(dir, opts.badges.audience, opts.badges.fixTarget, opts.badges.name));
|
|
248
248
|
results.push(await checkCoverageUpload(dir));
|
|
249
249
|
return results;
|
|
250
250
|
}
|
|
@@ -407,7 +407,11 @@ export async function runDoctor(dir, skillsDir) {
|
|
|
407
407
|
}
|
|
408
408
|
results.push(...(await runBaseChecks(targetDir, lock, {
|
|
409
409
|
hooks: jsGitHooksProfile(pkg),
|
|
410
|
-
badges: {
|
|
410
|
+
badges: {
|
|
411
|
+
audience: jsBadgeAudience(pkg),
|
|
412
|
+
fixTarget: 'badges',
|
|
413
|
+
name: typeof pkg?.name === 'string' ? pkg.name : undefined,
|
|
414
|
+
},
|
|
411
415
|
// Same `scripts` gating the fixer applies, or the workflow doctor
|
|
412
416
|
// compares against would reference steps the fixer never writes (#364).
|
|
413
417
|
presetWorkflow: renderGitHubWorkflow(githubJobs(inferProjectConfig(pkg), { scripts: scriptsOf(pkg) })),
|
package/dist/cli/commands/fix.js
CHANGED
|
@@ -489,7 +489,8 @@ export async function fixCommand(target, options = {}) {
|
|
|
489
489
|
// A fixer that refuses (e.g. dependabot, when the existing config carries
|
|
490
490
|
// repo-local `ignore:` rules the template can't reproduce) is a skip, not a
|
|
491
491
|
// crash — the remaining findings still deserve their fixers. The reason goes
|
|
492
|
-
// to stderr so it survives `--json`.
|
|
492
|
+
// to stderr so it survives `--json`. An unexpected error is skipped the same
|
|
493
|
+
// way (#636): rethrowing it killed the walk mid-run with no JSON on stdout.
|
|
493
494
|
let outcome;
|
|
494
495
|
try {
|
|
495
496
|
outcome = await applyFixer(fixer, result, targetDir, pkg, lock, dryRun, silent, {
|
|
@@ -499,12 +500,15 @@ export async function fixCommand(target, options = {}) {
|
|
|
499
500
|
});
|
|
500
501
|
}
|
|
501
502
|
catch (err) {
|
|
502
|
-
if (!(err instanceof FixerAbort))
|
|
503
|
-
throw err;
|
|
504
503
|
actions.push(recordFor(fixer.target, result.check, result.status, 'skipped', [], conflict));
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
504
|
+
if (err instanceof FixerAbort) {
|
|
505
|
+
console.error(chalk.red(` refused — ${err.message}`));
|
|
506
|
+
if (err.hint)
|
|
507
|
+
console.error(chalk.gray(` ${err.hint}`));
|
|
508
|
+
}
|
|
509
|
+
else {
|
|
510
|
+
console.error(chalk.red(` failed — ${err instanceof Error ? err.message : String(err)}`));
|
|
511
|
+
}
|
|
508
512
|
skippedCount++;
|
|
509
513
|
continue;
|
|
510
514
|
}
|
|
@@ -56,9 +56,29 @@ export function buildBadgeBlock(inputs) {
|
|
|
56
56
|
const row = buildBadgeRow(inputs);
|
|
57
57
|
return row ? `${BADGE_START}\n${row}\n${BADGE_END}` : '';
|
|
58
58
|
}
|
|
59
|
-
/**
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
/**
|
|
60
|
+
* True when the text carries npm/bundlephobia/codecov badges (which 404 on
|
|
61
|
+
* private/unpublished). Given `name`, npm/bundlephobia badges count only when
|
|
62
|
+
* they point at that package — a README may advertise *other* packages (#634).
|
|
63
|
+
*/
|
|
64
|
+
export function hasPublicOnlyBadges(text, name) {
|
|
65
|
+
if (/codecov\.io/.test(text))
|
|
66
|
+
return true;
|
|
67
|
+
if (!name)
|
|
68
|
+
return /img\.shields\.io\/npm\/|bundlephobia/.test(text);
|
|
69
|
+
const refs = text.matchAll(/(?:img\.shields\.io\/npm\/[\w-]+|bundlephobia\/[\w-]+|bundlephobia\.com\/package)\/([^\s)"'?#]+)/g);
|
|
70
|
+
for (const [, raw = ''] of refs) {
|
|
71
|
+
let ref = raw;
|
|
72
|
+
try {
|
|
73
|
+
ref = decodeURIComponent(raw);
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
// Malformed escape — compare the raw text.
|
|
77
|
+
}
|
|
78
|
+
if (ref === name || ref.startsWith(`${name}/`))
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
return false;
|
|
62
82
|
}
|
|
63
83
|
/**
|
|
64
84
|
* Insert or refresh the badge block in a README. Replaces an existing delimited
|
|
@@ -5,6 +5,10 @@ import fs from 'fs-extra';
|
|
|
5
5
|
* wins. One language per repo root; multi-language monorepos stay out of scope
|
|
6
6
|
* (#139, reaffirmed in #317 — the audit is root-only, but says so via
|
|
7
7
|
* `detectNestedLanguages`). A dir with no marker → 'unknown' (base checks only).
|
|
8
|
+
*
|
|
9
|
+
* One exception to the order: `Package.swift` beats a `package.json` that ships
|
|
10
|
+
* no JS (no `main`/`exports`/`bin`) — a Swift package whose root `package.json`
|
|
11
|
+
* only drives its docs site is a Swift repo (#633).
|
|
8
12
|
*/
|
|
9
13
|
const MARKERS = [
|
|
10
14
|
['js', ['package.json']],
|
|
@@ -12,7 +16,14 @@ const MARKERS = [
|
|
|
12
16
|
['perl', ['cpanfile', 'Makefile.PL', 'dist.ini']],
|
|
13
17
|
['python', ['pyproject.toml', 'setup.py']],
|
|
14
18
|
];
|
|
19
|
+
/** True when `package.json` declares a JS entry point — the repo ships JS, not just tooling. */
|
|
20
|
+
async function shipsJs(dir) {
|
|
21
|
+
const pkg = await fs.readJson(path.join(dir, 'package.json')).catch(() => null);
|
|
22
|
+
return Boolean(pkg && (pkg.main || pkg.exports || pkg.bin));
|
|
23
|
+
}
|
|
15
24
|
export async function detectLanguage(dir) {
|
|
25
|
+
if ((await fs.pathExists(path.join(dir, 'Package.swift'))) && !(await shipsJs(dir)))
|
|
26
|
+
return 'swift';
|
|
16
27
|
for (const [language, candidates] of MARKERS) {
|
|
17
28
|
for (const candidate of candidates) {
|
|
18
29
|
if (await fs.pathExists(path.join(dir, candidate)))
|
|
@@ -1147,7 +1147,10 @@ export async function checkExportsBuildable(dir, pkg, exec) {
|
|
|
1147
1147
|
}
|
|
1148
1148
|
/** Who this package's README badges are for — feeds the base badge check (#309). */
|
|
1149
1149
|
export function jsBadgeAudience(pkg) {
|
|
1150
|
-
|
|
1150
|
+
// No package.json: nothing here is published, so no npm badge can be ours (#634).
|
|
1151
|
+
if (!pkg)
|
|
1152
|
+
return 'not-applicable';
|
|
1153
|
+
if (pkg.private === true)
|
|
1151
1154
|
return 'private';
|
|
1152
1155
|
return isPublishableLibrary(pkg) ? 'public' : 'not-applicable';
|
|
1153
1156
|
}
|
|
@@ -304,6 +304,11 @@ export const FIXERS = [
|
|
|
304
304
|
canFixDrift: true,
|
|
305
305
|
async run({ targetDir, pkg }) {
|
|
306
306
|
const pkgPath = path.join(targetDir, 'package.json');
|
|
307
|
+
// Refuse before writing any hook: lint-staged lives in package.json, so a
|
|
308
|
+
// repo without a readable one cannot be wired (#636).
|
|
309
|
+
if (!pkg) {
|
|
310
|
+
throw new FixerAbort('no-package-json', 'no readable package.json — husky needs one for lint-staged', 'add a package.json, then re-run');
|
|
311
|
+
}
|
|
307
312
|
const existingLintStaged = pkg?.['lint-staged'] ?? {};
|
|
308
313
|
await generateHuskyConfig(inferProjectConfig(pkg), targetDir);
|
|
309
314
|
const updated = (await fs.readJson(pkgPath));
|
package/package.json
CHANGED