@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.
- package/dist/cli/commands/doctor.js +36 -0
- package/dist/cli/commands/fix.js +145 -0
- package/dist/cli/commands/setup-presets.js +6 -0
- package/dist/cli/commands/setup.js +19 -5
- package/dist/cli/generators/build.js +11 -0
- package/dist/cli/generators/linting.js +11 -0
- package/dist/cli/index.js +14 -0
- package/dist/cli/utils/copy-preset.js +10 -0
- package/package.json +12 -3
- package/tooling/changesets/README.md +35 -0
- package/tooling/changesets/config.json +11 -0
- package/tooling/oxlint/README.md +25 -0
- package/tooling/oxlint/oxlintrc.json +28 -0
|
@@ -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',
|
package/dist/cli/commands/fix.js
CHANGED
|
@@ -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: '
|
|
188
|
-
name: '
|
|
189
|
-
message: '🚀
|
|
190
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
+
}
|