@rtorcato/js-tooling 2.13.0 → 2.15.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.
@@ -529,11 +529,27 @@ async function checkDependabot(dir) {
529
529
  };
530
530
  }
531
531
  }
532
+ for (const candidate of [
533
+ 'renovate.json',
534
+ 'renovate.json5',
535
+ '.github/renovate.json',
536
+ '.github/renovate.json5',
537
+ '.renovaterc',
538
+ '.renovaterc.json',
539
+ ]) {
540
+ if (await fs.pathExists(path.join(dir, candidate))) {
541
+ return {
542
+ check: 'Dependabot',
543
+ status: 'ok',
544
+ detail: `${candidate} found (Renovate)`,
545
+ };
546
+ }
547
+ }
532
548
  return {
533
549
  check: 'Dependabot',
534
550
  status: 'optional-missing',
535
- detail: 'no .github/dependabot.yml',
536
- hint: 'Run `npx @rtorcato/js-tooling fix dependabot` to scaffold weekly dep updates',
551
+ detail: 'no Dependabot or Renovate config',
552
+ hint: 'Run `npx @rtorcato/js-tooling fix dependabot` (or `fix renovate`) to scaffold weekly dep updates',
537
553
  };
538
554
  }
539
555
  async function checkCodeQL(dir) {
@@ -661,7 +677,7 @@ async function checkGitLabCI(dir) {
661
677
  check: 'GitLab CI',
662
678
  status: 'optional-missing',
663
679
  detail: 'no .gitlab-ci.yml',
664
- hint: 'Add a .gitlab-ci.yml if this repo is hosted on GitLab',
680
+ hint: 'Run `npx @rtorcato/js-tooling fix gitlab-ci` to scaffold a starter GitLab pipeline',
665
681
  };
666
682
  }
