@rtorcato/repo-tooling 3.8.1 → 3.8.3

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.
@@ -24,13 +24,17 @@ export async function checkNestedLanguages(dir, rootLanguage) {
24
24
  hint: 'Run doctor with `--directory <path>` to audit a nested project on its own',
25
25
  };
26
26
  }
27
+ /** Applies a `FileCheck` matcher of either form to a file's contents. */
28
+ export function matches(matcher, contents) {
29
+ return typeof matcher === 'function' ? matcher(contents) : matcher.test(contents);
30
+ }
27
31
  export async function checkFile(dir, spec) {
28
32
  for (const candidate of spec.candidates) {
29
33
  const filepath = path.join(dir, candidate);
30
34
  if (!(await fs.pathExists(filepath)))
31
35
  continue;
32
36
  const contents = await fs.readFile(filepath, 'utf-8');
33
- if (spec.matcher.test(contents)) {
37
+ if (matches(spec.matcher, contents)) {
34
38
  return {
35
39
  check: spec.check,
36
40
  status: 'ok',
@@ -42,6 +42,53 @@ export function evaluateNodeVersion(version) {
42
42
  detail: display,
43
43
  };
44
44
  }
45
+ /**
46
+ * JSONC → object, or null when it genuinely won't parse. `biome.jsonc` is a
47
+ * declared candidate and the format allows comments and trailing commas, so
48
+ * bare `JSON.parse` would reject configs Biome itself accepts.
49
+ *
50
+ * The first alternative consumes whole string literals, so a `//` or `/*`
51
+ * inside one (a `$schema` URL, most obviously) is never mistaken for a comment.
52
+ */
53
+ function parseJsonc(text) {
54
+ const withoutComments = text.replace(/("(?:\\.|[^"\\])*")|\/\/[^\n]*|\/\*[\s\S]*?\*\//g, (_, str) => str ?? '');
55
+ try {
56
+ return JSON.parse(withoutComments.replace(/,(\s*[}\]])/g, '$1'));
57
+ }
58
+ catch {
59
+ return null;
60
+ }
61
+ }
62
+ const BIOME_PRESET_REF = /@rtorcato\/(?:js|repo)-tooling\/biome/;
63
+ /**
64
+ * Both shapes of a `biome.json` this package produces (#378): the thin
65
+ * `extends` pointer `fix biome` writes, and the whole preset inlined, which is
66
+ * what `copy biome` drops — it carries no `extends` at all, so matching only
67
+ * the first left every `copy biome` repo permanently drifted with no way out.
68
+ *
69
+ * Read structurally, not as text. Accepting the inline form on the presence of
70
+ * a `$schema` URL and a `preset` key alone would pass a config that keeps both
71
+ * markers and turns the linter off — the very drift this check exists to catch
72
+ * — and the markers could even sit in a `biome.jsonc` comment. So the config is
73
+ * parsed, `linter.enabled: false` disqualifies either shape, and anything
74
+ * unparseable counts as drift rather than being waved through.
75
+ */
76
+ function matchesBiomeConfig(contents) {
77
+ const config = parseJsonc(contents);
78
+ if (!config)
79
+ return false;
80
+ const linter = config.linter;
81
+ if (linter?.enabled === false)
82
+ return false;
83
+ if (BIOME_PRESET_REF.test(JSON.stringify(config.extends ?? '')))
84
+ return true;
85
+ // The inlined preset: biome's own `$schema` plus the `linter.rules.preset`
86
+ // key the shipped `tooling/biome/biome.json` sets.
87
+ const schema = config.$schema;
88
+ return (typeof schema === 'string' &&
89
+ schema.includes('biomejs.dev/schemas/') &&
90
+ linter?.rules?.preset !== undefined);
91
+ }
45
92
  export const FILE_CHECKS = [
46
93
  {
47
94
  check: 'TypeScript',
@@ -53,11 +100,9 @@ export const FILE_CHECKS = [
53
100
  {
54
101
  check: 'Biome',
55
102
  candidates: ['biome.json', 'biome.jsonc'],
56
- expected: `extends "${PACKAGE}/biome"`,
57
- matcher: /@rtorcato\/(?:js|repo)-tooling\/biome/,
103
+ expected: `extends "${PACKAGE}/biome" or inlines the preset, with the linter on`,
104
+ matcher: matchesBiomeConfig,
58
105
  optional: true,
59
- // `fix biome`, not `copy biome`: copy drops the whole preset inline, which
60
- // carries no `extends` for the matcher above to find (#365).
61
106
  hint: 'Run `npx @rtorcato/repo-tooling fix biome` to scaffold',
62
107
  },
63
108
  {
@@ -28,6 +28,22 @@ const ESLINT_SCRIPTS = {
28
28
  const PRETTIER_SCRIPTS = {
29
29
  format: 'prettier --write .',
30
30
  };
31
+ /** Kept in step with the knip line of getScripts() in package-json.ts. */
32
+ const KNIP_SCRIPTS = {
33
+ knip: 'knip',
34
+ };
35
+ /** Kept in step with the typescript branch of getScripts() in package-json.ts. */
36
+ const TSCONFIG_SCRIPTS = {
37
+ typecheck: 'tsc --noEmit',
38
+ };
39
+ // getScripts() also emits `test:ui`, but only the browser/both environments
40
+ // install @vitest/ui, so a fixed-up repo would get a script it can't run.
41
+ /** Kept in step with the vitest branch of getScripts() in package-json.ts. */
42
+ const VITEST_SCRIPTS = {
43
+ test: 'vitest',
44
+ 'test:watch': 'vitest --watch',
45
+ coverage: 'vitest run --coverage',
46
+ };
31
47
  import { WORKSPACE_FILE, dependsOnEsbuild, ensurePnpmSettings, familyGlob, } from '../../cli/generators/pnpm-workspace.js';
32
48
  import { generatePostcss } from '../../cli/generators/postcss.js';
33
49
  import { generateCypressConfig, generateVitestConfig } from '../../cli/generators/testing.js';
@@ -150,13 +166,19 @@ export const FIXERS = [
150
166
  },
151
167
  {
152
168
  target: 'tsconfig',
153
- description: 'Scaffold tsconfig.json extending the @rtorcato/repo-tooling preset',
169
+ description: 'Scaffold tsconfig.json extending the @rtorcato/repo-tooling preset, plus the `typecheck` script',
154
170
  appliesTo: ['TypeScript'],
155
- outputs: ['tsconfig.json'],
171
+ outputs: ['tsconfig.json', 'package.json (scripts)'],
156
172
  canFixDrift: true,
157
173
  async run({ targetDir }) {
158
174
  const result = await copyPreset('tsconfig', targetDir);
159
- return { filesWritten: [result.target] };
175
+ const filesWritten = [result.target];
176
+ // Without `typecheck` the generated CI drops its typecheck job entirely
177
+ // (#377). Shared with non-JS paths, so this is a no-op when there's no
178
+ // package.json to add it to.
179
+ if (await ensureScripts(targetDir, TSCONFIG_SCRIPTS))
180
+ filesWritten.push('package.json');
181
+ return { filesWritten };
160
182
  },
161
183
  },
162
184
  {
@@ -191,9 +213,9 @@ export const FIXERS = [
191
213
  },
192
214
  {
193
215
  target: 'vitest',
194
- description: 'Scaffold vitest.config.ts (preserves vitest.setup.ts if present)',
216
+ description: 'Scaffold vitest.config.ts (preserves vitest.setup.ts if present), plus the scripts that run it',
195
217
  appliesTo: ['Vitest'],
196
- outputs: ['vitest.config.ts'],
218
+ outputs: ['vitest.config.ts', 'package.json (scripts)'],
197
219
  canFixDrift: true,
198
220
  async run({ targetDir, pkg }) {
199
221
  const setupPath = path.join(targetDir, 'vitest.setup.ts');
@@ -203,7 +225,12 @@ export const FIXERS = [
203
225
  if (hadSetup && savedSetup !== null) {
204
226
  await fs.writeFile(setupPath, savedSetup);
205
227
  }
206
- return { filesWritten: ['vitest.config.ts'] };
228
+ const filesWritten = ['vitest.config.ts'];
229
+ // Without `coverage`/`test` the generated CI drops the whole test job,
230
+ // Codecov upload included (#377).
231
+ if (await ensureScripts(targetDir, VITEST_SCRIPTS))
232
+ filesWritten.push('package.json');
233
+ return { filesWritten };
207
234
  },
208
235
  },
209
236
  {
@@ -497,13 +524,18 @@ export const FIXERS = [
497
524
  },
498
525
  {
499
526
  target: 'knip',
500
- description: 'Scaffold knip.json (entry globs matched to the package.json build model)',
527
+ description: 'Scaffold knip.json (entry globs matched to the package.json build model), plus the `knip` script',
501
528
  appliesTo: ['knip'],
502
- outputs: ['knip.json'],
529
+ outputs: ['knip.json', 'package.json (scripts)'],
503
530
  canFixDrift: true,
504
531
  async run({ targetDir }) {
505
532
  await generateKnipConfig(targetDir);
506
- return { filesWritten: ['knip.json'] };
533
+ const filesWritten = ['knip.json'];
534
+ // Without `knip` the generated CI drops its unused-code step, and with no
535
+ // check/lint script either the whole lint job disappears (#377).
536
+ if (await ensureScripts(targetDir, KNIP_SCRIPTS))
537
+ filesWritten.push('package.json');
538
+ return { filesWritten };
507
539
  },
508
540
  },
509
541
  {
@@ -18,7 +18,7 @@
18
18
  */
19
19
  import path from 'node:path';
20
20
  import fs from 'fs-extra';
21
- import { checkFile } from '../../base/checks.js';
21
+ import { checkFile, matches } from '../../base/checks.js';
22
22
  import { PYTHON_GITIGNORE_SENTINELS } from './gitignore.js';
23
23
  import { PYTHON_HOOKS_DIR } from './git-hooks.js';
24
24
  const PYTHON_TOOL_CHECKS = [
@@ -68,7 +68,7 @@ async function checkPythonTool(dir, spec) {
68
68
  const filepath = path.join(dir, candidate);
69
69
  if (!(await fs.pathExists(filepath)))
70
70
  continue;
71
- if (spec.dedicated.matcher.test(await fs.readFile(filepath, 'utf-8'))) {
71
+ if (matches(spec.dedicated.matcher, await fs.readFile(filepath, 'utf-8'))) {
72
72
  return {
73
73
  check: spec.dedicated.check,
74
74
  status: 'ok',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rtorcato/repo-tooling",
3
- "version": "3.8.1",
3
+ "version": "3.8.3",
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": [