@rtorcato/js-tooling 2.12.0 → 2.14.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.
@@ -661,7 +661,7 @@ async function checkGitLabCI(dir) {
661
661
  check: 'GitLab CI',
662
662
  status: 'optional-missing',
663
663
  detail: 'no .gitlab-ci.yml',
664
- hint: 'Add a .gitlab-ci.yml if this repo is hosted on GitLab',
664
+ hint: 'Run `npx @rtorcato/js-tooling fix gitlab-ci` to scaffold a starter GitLab pipeline',
665
665
  };
666
666
  }
667
667
  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
  };
@@ -5,8 +5,10 @@ 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';
11
+ import { generateConfigs } from '../generators/index.js';
10
12
  import { composeVerifyScriptFromPkg } from '../generators/package-json.js';
11
13
  import { generateCodeQLWorkflow, generateDependabotConfig } from '../generators/security.js';
12
14
  import { generateVitestConfig } from '../generators/testing.js';
@@ -15,6 +17,7 @@ import { copyPreset } from '../utils/copy-preset.js';
15
17
  import { LOCKFILE_NAME, readLockfile, updateLockfileConfig, writeLockfile, } from '../utils/lockfile.js';
16
18
  import { runDoctor } from './doctor.js';
17
19
  import { declinedInLock, lockfilePatchForTarget } from './fix-targets.js';
20
+ import { computeFileList } from './setup-presets.js';
18
21
  function inferProjectConfig(pkg) {
19
22
  const deps = {
20
23
  ...(pkg?.dependencies ?? {}),
@@ -241,6 +244,17 @@ const FIXERS = [
241
244
  return { filesWritten: [written] };
242
245
  },
243
246
  },
247
+ {
248
+ target: 'gitlab-ci',
249
+ description: 'Scaffold .gitlab-ci.yml (lint/typecheck/test/build mirrored from GitHub Actions)',
250
+ appliesTo: ['GitLab CI'],
251
+ outputs: ['.gitlab-ci.yml'],
252
+ canFixDrift: true,
253
+ async run({ targetDir, pkg }) {
254
+ const written = await generateGitLabCI(inferProjectConfig(pkg), targetDir);
255
+ return { filesWritten: [written] };
256
+ },
257
+ },
244
258
  {
245
259
  target: 'editorconfig',
246
260
  description: 'Scaffold .editorconfig (UTF-8, LF, tab indent)',
@@ -479,6 +493,60 @@ export async function fixCommand(target, options = {}) {
479
493
  console.log();
480
494
  return;
481
495
  }
496
+ if (options.resync) {
497
+ if (target) {
498
+ console.error(chalk.red('\n❌ --resync cannot be combined with a [target] argument\n'));
499
+ process.exit(1);
500
+ }
501
+ const resyncLock = await readLockfile(targetDir);
502
+ if (!resyncLock) {
503
+ if (json) {
504
+ console.log(JSON.stringify({ directory: targetDir, error: 'no-lockfile', hint: 'run `fix lockfile` first' }, null, 2));
505
+ }
506
+ else {
507
+ console.error(chalk.red(`\n❌ No ${LOCKFILE_NAME} found — run \`fix lockfile\` first to record choices\n`));
508
+ }
509
+ process.exit(1);
510
+ }
511
+ const files = computeFileList(resyncLock.config);
512
+ if (!silent) {
513
+ console.log(chalk.cyan(`\n🔄 Resync from ${LOCKFILE_NAME} (${files.length} files in scope)\n`));
514
+ }
515
+ if (dryRun) {
516
+ if (json) {
517
+ console.log(JSON.stringify({ directory: targetDir, mode: 'resync', dryRun: true, files }, null, 2));
518
+ }
519
+ else {
520
+ for (const f of files)
521
+ console.log(chalk.cyan(` [dry-run] would write: ${f}`));
522
+ console.log();
523
+ }
524
+ return;
525
+ }
526
+ if (!assumeYes) {
527
+ const { confirm } = await inquirer.prompt([
528
+ {
529
+ type: 'confirm',
530
+ name: 'confirm',
531
+ message: `Re-scaffold ${files.length} file(s) from ${LOCKFILE_NAME}? Generators preserve existing customizations where possible, but README.md will be rewritten.`,
532
+ default: false,
533
+ },
534
+ ]);
535
+ if (!confirm) {
536
+ console.log(chalk.gray(' skipped\n'));
537
+ return;
538
+ }
539
+ }
540
+ await generateConfigs(resyncLock.config, targetDir);
541
+ await writeLockfile(targetDir, resyncLock.config);
542
+ if (json) {
543
+ console.log(JSON.stringify({ directory: targetDir, mode: 'resync', dryRun: false, files }, null, 2));
544
+ }
545
+ else {
546
+ console.log(chalk.green(` ✅ resynced ${files.length} file(s)\n`));
547
+ }
548
+ return;
549
+ }
482
550
  const pkg = await readPackageJson(targetDir);
483
551
  const lock = await readLockfile(targetDir);
484
552
  const results = await runDoctor(targetDir);
@@ -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
+ }
package/dist/cli/index.js CHANGED
@@ -222,12 +222,14 @@ program
222
222
  .option('--dry-run', 'Print what would change without writing files')
223
223
  .option('--json', 'Emit machine-readable JSON output (implies --yes)')
224
224
  .option('--list', 'List all registered fix targets and exit')
225
+ .option('--resync', 'Re-scaffold every file recorded in .js-tooling.json')
225
226
  .action((target, options) => fixCommand(target, {
226
227
  directory: options.directory,
227
228
  yes: options.yes,
228
229
  dryRun: options.dryRun,
229
230
  json: options.json,
230
231
  list: options.list,
232
+ resync: options.resync,
231
233
  }));
232
234
  program.hook('preAction', async (_, actionCommand) => {
233
235
  const name = actionCommand.name();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rtorcato/js-tooling",
3
- "version": "2.12.0",
3
+ "version": "2.14.0",
4
4
  "description": "JavaScript and TypeScript tooling for Node.js, React, Next.js, and Vitest.",
5
5
  "type": "module",
6
6
  "keywords": [