667
683
  export async function runDoctor(dir) {
@@ -21,6 +21,7 @@ export const FIX_TARGETS = {
21
21
  Dependabot: 'dependabot',
22
22
  CodeQL: 'codeql',
23
23
  CODEOWNERS: 'codeowners',
24
+ 'GitLab CI': 'gitlab-ci',
24
25
  lockfile: 'lockfile',
25
26
  '.js-tooling.json': 'lockfile',
26
27
  };
@@ -109,6 +110,7 @@ export function lockfilePatchForTarget(target, lock) {
109
110
  case 'semantic-release':
110
111
  return c.semanticRelease ? null : { semanticRelease: true };
111
112
  case 'dependabot':
113
+ case 'renovate':
112
114
  case 'codeql':
113
115
  return c.securityAutomation ? null : { securityAutomation: true };
114
116
  case 'tsconfig':
@@ -5,11 +5,12 @@ import inquirer from 'inquirer';
5
5
  import { generateSemanticReleaseConfig } from '../generators/build.js';
6
6
  import { generateCommitlintConfig, generateHuskyConfig, generatePrePushHook, } from '../generators/git.js';
7
7
  import { generateGitHubActions } from '../generators/github-actions.js';
8
+ import { generateGitLabCI } from '../generators/gitlab-ci.js';
8
9
  import { generateESLintConfig, generatePrettierConfig } from '../generators/linting.js';
9
10
  import { ensureEnginesNode, generateCodeowners, generateEditorConfig, generateKnipConfig, generateNvmrc, generateSizeLimitConfig, } from '../generators/misc.js';
10
11
  import { generateConfigs } from '../generators/index.js';
11
12
  import { composeVerifyScriptFromPkg } from '../generators/package-json.js';
12
- import { generateCodeQLWorkflow, generateDependabotConfig } from '../generators/security.js';
13
+ import { generateCodeQLWorkflow, generateDependabotConfig, generateRenovateConfig, } from '../generators/security.js';
13
14
  import { generateVitestConfig } from '../generators/testing.js';
14
15
  import { generateTreeshakeCheck, inferSubpathsFromExports } from '../generators/treeshake.js';
15
16
  import { copyPreset } from '../utils/copy-preset.js';
@@ -221,6 +222,17 @@ const FIXERS = [
221
222
  return { filesWritten: ['.github/dependabot.yml'] };
222
223
  },
223
224
  },
225
+ {
226
+ target: 'renovate',
227
+ description: 'Scaffold renovate.json (weekly schedule; alternative to Dependabot)',
228
+ appliesTo: ['Dependabot'],
229
+ outputs: ['renovate.json'],
230
+ riskLevel: 'safe-add',
231
+ async run({ targetDir }) {
232
+ await generateRenovateConfig(targetDir);
233
+ return { filesWritten: ['renovate.json'] };
234
+ },
235
+ },
224
236
  {
225
237
  target: 'codeql',
226
238
  description: 'Scaffold .github/workflows/codeql.yml (security scanning)',
@@ -243,6 +255,17 @@ const FIXERS = [
243
255
  return { filesWritten: [written] };
244
256
  },
245
257
  },
258
+ {
259
+ target: 'gitlab-ci',
260
+ description: 'Scaffold .gitlab-ci.yml (lint/typecheck/test/build mirrored from GitHub Actions)',
261
+ appliesTo: ['GitLab CI'],
262
+ outputs: ['.gitlab-ci.yml'],
263
+ canFixDrift: true,
264
+ async run({ targetDir, pkg }) {
265
+ const written = await generateGitLabCI(inferProjectConfig(pkg), targetDir);
266
+ return { filesWritten: [written] };
267
+ },
268
+ },
246
269
  {
247
270
  target: 'editorconfig',
248
271
  description: 'Scaffold .editorconfig (UTF-8, LF, tab indent)',
@@ -381,6 +404,18 @@ const FIXERS = [
381
404
  export function getFixers() {
382
405
  return FIXERS;
383
406
  }
407
+ async function ownOutputsPresent(targetDir, fixer) {
408
+ for (const out of fixer.outputs) {
409
+ // Outputs that reference a package.json field (e.g. "package.json (scripts.verify)")
410
+ // can't be cheaply file-checked here; treat as present so we don't accidentally
411
+ // re-run safe-merge fixers on every targeted invocation.
412
+ if (out.includes('('))
413
+ return true;
414
+ if (await fs.pathExists(path.join(targetDir, out)))
415
+ return true;
416
+ }
417
+ return false;
418
+ }
384
419
  function findFixer(target) {
385
420
  const normalized = target.toLowerCase();
386
421
  return FIXERS.find((f) => f.target.toLowerCase() === normalized);
@@ -575,7 +610,14 @@ export async function fixCommand(target, options = {}) {
575
610
  // fixable when the user explicitly targets it — treat it as optional-missing
576
611
  // so the override + lockfile resync paths run.
577
612
  const lockfileDemoted = lock !== null && declinedInLock(lock, result.check);
578
- const effectiveResult = result.status === 'ok' && lockfileDemoted ? { ...result, status: 'optional-missing' } : result;
613
+ // When multiple fixers share a check (e.g. dependabot + renovate both apply to
614
+ // "Dependabot" deps-update coverage), the check can be `ok` from a sibling tool
615
+ // while this fixer's own outputs are still absent. In that case, treat as missing
616
+ // so the targeted scaffold runs.
617
+ const fixerOutputsPresent = await ownOutputsPresent(targetDir, fixer);
618
+ const effectiveResult = result.status === 'ok' && (lockfileDemoted || !fixerOutputsPresent)
619
+ ? { ...result, status: 'optional-missing' }
620
+ : result;
579
621
  if (effectiveResult.status === 'ok') {
580
622
  actions.push(recordFor(fixer.target, result.check, 'ok', 'already-ok', []));
581
623
  if (json)
@@ -0,0 +1,87 @@
1
+ import path from 'node:path';
2
+ import fs from 'fs-extra';
3
+ export async function generateGitLabCI(config, targetDir) {
4
+ const yamlPath = path.join(targetDir, '.gitlab-ci.yml');
5
+ await fs.writeFile(yamlPath, renderGitLabCI(config));
6
+ return '.gitlab-ci.yml';
7
+ }
8
+ function lintCommand(config) {
9
+ if (config.linting.tool === 'biome' || config.linting.tool === 'both')
10
+ return 'pnpm check';
11
+ return 'pnpm lint';
12
+ }
13
+ function renderGitLabCI(config) {
14
+ const hasTypeScript = config.typescript.enabled;
15
+ const hasTests = config.testing.framework !== 'none';
16
+ const hasLint = config.linting.tool !== 'none';
17
+ const hasBuild = config.bundler !== 'none';
18
+ const stages = [];
19
+ if (hasLint || hasTypeScript || hasTests)
20
+ stages.push('test');
21
+ if (hasBuild)
22
+ stages.push('build');
23
+ if (stages.length === 0)
24
+ stages.push('test');
25
+ const jobs = [];
26
+ if (hasLint) {
27
+ jobs.push(`lint:
28
+ stage: test
29
+ script:
30
+ - ${lintCommand(config)}`);
31
+ }
32
+ if (hasTypeScript) {
33
+ jobs.push(`typecheck:
34
+ stage: test
35
+ script:
36
+ - pnpm typecheck`);
37
+ }
38
+ if (hasTests) {
39
+ const testCmd = config.testing.framework === 'vitest'
40
+ ? 'pnpm exec vitest run'
41
+ : config.testing.framework === 'playwright'
42
+ ? 'pnpm test:e2e'
43
+ : 'pnpm test';
44
+ jobs.push(`test:
45
+ stage: test
46
+ script:
47
+ - ${testCmd}`);
48
+ }
49
+ if (hasBuild) {
50
+ jobs.push(`build:
51
+ stage: build
52
+ script:
53
+ - pnpm build
54
+ artifacts:
55
+ paths:
56
+ - dist/
57
+ expire_in: 1 week`);
58
+ }
59
+ return `# .gitlab-ci.yml — generated by @rtorcato/js-tooling
60
+ # Customize stages and jobs to fit your pipeline.
61
+
62
+ image: node:20
63
+
64
+ stages:
65
+ ${stages.map((s) => ` - ${s}`).join('\n')}
66
+
67
+ variables:
68
+ PNPM_CACHE_FOLDER: .pnpm-store
69
+
70
+ cache:
71
+ key:
72
+ files:
73
+ - pnpm-lock.yaml
74
+ paths:
75
+ - .pnpm-store
76
+ - node_modules
77
+
78
+ default:
79
+ before_script:
80
+ - corepack enable
81
+ - corepack prepare pnpm@latest --activate
82
+ - pnpm config set store-dir "$PNPM_CACHE_FOLDER"
83
+ - pnpm install --frozen-lockfile
84
+
85
+ ${jobs.join('\n\n')}
86
+ `;
87
+ }
@@ -24,6 +24,28 @@ updates:
24
24
  `;
25
25
  await fs.writeFile(filepath, content);
26
26
  }
27
+ export async function generateRenovateConfig(targetDir) {
28
+ const filepath = path.join(targetDir, 'renovate.json');
29
+ const content = `${JSON.stringify({
30
+ $schema: 'https://docs.renovatebot.com/renovate-schema.json',
31
+ extends: ['config:recommended', ':semanticCommits', ':semanticCommitTypeAll(chore)'],
32
+ schedule: ['before 4am on Monday'],
33
+ prConcurrentLimit: 10,
34
+ prHourlyLimit: 0,
35
+ rangeStrategy: 'bump',
36
+ packageRules: [
37
+ {
38
+ matchManagers: ['github-actions'],
39
+ commitMessagePrefix: 'chore(ci):',
40
+ },
41
+ {
42
+ matchManagers: ['npm'],
43
+ commitMessagePrefix: 'chore(deps):',
44
+ },
45
+ ],
46
+ }, null, 2)}\n`;
47
+ await fs.writeFile(filepath, content);
48
+ }
27
49
  export async function generateCodeQLWorkflow(targetDir) {
28
50
  await fs.ensureDir(path.join(targetDir, '.github', 'workflows'));
29
51
  const filepath = path.join(targetDir, '.github', 'workflows', 'codeql.yml');
package/dist/cli/index.js CHANGED
@@ -184,6 +184,12 @@ const TOOL_CATALOG = [
184
184
  exports: [],
185
185
  fixTarget: 'dependabot',
186
186
  },
187
+ {
188
+ name: 'Renovate',
189
+ description: 'Weekly automated dependency updates (alternative to Dependabot)',
190
+ exports: [],
191
+ fixTarget: 'renovate',
192
+ },
187
193
  {
188
194
  name: 'CodeQL',
189
195
  description: 'GitHub security scanning workflow',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rtorcato/js-tooling",
3
- "version": "2.13.0",
3
+ "version": "2.15.0",
4
4
  "description": "JavaScript and TypeScript tooling for Node.js, React, Next.js, and Vitest.",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -240,7 +240,7 @@
240
240
  "ts-jest": "^29.0.0",
241
241
  "tsup": "^8.0.0",
242
242
  "typescript": ">=5.0.0",
243
- "typescript-eslint": "*",
243
+ "typescript-eslint": "^8.0.0",
244
244
  "vitest": ">=3.0.0"
245
245
  },
246
246
  "peerDependenciesMeta": {