@rtorcato/js-tooling 2.4.0 → 2.5.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/README.md +3 -1
- package/dist/cli/commands/fix.js +66 -21
- package/dist/cli/commands/setup.js +31 -1
- package/dist/cli/index.js +10 -1
- package/package.json +1 -1
- package/tooling/eslint/README.md +0 -3
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ JavaScript and TypeScript tooling for Node.js, React, Next.js, and Vitest.
|
|
|
9
9
|
[](https://codecov.io/gh/rtorcato/js-tooling)
|
|
10
10
|
[](https://opensource.org/licenses/MIT)
|
|
11
11
|
|
|
12
|
-
Most tooling libraries give you one piece — just TypeScript configs, or just an ESLint preset. **js-tooling** covers the entire lifecycle: TypeScript, Biome/ESLint, Vitest/Jest, Commitlint, Husky, Semantic Release,
|
|
12
|
+
Most tooling libraries give you one piece — just TypeScript configs, or just an ESLint preset. **js-tooling** covers the entire lifecycle: TypeScript, Biome/ESLint, Vitest/Jest, Commitlint, Husky, Semantic Release, GitHub Actions CI, and supply-chain security (Dependabot + CodeQL) — all wired together. The interactive `setup` wizard scaffolds everything in one shot; `doctor` checks an existing project for drift; `fix` applies the missing pieces incrementally.
|
|
13
13
|
|
|
14
14
|
**[Full documentation →](https://rtorcato.github.io/js-tooling/)**
|
|
15
15
|
|
|
@@ -23,6 +23,8 @@ npx @rtorcato/js-tooling setup
|
|
|
23
23
|
|
|
24
24
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
25
25
|
|
|
26
|
+
**v2.4.0** — New `fix` command applies scaffolders for items `doctor` flags, with `--yes` and `--dry-run` flags. Drift never auto-overwrites — every existing file you'd lose is confirmed first. Doctor grew checks for `engines.node`, `.editorconfig`, `.nvmrc`, Husky, `lint-staged`, semantic-release, knip, GitHub Actions, GitLab CI, Dependabot, and CodeQL — plus a `Next steps:` footer that names the exact `fix` command to run for each finding. Setup wizard adds a "Include security automation?" prompt for Dependabot + CodeQL.
|
|
27
|
+
|
|
26
28
|
**v2.0.0** — All 39 tool packages moved from `dependencies` to `peerDependencies`. Add them to your own `devDependencies`. Also ships: `doctor` subcommand, generator unit tests, Dependabot, CI matrix (Node 22 + 24).
|
|
27
29
|
|
|
28
30
|
**v1.1.0** — Stricter commitlint limits, fix for CLI path resolution when copying configs.
|
package/dist/cli/commands/fix.js
CHANGED
|
@@ -269,16 +269,18 @@ function logTargets() {
|
|
|
269
269
|
console.log(` ${chalk.green('●')} ${chalk.bold(f.target)}: ${chalk.gray(f.description)}`);
|
|
270
270
|
}
|
|
271
271
|
}
|
|
272
|
-
async function applyFixer(fixer, result, targetDir, pkg, dryRun) {
|
|
272
|
+
async function applyFixer(fixer, result, targetDir, pkg, dryRun, silent) {
|
|
273
273
|
if (dryRun) {
|
|
274
|
-
|
|
275
|
-
|
|
274
|
+
if (!silent) {
|
|
275
|
+
console.log(chalk.cyan(` [dry-run] would write: ${fixer.outputs.join(', ')}`));
|
|
276
|
+
}
|
|
277
|
+
return { filesWritten: [], dryRun: true };
|
|
276
278
|
}
|
|
277
279
|
const { filesWritten } = await fixer.run({ targetDir, pkg, result });
|
|
278
|
-
if (filesWritten.length > 0) {
|
|
280
|
+
if (!silent && filesWritten.length > 0) {
|
|
279
281
|
console.log(chalk.green(` ✅ wrote ${filesWritten.join(', ')}`));
|
|
280
282
|
}
|
|
281
|
-
return {
|
|
283
|
+
return { filesWritten, dryRun: false };
|
|
282
284
|
}
|
|
283
285
|
async function confirmApply(fixer, result, assumeYes) {
|
|
284
286
|
if (assumeYes)
|
|
@@ -292,15 +294,35 @@ async function confirmApply(fixer, result, assumeYes) {
|
|
|
292
294
|
]);
|
|
293
295
|
return confirm === true;
|
|
294
296
|
}
|
|
297
|
+
function recordFor(target, check, doctorStatus, status, filesWritten) {
|
|
298
|
+
return { target, check, status, doctorStatus, filesWritten };
|
|
299
|
+
}
|
|
295
300
|
export async function fixCommand(target, options = {}) {
|
|
296
301
|
const targetDir = path.resolve(options.directory ?? process.cwd());
|
|
297
|
-
const assumeYes = options.yes === true;
|
|
298
302
|
const dryRun = options.dryRun === true;
|
|
303
|
+
const json = options.json === true;
|
|
304
|
+
// JSON mode implies --yes so prompts don't corrupt the output stream.
|
|
305
|
+
const assumeYes = options.yes === true || json;
|
|
306
|
+
const silent = json;
|
|
299
307
|
const pkg = await readPackageJson(targetDir);
|
|
300
308
|
const results = await runDoctor(targetDir);
|
|
309
|
+
const actions = [];
|
|
310
|
+
const emitJson = (resolvedTarget) => {
|
|
311
|
+
const payload = { directory: targetDir, target: resolvedTarget, actions };
|
|
312
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
313
|
+
};
|
|
301
314
|
if (target) {
|
|
302
315
|
const fixer = findFixer(target);
|
|
303
316
|
if (!fixer) {
|
|
317
|
+
if (json) {
|
|
318
|
+
console.log(JSON.stringify({
|
|
319
|
+
directory: targetDir,
|
|
320
|
+
error: 'unknown-target',
|
|
321
|
+
target,
|
|
322
|
+
available: FIXERS.map((f) => f.target),
|
|
323
|
+
}, null, 2));
|
|
324
|
+
process.exit(1);
|
|
325
|
+
}
|
|
304
326
|
console.error(chalk.red(`\n❌ Unknown fix target: ${target}\n`));
|
|
305
327
|
logTargets();
|
|
306
328
|
console.log();
|
|
@@ -309,46 +331,69 @@ export async function fixCommand(target, options = {}) {
|
|
|
309
331
|
const result = results.find((r) => fixer.appliesTo.includes(r.check)) ??
|
|
310
332
|
{ check: fixer.appliesTo[0] ?? fixer.target, status: 'missing', detail: '' };
|
|
311
333
|
if (result.status === 'ok') {
|
|
334
|
+
actions.push(recordFor(fixer.target, result.check, 'ok', 'already-ok', []));
|
|
335
|
+
if (json)
|
|
336
|
+
return emitJson(fixer.target);
|
|
312
337
|
console.log(chalk.green(`\n✅ ${result.check} is already configured\n`));
|
|
313
338
|
return;
|
|
314
339
|
}
|
|
315
|
-
|
|
340
|
+
if (!silent) {
|
|
341
|
+
console.log(chalk.cyan(`\n🔧 ${fixer.target} — ${chalk.bold(result.check)} is ${result.status}\n`));
|
|
342
|
+
}
|
|
316
343
|
const ok = await confirmApply(fixer, result, assumeYes);
|
|
317
344
|
if (!ok) {
|
|
345
|
+
actions.push(recordFor(fixer.target, result.check, result.status, 'skipped', []));
|
|
346
|
+
if (json)
|
|
347
|
+
return emitJson(fixer.target);
|
|
318
348
|
console.log(chalk.gray(' skipped\n'));
|
|
319
349
|
return;
|
|
320
350
|
}
|
|
321
|
-
await applyFixer(fixer, result, targetDir, pkg, dryRun);
|
|
351
|
+
const outcome = await applyFixer(fixer, result, targetDir, pkg, dryRun, silent);
|
|
352
|
+
actions.push(recordFor(fixer.target, result.check, result.status, outcome.dryRun ? 'dry-run' : 'applied', outcome.filesWritten));
|
|
353
|
+
if (json)
|
|
354
|
+
return emitJson(fixer.target);
|
|
322
355
|
console.log();
|
|
323
356
|
return;
|
|
324
357
|
}
|
|
325
|
-
// No target — walk all non-ok results
|
|
326
358
|
const fixable = results.filter((r) => r.status !== 'ok');
|
|
327
359
|
if (fixable.length === 0) {
|
|
360
|
+
if (json)
|
|
361
|
+
return emitJson(null);
|
|
328
362
|
console.log(chalk.green('\n✅ All checks pass — nothing to fix\n'));
|
|
329
363
|
return;
|
|
330
364
|
}
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
let
|
|
365
|
+
if (!silent) {
|
|
366
|
+
console.log(chalk.cyan(`\n🔧 ${fixable.length} item(s) to address\n`));
|
|
367
|
+
}
|
|
368
|
+
let appliedCount = 0;
|
|
369
|
+
let skippedCount = 0;
|
|
370
|
+
let unsupportedCount = 0;
|
|
335
371
|
for (const result of fixable) {
|
|
336
372
|
const fixer = findFixerForCheck(result.check);
|
|
337
373
|
if (!fixer) {
|
|
338
|
-
|
|
339
|
-
|
|
374
|
+
actions.push(recordFor(null, result.check, result.status, 'unsupported', []));
|
|
375
|
+
if (!silent)
|
|
376
|
+
console.log(chalk.gray(` — ${result.check}: no fixer registered`));
|
|
377
|
+
unsupportedCount++;
|
|
340
378
|
continue;
|
|
341
379
|
}
|
|
342
|
-
|
|
380
|
+
if (!silent) {
|
|
381
|
+
console.log(` ${chalk.bold(result.check)} (${result.status}) → ${fixer.target}`);
|
|
382
|
+
}
|
|
343
383
|
const ok = await confirmApply(fixer, result, assumeYes);
|
|
344
384
|
if (!ok) {
|
|
345
|
-
|
|
346
|
-
|
|
385
|
+
actions.push(recordFor(fixer.target, result.check, result.status, 'skipped', []));
|
|
386
|
+
if (!silent)
|
|
387
|
+
console.log(chalk.gray(' skipped'));
|
|
388
|
+
skippedCount++;
|
|
347
389
|
continue;
|
|
348
390
|
}
|
|
349
|
-
await applyFixer(fixer, result, targetDir, pkg, dryRun);
|
|
350
|
-
applied
|
|
391
|
+
const outcome = await applyFixer(fixer, result, targetDir, pkg, dryRun, silent);
|
|
392
|
+
actions.push(recordFor(fixer.target, result.check, result.status, outcome.dryRun ? 'dry-run' : 'applied', outcome.filesWritten));
|
|
393
|
+
appliedCount++;
|
|
351
394
|
}
|
|
395
|
+
if (json)
|
|
396
|
+
return emitJson(null);
|
|
352
397
|
console.log();
|
|
353
|
-
console.log(` Summary: ${chalk.green(`${
|
|
398
|
+
console.log(` Summary: ${chalk.green(`${appliedCount} applied`)}, ${chalk.gray(`${skippedCount} skipped`)}, ${chalk.yellow(`${unsupportedCount} unsupported`)}\n`);
|
|
354
399
|
}
|
|
@@ -220,6 +220,36 @@ function showNextSteps(config, _targetDir) {
|
|
|
220
220
|
steps.forEach((step, index) => {
|
|
221
221
|
console.log(` ${index + 1}. ${step}`);
|
|
222
222
|
});
|
|
223
|
-
|
|
223
|
+
const skipped = collectSkippedFixSuggestions(config);
|
|
224
|
+
if (skipped.length > 0) {
|
|
225
|
+
console.log(chalk.bold('\n💡 Want to add something you skipped?\n'));
|
|
226
|
+
for (const s of skipped) {
|
|
227
|
+
console.log(` ${chalk.gray('-')} ${s}`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
console.log(chalk.dim('\n📁 All configuration files have been generated in your project directory.'));
|
|
224
231
|
console.log(chalk.dim(' You can modify them to suit your specific needs.\n'));
|
|
225
232
|
}
|
|
233
|
+
function collectSkippedFixSuggestions(config) {
|
|
234
|
+
const suggestions = [];
|
|
235
|
+
if (!config.gitHooks) {
|
|
236
|
+
suggestions.push('Run `npx @rtorcato/js-tooling fix husky` to add git hooks later');
|
|
237
|
+
}
|
|
238
|
+
if (!config.commitLint) {
|
|
239
|
+
suggestions.push('Run `npx @rtorcato/js-tooling fix commitlint` to add conventional-commit linting');
|
|
240
|
+
}
|
|
241
|
+
if (!config.semanticRelease && config.projectType === 'library') {
|
|
242
|
+
suggestions.push('Run `npx @rtorcato/js-tooling fix semantic-release` to add automated releases');
|
|
243
|
+
}
|
|
244
|
+
if (!config.securityAutomation) {
|
|
245
|
+
suggestions.push('Run `npx @rtorcato/js-tooling fix dependabot` and `fix codeql` for security automation');
|
|
246
|
+
}
|
|
247
|
+
if (config.linting.tool === 'none') {
|
|
248
|
+
suggestions.push('Run `npx @rtorcato/js-tooling fix biome` or `fix eslint` to add linting');
|
|
249
|
+
}
|
|
250
|
+
if (config.testing.framework === 'none') {
|
|
251
|
+
suggestions.push('Run `npx @rtorcato/js-tooling fix vitest` to add a test runner');
|
|
252
|
+
}
|
|
253
|
+
suggestions.push('Run `npx @rtorcato/js-tooling doctor` any time to audit drift');
|
|
254
|
+
return suggestions;
|
|
255
|
+
}
|
package/dist/cli/index.js
CHANGED
|
@@ -74,14 +74,21 @@ program
|
|
|
74
74
|
{ name: 'Playwright', desc: 'End-to-end testing configuration' },
|
|
75
75
|
{ name: 'Commitlint', desc: 'Conventional commit linting' },
|
|
76
76
|
{ name: 'Husky', desc: 'Git hooks for pre-commit validation' },
|
|
77
|
+
{ name: 'lint-staged', desc: 'Run linters on staged files (pairs with Husky)' },
|
|
77
78
|
{ name: 'Semantic Release', desc: 'Automated versioning and publishing' },
|
|
78
79
|
{ name: 'tsup', desc: 'TypeScript bundler configuration' },
|
|
79
80
|
{ name: 'esbuild', desc: 'Fast JavaScript bundler configuration' },
|
|
81
|
+
{ name: 'EditorConfig', desc: 'Cross-editor formatting consistency (.editorconfig)' },
|
|
82
|
+
{ name: '.nvmrc', desc: 'Pin Node version per repository' },
|
|
83
|
+
{ name: 'knip', desc: 'Find unused files, exports, and dependencies' },
|
|
84
|
+
{ name: 'Dependabot', desc: 'Weekly automated dependency updates' },
|
|
85
|
+
{ name: 'CodeQL', desc: 'GitHub security scanning workflow' },
|
|
80
86
|
];
|
|
81
87
|
configs.forEach(({ name, desc }) => {
|
|
82
88
|
console.log(` ${chalk.green('●')} ${chalk.bold(name)}: ${chalk.gray(desc)}`);
|
|
83
89
|
});
|
|
84
|
-
console.log(chalk.dim('\n💡 Run
|
|
90
|
+
console.log(chalk.dim('\n💡 Run `js-tooling setup` for a new project'));
|
|
91
|
+
console.log(chalk.dim(' or `js-tooling fix` to apply missing pieces to an existing one\n'));
|
|
85
92
|
});
|
|
86
93
|
program
|
|
87
94
|
.command('doctor')
|
|
@@ -95,10 +102,12 @@ program
|
|
|
95
102
|
.option('-d, --directory <path>', 'Target directory', process.cwd())
|
|
96
103
|
.option('--yes', 'Assume yes to all prompts (including drift overwrites)')
|
|
97
104
|
.option('--dry-run', 'Print what would change without writing files')
|
|
105
|
+
.option('--json', 'Emit machine-readable JSON output (implies --yes)')
|
|
98
106
|
.action((target, options) => fixCommand(target, {
|
|
99
107
|
directory: options.directory,
|
|
100
108
|
yes: options.yes,
|
|
101
109
|
dryRun: options.dryRun,
|
|
110
|
+
json: options.json,
|
|
102
111
|
}));
|
|
103
112
|
program.hook('preAction', async (_, actionCommand) => {
|
|
104
113
|
const name = actionCommand.name();
|
package/package.json
CHANGED
package/tooling/eslint/README.md
DELETED