@rtorcato/js-tooling 2.17.1 → 2.18.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.
@@ -89,6 +89,24 @@ const FILE_CHECKS = [
89
89
  matcher: /@rtorcato\/js-tooling\/commitlint\/config/,
90
90
  optional: true,
91
91
  },
92
+ {
93
+ check: 'Oxlint',
94
+ candidates: ['.oxlintrc.json', 'oxlintrc.json'],
95
+ // Oxlint configs are project-owned (extends from npm packages isn't
96
+ // reliably supported), so any well-formed file counts as ok.
97
+ expected: 'is a valid Oxlint configuration',
98
+ matcher: /"(rules|plugins|categories|extends)"/,
99
+ optional: true,
100
+ hint: 'Run `npx @rtorcato/js-tooling copy oxlint` to scaffold',
101
+ },
102
+ {
103
+ check: 'Changesets',
104
+ candidates: ['.changeset/config.json'],
105
+ expected: 'is a valid Changesets configuration',
106
+ matcher: /"(changelog|access|baseBranch)"/,
107
+ optional: true,
108
+ hint: 'Run `npx @rtorcato/js-tooling copy changesets` to scaffold',
109
+ },
92
110
  ];
93
111
  async function checkFile(dir, spec) {
94
112
  for (const candidate of spec.candidates) {
@@ -451,7 +469,25 @@ async function checkSemanticRelease(dir, pkg) {
451
469
  break;
452
470
  }
453
471
  }
472
+ const hasChangesets = await fs.pathExists(path.join(dir, '.changeset', 'config.json'));
473
+ // Conflict: both semantic-release and Changesets configured.
474
+ if ((inPkg || configFile) && hasChangesets) {
475
+ return {
476
+ check: 'semantic-release',
477
+ status: 'drift',
478
+ detail: 'both semantic-release and Changesets are configured',
479
+ hint: 'Pick one release tool — remove either the semantic-release config or the .changeset/ directory',
480
+ };
481
+ }
454
482
  if (!inPkg && !configFile) {
483
+ // Changesets present — treat semantic-release as intentionally not used.
484
+ if (hasChangesets) {
485
+ return {
486
+ check: 'semantic-release',
487
+ status: 'ok',
488
+ detail: 'using Changesets (.changeset/config.json) instead',
489
+ };
490
+ }
455
491
  return {
456
492
  check: 'semantic-release',
457
493
  status: isPrivate ? 'optional-missing' : 'drift',
@@ -1,5 +1,7 @@
1
1
  import path from 'node:path';
2
+ import os from 'node:os';
2
3
  import chalk from 'chalk';
4
+ import { createPatch } from 'diff';
3
5
  import fs from 'fs-extra';
4
6
  import inquirer from 'inquirer';
5
7
  import { generateSemanticReleaseConfig } from '../generators/build.js';
@@ -202,6 +204,28 @@ const FIXERS = [
202
204
  return { filesWritten: ['release.config.mjs'] };
203
205
  },
204
206
  },
207
+ {
208
+ target: 'changesets',
209
+ description: 'Scaffold .changeset/config.json (alternative to semantic-release)',
210
+ appliesTo: ['Changesets'],
211
+ outputs: ['.changeset/config.json'],
212
+ canFixDrift: true,
213
+ async run({ targetDir }) {
214
+ const result = await copyPreset('changesets', targetDir);
215
+ return { filesWritten: [result.target] };
216
+ },
217
+ },
218
+ {
219
+ target: 'oxlint',
220
+ description: 'Scaffold .oxlintrc.json (additive to Biome/ESLint)',
221
+ appliesTo: ['Oxlint'],
222
+ outputs: ['.oxlintrc.json'],
223
+ canFixDrift: true,
224
+ async run({ targetDir }) {
225
+ const result = await copyPreset('oxlint', targetDir);
226
+ return { filesWritten: [result.target] };
227
+ },
228
+ },
205
229
  {
206
230
  target: 'github-actions',
207
231
  description: 'Scaffold .github/workflows/ci.yml',
@@ -464,6 +488,117 @@ export function listFixers() {
464
488
  canFixDrift: f.canFixDrift ?? false,
465
489
  }));
466
490
  }
