@rtorcato/js-tooling 2.11.0 ā 2.13.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.
|
@@ -630,6 +630,23 @@ function checkLockfile(lock) {
|
|
|
630
630
|
detail: `.js-tooling.json v${lock.version} (written by ${lock.writtenBy})`,
|
|
631
631
|
};
|
|
632
632
|
}
|
|
633
|
+
async function checkCodeowners(dir) {
|
|
634
|
+
for (const candidate of ['CODEOWNERS', '.github/CODEOWNERS', 'docs/CODEOWNERS']) {
|
|
635
|
+
if (await fs.pathExists(path.join(dir, candidate))) {
|
|
636
|
+
return {
|
|
637
|
+
check: 'CODEOWNERS',
|
|
638
|
+
status: 'ok',
|
|
639
|
+
detail: `${candidate} found`,
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
return {
|
|
644
|
+
check: 'CODEOWNERS',
|
|
645
|
+
status: 'optional-missing',
|
|
646
|
+
detail: 'no CODEOWNERS file',
|
|
647
|
+
hint: 'Run `npx @rtorcato/js-tooling fix codeowners` to scaffold .github/CODEOWNERS',
|
|
648
|
+
};
|
|
649
|
+
}
|
|
633
650
|
async function checkGitLabCI(dir) {
|
|
634
651
|
for (const candidate of ['.gitlab-ci.yml', '.gitlab-ci.yaml']) {
|
|
635
652
|
if (await fs.pathExists(path.join(dir, candidate))) {
|
|
@@ -672,6 +689,7 @@ export async function runDoctor(dir) {
|
|
|
672
689
|
results.push(await checkDependabot(targetDir));
|
|
673
690
|
results.push(await checkCodeQL(targetDir));
|
|
674
691
|
results.push(await checkGitLabCI(targetDir));
|
|
692
|
+
results.push(await checkCodeowners(targetDir));
|
|
675
693
|
results.push(await checkTreeshakeSetup(targetDir, pkg));
|
|
676
694
|
// Lockfile-driven demotion: if the lock records an intentional opt-out for a
|
|
677
695
|
// check that's currently optional-missing, demote it to ok with a clear detail.
|
package/dist/cli/commands/fix.js
CHANGED
|
@@ -6,7 +6,8 @@ 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
8
|
import { generateESLintConfig, generatePrettierConfig } from '../generators/linting.js';
|
|
9
|
-
import { ensureEnginesNode, generateEditorConfig, generateKnipConfig, generateNvmrc, generateSizeLimitConfig, } from '../generators/misc.js';
|
|
9
|
+
import { ensureEnginesNode, generateCodeowners, generateEditorConfig, generateKnipConfig, generateNvmrc, generateSizeLimitConfig, } from '../generators/misc.js';
|
|
10
|
+
import { generateConfigs } from '../generators/index.js';
|
|
10
11
|
import { composeVerifyScriptFromPkg } from '../generators/package-json.js';
|
|
11
12
|
import { generateCodeQLWorkflow, generateDependabotConfig } from '../generators/security.js';
|
|
12
13
|
import { generateVitestConfig } from '../generators/testing.js';
|
|
@@ -15,6 +16,7 @@ import { copyPreset } from '../utils/copy-preset.js';
|
|
|
15
16
|
import { LOCKFILE_NAME, readLockfile, updateLockfileConfig, writeLockfile, } from '../utils/lockfile.js';
|
|
16
17
|
import { runDoctor } from './doctor.js';
|
|
17
18
|
import { declinedInLock, lockfilePatchForTarget } from './fix-targets.js';
|
|
19
|
+
import { computeFileList } from './setup-presets.js';
|
|
18
20
|
function inferProjectConfig(pkg) {
|
|
19
21
|
const deps = {
|
|
20
22
|
...(pkg?.dependencies ?? {}),
|
|
@@ -229,6 +231,18 @@ const FIXERS = [
|
|
|
229
231
|
return { filesWritten: ['.github/workflows/codeql.yml'] };
|
|
230
232
|
},
|
|
231
233
|
},
|
|
234
|
+
{
|
|
235
|
+
target: 'codeowners',
|
|
236
|
+
description: 'Scaffold .github/CODEOWNERS with commented examples',
|
|
237
|
+
appliesTo: ['CODEOWNERS'],
|
|
238
|
+
outputs: ['.github/CODEOWNERS'],
|
|
239
|
+
riskLevel: 'safe-add',
|
|
240
|
+
canFixDrift: false,
|
|
241
|
+
async run({ targetDir }) {
|
|
242
|
+
const written = await generateCodeowners(targetDir);
|
|
243
|
+
return { filesWritten: [written] };
|
|
244
|
+
},
|
|
245
|
+
},
|
|
232
246
|
{
|
|
233
247
|
target: 'editorconfig',
|
|
234
248
|
description: 'Scaffold .editorconfig (UTF-8, LF, tab indent)',
|
|
@@ -380,6 +394,16 @@ function logTargets() {
|
|
|
380
394
|
console.log(` ${chalk.green('ā')} ${chalk.bold(f.target)}: ${chalk.gray(f.description)}`);
|
|
381
395
|
}
|
|
382
396
|
}
|
|
397
|
+
export function listFixers() {
|
|
398
|
+
return FIXERS.map((f) => ({
|
|
399
|
+
target: f.target,
|
|
400
|
+
description: f.description,
|
|
401
|
+
appliesTo: f.appliesTo,
|
|
402
|
+
outputs: f.outputs,
|
|
403
|
+
riskLevel: f.riskLevel ?? 'destructive',
|
|
404
|
+
canFixDrift: f.canFixDrift ?? false,
|
|
405
|
+
}));
|
|
406
|
+
}
|
|
383
407
|
async function applyFixer(fixer, result, targetDir, pkg, lock, dryRun, silent) {
|
|
384
408
|
if (dryRun) {
|
|
385
409
|
if (!silent) {
|
|
@@ -442,6 +466,75 @@ export async function fixCommand(target, options = {}) {
|
|
|
442
466
|
// JSON mode implies --yes so prompts don't corrupt the output stream.
|
|
443
467
|
const assumeYes = options.yes === true || json;
|
|
444
468
|
const silent = json;
|
|
469
|
+
if (options.list) {
|
|
470
|
+
const summary = listFixers();
|
|
471
|
+
if (json) {
|
|
472
|
+
console.log(JSON.stringify({ targets: summary }, null, 2));
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
console.log(chalk.cyan('\nš§ Registered fix targets:\n'));
|
|
476
|
+
for (const f of summary) {
|
|
477
|
+
console.log(` ${chalk.green('ā')} ${chalk.bold(f.target)}`);
|
|
478
|
+
console.log(` ${chalk.gray(f.description)}`);
|
|
479
|
+
console.log(` ${chalk.dim(`risk=${f.riskLevel}, drift=${f.canFixDrift ? 'yes' : 'no'}, outputs=${f.outputs.join(', ')}`)}`);
|
|
480
|
+
}
|
|
481
|
+
console.log();
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
if (options.resync) {
|
|
485
|
+
if (target) {
|
|
486
|
+
console.error(chalk.red('\nā --resync cannot be combined with a [target] argument\n'));
|
|
487
|
+
process.exit(1);
|
|
488
|
+
}
|
|
489
|
+
const resyncLock = await readLockfile(targetDir);
|
|
490
|
+
if (!resyncLock) {
|
|
491
|
+
if (json) {
|
|
492
|
+
console.log(JSON.stringify({ directory: targetDir, error: 'no-lockfile', hint: 'run `fix lockfile` first' }, null, 2));
|
|
493
|
+
}
|
|
494
|
+
else {
|
|
495
|
+
console.error(chalk.red(`\nā No ${LOCKFILE_NAME} found ā run \`fix lockfile\` first to record choices\n`));
|
|
496
|
+
}
|
|
497
|
+
process.exit(1);
|
|
498
|
+
}
|
|
499
|
+
const files = computeFileList(resyncLock.config);
|
|
500
|
+
if (!silent) {
|
|
501
|
+
console.log(chalk.cyan(`\nš Resync from ${LOCKFILE_NAME} (${files.length} files in scope)\n`));
|
|
502
|
+
}
|
|
503
|
+
if (dryRun) {
|
|
504
|
+
if (json) {
|
|
505
|
+
console.log(JSON.stringify({ directory: targetDir, mode: 'resync', dryRun: true, files }, null, 2));
|
|
506
|
+
}
|
|
507
|
+
else {
|
|
508
|
+
for (const f of files)
|
|
509
|
+
console.log(chalk.cyan(` [dry-run] would write: ${f}`));
|
|
510
|
+
console.log();
|
|
511
|
+
}
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
if (!assumeYes) {
|
|
515
|
+
const { confirm } = await inquirer.prompt([
|
|
516
|
+
{
|
|
517
|
+
type: 'confirm',
|
|
518
|
+
name: 'confirm',
|
|
519
|
+
message: `Re-scaffold ${files.length} file(s) from ${LOCKFILE_NAME}? Generators preserve existing customizations where possible, but README.md will be rewritten.`,
|
|
520
|
+
default: false,
|
|
521
|
+
},
|
|
522
|
+
]);
|
|
523
|
+
if (!confirm) {
|
|
524
|
+
console.log(chalk.gray(' skipped\n'));
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
await generateConfigs(resyncLock.config, targetDir);
|
|
529
|
+
await writeLockfile(targetDir, resyncLock.config);
|
|
530
|
+
if (json) {
|
|
531
|
+
console.log(JSON.stringify({ directory: targetDir, mode: 'resync', dryRun: false, files }, null, 2));
|
|
532
|
+
}
|
|
533
|
+
else {
|
|
534
|
+
console.log(chalk.green(` ā
resynced ${files.length} file(s)\n`));
|
|
535
|
+
}
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
445
538
|
const pkg = await readPackageJson(targetDir);
|
|
446
539
|
const lock = await readLockfile(targetDir);
|
|
447
540
|
const results = await runDoctor(targetDir);
|
|
@@ -30,6 +30,20 @@ const SIZE_LIMIT_CONFIG = [
|
|
|
30
30
|
limit: '10 kB',
|
|
31
31
|
},
|
|
32
32
|
];
|
|
33
|
+
const CODEOWNERS_CONTENT = `# .github/CODEOWNERS
|
|
34
|
+
# Each line is a file pattern followed by one or more owners.
|
|
35
|
+
# Owners can be GitHub usernames (@user) or team names (@org/team).
|
|
36
|
+
# Order matters ā the last matching pattern wins.
|
|
37
|
+
# See https://docs.github.com/articles/about-code-owners/
|
|
38
|
+
#
|
|
39
|
+
# Examples:
|
|
40
|
+
# * @your-username
|
|
41
|
+
# /src/api/ @backend-team
|
|
42
|
+
# /docs/ @docs-team @your-username
|
|
43
|
+
# *.md @docs-team
|
|
44
|
+
|
|
45
|
+
* @TODO-owner
|
|
46
|
+
`;
|
|
33
47
|
export async function generateEditorConfig(targetDir) {
|
|
34
48
|
await fs.writeFile(path.join(targetDir, '.editorconfig'), EDITORCONFIG_CONTENT);
|
|
35
49
|
}
|
|
@@ -54,6 +68,12 @@ export async function generateKnipConfig(targetDir) {
|
|
|
54
68
|
export async function generateSizeLimitConfig(targetDir) {
|
|
55
69
|
await fs.writeJson(path.join(targetDir, '.size-limit.json'), SIZE_LIMIT_CONFIG, { spaces: 2 });
|
|
56
70
|
}
|
|
71
|
+
export async function generateCodeowners(targetDir) {
|
|
72
|
+
const filepath = path.join(targetDir, '.github', 'CODEOWNERS');
|
|
73
|
+
await fs.ensureDir(path.dirname(filepath));
|
|
74
|
+
await fs.writeFile(filepath, CODEOWNERS_CONTENT);
|
|
75
|
+
return '.github/CODEOWNERS';
|
|
76
|
+
}
|
|
57
77
|
export async function generateMiscBaseline(targetDir) {
|
|
58
78
|
await generateEditorConfig(targetDir);
|
|
59
79
|
await generateNvmrc(targetDir);
|
package/dist/cli/index.js
CHANGED
|
@@ -221,15 +221,22 @@ program
|
|
|
221
221
|
.option('--yes', 'Assume yes to all prompts (including drift overwrites)')
|
|
222
222
|
.option('--dry-run', 'Print what would change without writing files')
|
|
223
223
|
.option('--json', 'Emit machine-readable JSON output (implies --yes)')
|
|
224
|
+
.option('--list', 'List all registered fix targets and exit')
|
|
225
|
+
.option('--resync', 'Re-scaffold every file recorded in .js-tooling.json')
|
|
224
226
|
.action((target, options) => fixCommand(target, {
|
|
225
227
|
directory: options.directory,
|
|
226
228
|
yes: options.yes,
|
|
227
229
|
dryRun: options.dryRun,
|
|
228
230
|
json: options.json,
|
|
231
|
+
list: options.list,
|
|
232
|
+
resync: options.resync,
|
|
229
233
|
}));
|
|
230
234
|
program.hook('preAction', async (_, actionCommand) => {
|
|
231
235
|
const name = actionCommand.name();
|
|
232
236
|
if (name === 'setup' || name === 'doctor' || name === 'fix') {
|
|
237
|
+
// `fix --list` is read-only and safe to run anywhere, including this repo.
|
|
238
|
+
if (name === 'fix' && actionCommand.opts().list)
|
|
239
|
+
return;
|
|
233
240
|
const dir = actionCommand.opts().directory ?? process.cwd();
|
|
234
241
|
if (await isSelfRepo(dir)) {
|
|
235
242
|
console.log(chalk.yellow('\nā ļø This command cannot be run inside the @rtorcato/js-tooling repo itself.\n'));
|