@rtorcato/repo-tooling 3.4.0 → 3.5.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.
@@ -1,6 +1,29 @@
1
1
  import path from 'node:path';
2
2
  import fs from 'fs-extra';
3
3
  import { BADGE_START, hasPublicOnlyBadges } from '../cli/generators/badges.js';
4
+ import { detectNestedLanguages } from '../cli/utils/detect-language.js';
5
+ /**
6
+ * Root-only detection is the decision (#317), not an oversight — but it used to
7
+ * be a *silent* one: a Swift repo with a TypeScript docs app audited as Swift
8
+ * and never mentioned the half it skipped. This says so.
9
+ *
10
+ * Always `ok`. Nesting a second language is a legitimate repo shape, so failing
11
+ * on it would break every monorepo's CI to report a limitation of ours.
12
+ */
13
+ export async function checkNestedLanguages(dir, rootLanguage) {
14
+ const check = 'Monorepo';
15
+ const nested = await detectNestedLanguages(dir, rootLanguage);
16
+ if (nested.length === 0) {
17
+ return { check, status: 'ok', detail: 'single-language repo' };
18
+ }
19
+ const summary = nested.map((n) => `${n.dir} (${n.language})`).join(', ');
20
+ return {
21
+ check,
22
+ status: 'ok',
23
+ detail: `auditing the root only — ${summary} ${nested.length === 1 ? 'is' : 'are'} not audited`,
24
+ hint: 'Run doctor with `--directory <path>` to audit a nested project on its own',
25
+ };
26
+ }
4
27
  export async function checkFile(dir, spec) {
5
28
  for (const candidate of spec.candidates) {
6
29
  const filepath = path.join(dir, candidate);
@@ -12,7 +12,7 @@ import { checkGitHubSettings } from '../../base/github-settings.js';
12
12
  import { checkGitIdentity } from '../../base/git-identity.js';
13
13
  import { LOCKFILE_VERSION, readLockfile } from '../utils/lockfile.js';
14
14
  import { declinedInLock, getFixTargetForCheck } from './fix-targets.js';
15
- import { checkAiSetup, checkCodeowners, checkCodeQL, checkCommunityHealth, checkCoverageUpload, checkDependabot, checkEditorConfig, checkFile, checkGitHooks, checkGitHubActions, checkGitLabCI, checkPrePushHook, checkReadmeBadges, COMMITLINT_FILE_CHECK, } from '../../base/checks.js';
15
+ import { checkAiSetup, checkCodeowners, checkCodeQL, checkCommunityHealth, checkCoverageUpload, checkDependabot, checkEditorConfig, checkFile, checkGitHooks, checkGitHubActions, checkGitLabCI, checkNestedLanguages, checkPrePushHook, checkReadmeBadges, COMMITLINT_FILE_CHECK, } from '../../base/checks.js';
16
16
  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';
17
17
  export { evaluateNodeVersion };
18
18
  const PACKAGE = '@rtorcato/repo-tooling';
@@ -145,6 +145,7 @@ function demoteDeclined(results, lock) {
145
145
  async function runBaseChecks(dir, lock, opts) {
146
146
  const results = [];
147
147
  results.push(checkLockfile(lock));
148
+ results.push(await checkNestedLanguages(dir, opts.language));
148
149
  results.push(await checkGitIdentity(dir));
149
150
  results.push(await checkEditorConfig(dir));
150
151
  results.push(await checkFile(dir, COMMITLINT_FILE_CHECK));
@@ -191,6 +192,7 @@ export async function runDoctor(dir) {
191
192
  hooks: null,
192
193
  badges: { audience: 'public', fixTarget: null },
193
194
  presetWorkflow: null,
195
+ language,
194
196
  })),
195
197
  ];
196
198
  return demoteDeclined(results, lock);
@@ -211,6 +213,7 @@ export async function runDoctor(dir) {
211
213
  // package.json name/repository, which a Swift repo hasn't got.
212
214
  badges: { audience: 'public', fixTarget: null },
213
215
  presetWorkflow: renderSwiftWorkflow(await readSwiftPackage(targetDir)),
216
+ language,
214
217
  })),
215
218
  ...(await runSwiftChecks(targetDir)),
216
219
  ];
@@ -261,6 +264,7 @@ export async function runDoctor(dir) {
261
264
  hooks: jsGitHooksProfile(pkg),
262
265
  badges: { audience: jsBadgeAudience(pkg), fixTarget: 'badges' },
263
266
  presetWorkflow: renderGitHubWorkflow(githubJobs(inferProjectConfig(pkg))),
267
+ language,
264
268
  })));
265
269
  return demoteDeclined(results, lock);
266
270
  }
@@ -2,8 +2,9 @@ import path from 'node:path';
2
2
  import fs from 'fs-extra';
3
3
  /**
4
4
  * Marker files that identify a repo's language, checked in order — first match
5
- * wins. One language per repo root; multi-language monorepos are out of scope
6
- * for v1 (see #139). A dir with no marker → 'unknown' (base checks only).
5
+ * wins. One language per repo root; multi-language monorepos stay out of scope
6
+ * (#139, reaffirmed in #317 — the audit is root-only, but says so via
7
+ * `detectNestedLanguages`). A dir with no marker → 'unknown' (base checks only).
7
8
  */
8
9
  const MARKERS = [
9
10
  ['js', ['package.json']],
@@ -20,3 +21,44 @@ export async function detectLanguage(dir) {
20
21
  }
21
22
  return 'unknown';
22
23
  }
24
+ /** Never worth descending into, and expensive when we do. */
25
+ const SKIP_DIRS = new Set(['node_modules', 'dist', 'build', 'target', 'vendor', '.build']);
26
+ /**
27
+ * Sub-project dirs whose language marker disagrees with the root's, as
28
+ * `{ dir, language }` sorted by dir. Depth-2 scan — enough for the shapes that
29
+ * exist (`apps/docs`, `packages/*`, a top-level `docs/`) and cheap enough to run
30
+ * on every doctor.
31
+ *
32
+ * Deliberately *not* driven by the workspace manifest: the case #317 is about is
33
+ * a Swift or Perl root with a JS docs app, and such a repo has no
34
+ * `pnpm-workspace.yaml` to read. Walking the tree finds it either way, and needs
35
+ * no glob dependency.
36
+ */
37
+ export async function detectNestedLanguages(dir, rootLanguage) {
38
+ const found = [];
39
+ const scan = async (relative, depth) => {
40
+ let entries;
41
+ try {
42
+ entries = await fs.readdir(path.join(dir, relative));
43
+ }
44
+ catch {
45
+ return; // unreadable dir is the caller's problem, not the audit's
46
+ }
47
+ for (const entry of entries) {
48
+ if (entry.startsWith('.') || SKIP_DIRS.has(entry))
49
+ continue;
50
+ const child = relative ? `${relative}/${entry}` : entry;
51
+ if (!(await fs.stat(path.join(dir, child)).catch(() => null))?.isDirectory())
52
+ continue;
53
+ const language = await detectLanguage(path.join(dir, child));
54
+ if (language !== 'unknown' && language !== rootLanguage) {
55
+ found.push({ dir: child, language });
56
+ continue; // a package's own subdirs belong to it, not to the root
57
+ }
58
+ if (depth > 1)
59
+ await scan(child, depth - 1);
60
+ }
61
+ };
62
+ await scan('', 2);
63
+ return found.sort((a, b) => a.dir.localeCompare(b.dir));
64
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rtorcato/repo-tooling",
3
- "version": "3.4.0",
3
+ "version": "3.5.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": [