491
+ // Fixer outputs sometimes carry annotations like
492
+ // "package.json (lint-staged field)" — strip them to get a usable filesystem path.
493
+ function outputToRelativePath(output) {
494
+ return output.split(' ')[0] ?? output;
495
+ }
496
+ function shouldColorise() {
497
+ // Respect NO_COLOR (https://no-color.org) and chalk's own detection.
498
+ if (process.env.NO_COLOR && process.env.NO_COLOR !== '')
499
+ return false;
500
+ return chalk.level > 0;
501
+ }
502
+ function colorisePatch(patch) {
503
+ if (!shouldColorise())
504
+ return patch;
505
+ return patch
506
+ .split('\n')
507
+ .map((line) => {
508
+ if (line.startsWith('+++') || line.startsWith('---'))
509
+ return chalk.bold(line);
510
+ if (line.startsWith('@@'))
511
+ return chalk.cyan(line);
512
+ if (line.startsWith('+'))
513
+ return chalk.green(line);
514
+ if (line.startsWith('-'))
515
+ return chalk.red(line);
516
+ return line;
517
+ })
518
+ .join('\n');
519
+ }
520
+ /**
521
+ * Shadow-run a fixer in a temp copy of the target directory and return per-output
522
+ * diffs. We copy the real target into tmp so fixers that read existing state
523
+ * (e.g. husky reading package.json) still produce realistic output.
524
+ */
525
+ async function previewFixer(fixer, result, targetDir, pkg, lock) {
526
+ // Pick a tmp root that is NOT inside targetDir. macOS sometimes hands us a
527
+ // $TMPDIR that lives under the working dir (e.g. when the caller is itself
528
+ // running inside a tempdir tree), which would make fs.copy fail with
529
+ // "subdirectory of itself". Fall back to the parent of targetDir if so.
530
+ const resolvedTarget = path.resolve(targetDir);
531
+ let tmpRoot = path.resolve(os.tmpdir());
532
+ if (tmpRoot === resolvedTarget || tmpRoot.startsWith(resolvedTarget + path.sep)) {
533
+ tmpRoot = path.dirname(resolvedTarget);
534
+ }
535
+ const tmpDir = await fs.mkdtemp(path.join(tmpRoot, 'js-tooling-fix-preview-'));
536
+ try {
537
+ await fs.copy(targetDir, tmpDir, {
538
+ filter: (src) => {
539
+ const rel = path.relative(targetDir, src);
540
+ if (!rel)
541
+ return true;
542
+ const first = rel.split(path.sep)[0];
543
+ // Skip large/derived dirs that fixers never touch — keeps preview fast on
544
+ // big repos.
545
+ return first !== 'node_modules' && first !== 'dist' && first !== 'build' && first !== '.git';
546
+ },
547
+ });
548
+ await fixer.run({ targetDir: tmpDir, pkg, result, lock });
549
+ const previews = [];
550
+ const seen = new Set();
551
+ for (const output of fixer.outputs) {
552
+ const rel = outputToRelativePath(output);
553
+ if (seen.has(rel))
554
+ continue;
555
+ seen.add(rel);
556
+ const tmpPath = path.join(tmpDir, rel);
557
+ const realPath = path.join(targetDir, rel);
558
+ if (!(await fs.pathExists(tmpPath)))
559
+ continue;
560
+ const newContent = await fs.readFile(tmpPath, 'utf-8');
561
+ const existed = await fs.pathExists(realPath);
562
+ const oldContent = existed ? await fs.readFile(realPath, 'utf-8') : '';
563
+ if (newContent === oldContent) {
564
+ previews.push({ path: rel, kind: 'unchanged', patch: null });
565
+ continue;
566
+ }
567
+ const patch = createPatch(rel, oldContent, newContent, undefined, undefined, { context: 3 });
568
+ previews.push({
569
+ path: rel,
570
+ kind: existed ? 'modify' : 'create',
571
+ patch: colorisePatch(patch),
572
+ });
573
+ }
574
+ return previews;
575
+ }
576
+ finally {
577
+ await fs.remove(tmpDir).catch(() => {
578
+ // Best-effort cleanup; tmp dirs get GC'd by the OS eventually.
579
+ });
580
+ }
581
+ }
582
+ function printPreviews(previews) {
583
+ if (previews.length === 0) {
584
+ console.log(chalk.gray(' (no preview available — fixer produced no recognisable outputs)'));
585
+ return;
586
+ }
587
+ for (const p of previews) {
588
+ if (p.kind === 'unchanged') {
589
+ console.log(chalk.gray(` ${p.path} — unchanged`));
590
+ continue;
591
+ }
592
+ const label = p.kind === 'create' ? chalk.green('create') : chalk.yellow('modify');
593
+ console.log(` ${label} ${chalk.bold(p.path)}`);
594
+ if (p.patch) {
595
+ console.log(p.patch
596
+ .split('\n')
597
+ .map((l) => ` ${l}`)
598
+ .join('\n'));
599
+ }
600
+ }
601
+ }
467
602
  async function applyFixer(fixer, result, targetDir, pkg, lock, dryRun, silent) {
468
603
  if (dryRun) {
469
604
  if (!silent) {
@@ -526,6 +661,8 @@ export async function fixCommand(target, options = {}) {
526
661
  // JSON mode implies --yes so prompts don't corrupt the output stream.
527
662
  const assumeYes = options.yes === true || json;
528
663
  const silent = json;
664
+ // Diff preview is interactive-only — suppress in JSON mode.
665
+ const showDiff = options.diff === true && !json;
529
666
  if (options.list) {
530
667
  const summary = listFixers();
531
668
  if (json) {
@@ -654,6 +791,10 @@ export async function fixCommand(target, options = {}) {
654
791
  console.log(chalk.cyan(`\n🔧 ${fixer.target} — ${chalk.bold(result.check)} is ${effectiveResult.status}\n`));
655
792
  }
656
793
  const conflict = noteLockConflict(result.check);
794
+ if (showDiff && (fixer.riskLevel ?? 'destructive') !== 'safe-add') {
795
+ const previews = await previewFixer(fixer, effectiveResult, targetDir, pkg, lock);
796
+ printPreviews(previews);
797
+ }
657
798
  const ok = await confirmApply(fixer, effectiveResult, assumeYes);
658
799
  if (!ok) {
659
800
  actions.push(recordFor(fixer.target, result.check, effectiveResult.status, 'skipped', [], conflict));
@@ -695,6 +836,10 @@ export async function fixCommand(target, options = {}) {
695
836
  console.log(` ${chalk.bold(result.check)} (${result.status}) → ${fixer.target}`);
696
837
  }
697
838
  const conflict = noteLockConflict(result.check);
839
+ if (showDiff && (fixer.riskLevel ?? 'destructive') !== 'safe-add') {
840
+ const previews = await previewFixer(fixer, result, targetDir, pkg, lock);
841
+ printPreviews(previews);
842
+ }
698
843
  const ok = await confirmApply(fixer, result, assumeYes);
699
844
  if (!ok) {
700
845
  actions.push(recordFor(fixer.target, result.check, result.status, 'skipped', [], conflict));
@@ -125,6 +125,8 @@ export const CONFIG_SCHEMA = {
125
125
  gitHooks: { type: 'boolean' },
126
126
  commitLint: { type: 'boolean' },
127
127
  semanticRelease: { type: 'boolean' },
128
+ changesets: { type: 'boolean' },
129
+ oxlint: { type: 'boolean' },
128
130
  securityAutomation: { type: 'boolean' },
129
131
  bundler: { type: 'string', enum: ['tsup', 'esbuild', 'vite', 'none'] },
130
132
  treeshakeCheck: { type: 'boolean' },
@@ -192,6 +194,10 @@ export function computeFileList(config) {
192
194
  files.push('vite.config.ts');
193
195
  if (config.semanticRelease)
194
196
  files.push('release.config.mjs');
197
+ if (config.changesets)
198
+ files.push('.changeset/config.json');
199
+ if (config.oxlint)
200
+ files.push('.oxlintrc.json');
195
201
  if (config.treeshakeCheck && config.projectType === 'library') {
196
202
  files.push('apps/treeshake-check/package.json', 'apps/treeshake-check/check.mjs', 'apps/treeshake-check/src/entry.ts');
197
203
  }
@@ -146,6 +146,13 @@ async function promptForConfig() {
146
146
  },
147
147
  when: (answers) => answers.lintingTool === 'eslint' || answers.lintingTool === 'both',
148
148
  },
149
+ {
150
+ type: 'confirm',
151
+ name: 'oxlint',
152
+ message: '🦀 Also run Oxlint alongside (50–100× faster than ESLint)?',
153
+ default: false,
154
+ when: (answers) => answers.lintingTool !== 'none',
155
+ },
149
156
  {
150
157
  type: 'list',
151
158
  name: 'testingFramework',
@@ -184,10 +191,15 @@ async function promptForConfig() {
184
191
  when: (answers) => answers.gitHooks,
185
192
  },
186
193
  {
187
- type: 'confirm',
188
- name: 'semanticRelease',
189
- message: '🚀 Set up semantic release for automated versioning?',
190
- default: (answers) => answers.projectType === 'library',
194
+ type: 'list',
195
+ name: 'releaseTool',
196
+ message: '🚀 Automated release tool?',
197
+ choices: [
198
+ { name: '📦 semantic-release (commit-message-driven)', value: 'semantic-release' },
199
+ { name: '📝 Changesets (changeset-file-driven, monorepo-friendly)', value: 'changesets' },
200
+ { name: '❌ None', value: 'none' },
201
+ ],
202
+ default: 'semantic-release',
191
203
  when: (answers) => answers.projectType === 'library',
192
204
  },
193
205
  {
@@ -245,7 +257,9 @@ async function promptForConfig() {
245
257
  },
246
258
  gitHooks: answers.gitHooks || false,
247
259
  commitLint: answers.commitLint || false,
248
- semanticRelease: answers.semanticRelease || false,
260
+ semanticRelease: answers.releaseTool === 'semantic-release',
261
+ changesets: answers.releaseTool === 'changesets',
262
+ oxlint: answers.oxlint || false,
249
263
  securityAutomation: answers.securityAutomation ?? false,
250
264
  bundler: answers.bundler || 'none',
251
265
  treeshakeCheck: answers.treeshakeCheck || false,
@@ -14,6 +14,10 @@ export async function generateBuildConfigs(config, targetDir) {
14
14
  if (config.semanticRelease) {
15
15
  await generateSemanticReleaseConfig(targetDir);
16
16
  }
17
+ // Generate Changesets config (alternative to semantic-release)
18
+ if (config.changesets) {
19
+ await generateChangesetsConfig(targetDir);
20
+ }
17
21
  }
18
22
  async function generateTsupConfig(targetDir) {
19
23
  const tsupConfigPath = path.join(targetDir, 'tsup.config.ts');
@@ -73,3 +77,10 @@ export async function generateSemanticReleaseConfig(targetDir) {
73
77
  `;
74
78
  await fs.writeFile(releaseConfigPath, releaseConfig);
75
79
  }
80
+ export async function generateChangesetsConfig(targetDir) {
81
+ // Drop the canonical Changesets config into .changeset/config.json. The user
82
+ // owns this file once it's in their repo; subsequent `pnpm changeset` runs
83
+ // create per-change markdown files alongside it.
84
+ const { copyPreset } = await import('../utils/copy-preset.js');
85
+ await copyPreset('changesets', targetDir);
86
+ }
@@ -13,6 +13,17 @@ export async function generateLintingConfigs(config, targetDir) {
13
13
  if (config.linting.tool === 'eslint') {
14
14
  await generatePrettierConfig(targetDir);
15
15
  }
16
+ // Generate Oxlint config (additive — runs alongside Biome/ESLint)
17
+ if (config.oxlint) {
18
+ await generateOxlintConfig(targetDir);
19
+ }
20
+ }
21
+ export async function generateOxlintConfig(targetDir) {
22
+ // Oxlint's `extends` resolution from npm packages isn't reliably supported,
23
+ // so we copy the full preset rather than write a thin pointer file (same
24
+ // pattern as biome.jsonc — the user owns the file once it's in their repo).
25
+ const { copyPreset } = await import('../utils/copy-preset.js');
26
+ await copyPreset('oxlint', targetDir);
16
27
  }
17
28
  export async function generateBiomeConfig(targetDir) {
18
29
  const biomeConfigPath = path.join(targetDir, 'biome.jsonc');
package/dist/cli/index.js CHANGED
@@ -142,6 +142,18 @@ const TOOL_CATALOG = [
142
142
  ],
143
143
  fixTarget: 'semantic-release',
144
144
  },
145
+ {
146
+ name: 'Changesets',
147
+ description: 'Monorepo-friendly release tool (alternative to semantic-release)',
148
+ exports: ['@rtorcato/js-tooling/changesets'],
149
+ fixTarget: 'changesets',
150
+ },
151
+ {
152
+ name: 'Oxlint',
153
+ description: 'Rust-based linter (additive to Biome/ESLint)',
154
+ exports: ['@rtorcato/js-tooling/oxlint'],
155
+ fixTarget: 'oxlint',
156
+ },
145
157
  {
146
158
  name: 'tsup',
147
159
  description: 'TypeScript bundler configuration',
@@ -235,6 +247,7 @@ program
235
247
  .option('--json', 'Emit machine-readable JSON output (implies --yes)')
236
248
  .option('--list', 'List all registered fix targets and exit')
237
249
  .option('--resync', 'Re-scaffold every file recorded in .js-tooling.json')
250
+ .option('--diff', 'Show a unified diff of each change before confirming')
238
251
  .action((target, options) => fixCommand(target, {
239
252
  directory: options.directory,
240
253
  yes: options.yes,
@@ -242,6 +255,7 @@ program
242
255
  json: options.json,
243
256
  list: options.list,
244
257
  resync: options.resync,
258
+ diff: options.diff,
245
259
  }));
246
260
  program.hook('preAction', async (_, actionCommand) => {
247
261
  const name = actionCommand.name();
@@ -6,6 +6,16 @@ export const PRESETS = {
6
6
  target: 'biome.json',
7
7
  desc: 'Biome formatter and linter configuration',
8
8
  },
9
+ changesets: {
10
+ source: 'tooling/changesets/config.json',
11
+ target: '.changeset/config.json',
12
+ desc: 'Changesets release-tool configuration',
13
+ },
14
+ oxlint: {
15
+ source: 'tooling/oxlint/oxlintrc.json',
16
+ target: '.oxlintrc.json',
17
+ desc: 'Oxlint linter configuration (additive to Biome)',
18
+ },
9
19
  tsconfig: {
10
20
  source: 'tooling/typescript/tsconfig.base.json',
11
21
  target: 'tsconfig.json',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rtorcato/js-tooling",
3
- "version": "2.17.1",
3
+ "version": "2.18.0",
4
4
  "description": "JavaScript and TypeScript tooling for Node.js, React, Next.js, and Vitest.",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -9,6 +9,7 @@
9
9
  "eslint"
10
10
  ],
11
11
  "author": "Richard Torcato",
12
+ "packageManager": "pnpm@11.1.3",
12
13
  "sideEffects": false,
13
14
  "license": "MIT",
14
15
  "repository": {
@@ -23,6 +24,8 @@
23
24
  "build": "pnpm build-cli",
24
25
  "build-cli": "rimraf ./dist/cli && tsc --project src/cli/tsconfig.json",
25
26
  "prepublishOnly": "./scripts/fix-bins.sh",
27
+ "dev": "pnpm --filter @rtorcato/docs dev",
28
+ "docs:build": "pnpm --filter @rtorcato/docs build",
26
29
  "==================== Common ====================": "",
27
30
  "lint": "pnpm exec biome lint --config-path=tooling/biome/biome.json src scripts",
28
31
  "format": "pnpm exec biome format --config-path=tooling/biome/biome.json src scripts",
@@ -70,6 +73,8 @@
70
73
  "tooling/tests/ssr-safety.d.mts",
71
74
  "tooling/tsup/index.ts",
72
75
  "tooling/biome/biome.json",
76
+ "tooling/changesets/config.json",
77
+ "tooling/oxlint/oxlintrc.json",
73
78
  "tooling/typedoc/typedoc.json",
74
79
  "tooling/semantic-release/*.mjs",
75
80
  "tooling/semantic-release/*.d.mts",
@@ -143,6 +148,8 @@
143
148
  },
144
149
  "./tsup": "./tooling/tsup/index.ts",
145
150
  "./biome": "./tooling/biome/biome.json",
151
+ "./changesets": "./tooling/changesets/config.json",
152
+ "./oxlint": "./tooling/oxlint/oxlintrc.json",
146
153
  "./typedoc": "./tooling/typedoc/typedoc.json",
147
154
  "./semantic-release": {
148
155
  "types": "./tooling/semantic-release/index.d.mts",
@@ -168,11 +175,13 @@
168
175
  "dependencies": {
169
176
  "chalk": "^5.6.2",
170
177
  "commander": "^14.0.3",
178
+ "diff": "^9.0.0",
171
179
  "fs-extra": "^11.3.2",
172
180
  "inquirer": "^14.0.2"
173
181
  },
174
182
  "devDependencies": {
175
183
  "@biomejs/biome": "^2.4.16",
184
+ "@types/diff": "^8.0.0",
176
185
  "@commitlint/cli": "^20.1.0",
177
186
  "@commitlint/config-conventional": "^21.0.2",
178
187
  "@commitlint/types": "^20.0.0",
@@ -215,7 +224,7 @@
215
224
  "tsup": "8.5.1",
216
225
  "typescript": "^5.9.3",
217
226
  "typescript-eslint": "^8.60.0",
218
- "vitest": "4.0.3"
227
+ "vitest": "^4.1.8"
219
228
  },
220
229
  "peerDependencies": {
221
230
  "@biomejs/biome": "^2.0.0",
@@ -232,6 +241,7 @@
232
241
  "@semantic-release/github": "^12.0.8",
233
242
  "@semantic-release/npm": "^13.0.0",
234
243
  "@semantic-release/release-notes-generator": "^14.0.0",
244
+ "@size-limit/preset-small-lib": "^12.0.0",
235
245
  "@total-typescript/ts-reset": "^0.6.0",
236
246
  "@typescript-eslint/eslint-plugin": "^8.0.0",
237
247
  "@typescript-eslint/parser": "^8.0.0",
@@ -245,7 +255,6 @@
245
255
  "prettier": "^3.0.0",
246
256
  "semantic-release": "^25.0.0",
247
257
  "size-limit": "^12.0.0",
248
- "@size-limit/preset-small-lib": "^12.0.0",
249
258
  "ts-jest": "^29.0.0",
250
259
  "tsup": "^8.0.0",
251
260
  "typescript": ">=5.0.0",
@@ -0,0 +1,35 @@
1
+ # Changesets preset
2
+
3
+ Shared [Changesets](https://github.com/changesets/changesets) configuration for projects using `@rtorcato/js-tooling`.
4
+
5
+ Changesets is a **monorepo-friendly alternative to semantic-release**. The release workflow is the same shape (CI bumps versions, generates a changelog, publishes to npm), but the *intent* is captured in changeset markdown files at PR time rather than parsed from commit messages.
6
+
7
+ Pick one — Changesets or semantic-release — per repo. The `doctor` check flags repos configured for both.
8
+
9
+ ## Usage
10
+
11
+ ```bash
12
+ npx @rtorcato/js-tooling copy changesets
13
+ ```
14
+
15
+ This scaffolds `.changeset/config.json` from this preset. After that:
16
+
17
+ ```bash
18
+ pnpm changeset # interactive — author a changeset for the current change
19
+ pnpm changeset version # consume changesets, bump versions, write CHANGELOG.md
20
+ pnpm changeset publish # publish to npm
21
+ ```
22
+
23
+ CI typically runs the [Changesets release bot](https://github.com/changesets/action), which opens a "Version Packages" PR when changesets are present and publishes on merge.
24
+
25
+ ## Why pick this over semantic-release
26
+
27
+ - **Monorepos** — Changesets handles multi-package version bumps in a way semantic-release doesn't out of the box.
28
+ - **Explicit intent** — Authors declare a change's bump level in the changeset file rather than encoding it in commit message conventions, which is more forgiving for human commits.
29
+ - **Pre-release flows** — `--snapshot` and `pre enter <tag>` are first-class.
30
+
31
+ ## Why pick semantic-release instead
32
+
33
+ - **Single-package repos** — Less ceremony; nothing to author per change.
34
+ - **Strict conventional-commits discipline already in place.**
35
+ - **Existing CI built around the `npm run release` path.**
@@ -0,0 +1,11 @@
1
+ {
2
+ "$schema": "https://unpkg.com/@changesets/config@3.0.5/schema.json",
3
+ "changelog": "@changesets/cli/changelog",
4
+ "commit": false,
5
+ "fixed": [],
6
+ "linked": [],
7
+ "access": "public",
8
+ "baseBranch": "main",
9
+ "updateInternalDependencies": "patch",
10
+ "ignore": []
11
+ }
@@ -0,0 +1,25 @@
1
+ # Oxlint preset
2
+
3
+ Shared [Oxlint](https://oxc.rs/docs/guide/usage/linter.html) configuration for projects using `@rtorcato/js-tooling`.
4
+
5
+ Oxlint is a Rust-based linter that's 50–100× faster than ESLint. It is intentionally **additive** to Biome — Biome handles formatting and the broad lint baseline, Oxlint adds a faster pass for the type-aware and import rules Biome doesn't cover yet.
6
+
7
+ ## Usage
8
+
9
+ ```bash
10
+ npx @rtorcato/js-tooling copy oxlint
11
+ ```
12
+
13
+ This drops `.oxlintrc.json` at the project root, extending the conventions in this preset. Run it with:
14
+
15
+ ```bash
16
+ pnpm oxlint
17
+ # or
18
+ npx oxlint
19
+ ```
20
+
21
+ ## Notes
22
+
23
+ - Oxlint shares its rule catalog with ESLint's plugins (`typescript`, `unicorn`, `oxc`, `import`), so most ESLint rules you know already work here.
24
+ - The preset disables Biome-overlapping rules to keep CI noise down — Biome stays the source of truth for formatting and the baseline lint set.
25
+ - For projects without Biome, you can run Oxlint standalone and re-enable the `style` and `pedantic` categories.
@@ -0,0 +1,28 @@
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/oxc-project/oxc/main/npm/oxlint/configuration_schema.json",
3
+ "categories": {
4
+ "correctness": "error",
5
+ "perf": "warn",
6
+ "suspicious": "warn",
7
+ "pedantic": "off",
8
+ "style": "off",
9
+ "restriction": "off",
10
+ "nursery": "off"
11
+ },
12
+ "plugins": ["typescript", "unicorn", "oxc", "import"],
13
+ "rules": {
14
+ "no-console": "warn",
15
+ "no-debugger": "error",
16
+ "no-empty": "warn",
17
+ "no-unused-vars": "off",
18
+ "@typescript-eslint/no-unused-vars": [
19
+ "warn",
20
+ { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" }
21
+ ],
22
+ "unicorn/filename-case": "off",
23
+ "unicorn/no-null": "off",
24
+ "unicorn/prevent-abbreviations": "off",
25
+ "import/no-default-export": "off"
26
+ },
27
+ "ignorePatterns": ["node_modules", "dist", "build", "coverage", ".next", "*.d.ts"]
28
+ }