@rtorcato/js-tooling 2.10.0 → 2.11.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.
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
3
|
import fs from 'fs-extra';
|
|
4
|
-
import {
|
|
4
|
+
import { LOCKFILE_VERSION, readLockfile } from '../utils/lockfile.js';
|
|
5
|
+
import { declinedInLock, getFixTargetForCheck } from './fix-targets.js';
|
|
5
6
|
const PACKAGE = '@rtorcato/js-tooling';
|
|
6
7
|
const NODE_MIN_MAJOR = 22;
|
|
7
8
|
const NODE_LTS_REQUIREMENTS = {
|
|
@@ -606,6 +607,29 @@ async function checkTreeshakeSetup(dir, pkg) {
|
|
|
606
607
|
hint: 'Run `npx @rtorcato/js-tooling fix treeshake-check` to scaffold an esbuild metafile assertion',
|
|
607
608
|
};
|
|
608
609
|
}
|
|
610
|
+
function checkLockfile(lock) {
|
|
611
|
+
if (!lock) {
|
|
612
|
+
return {
|
|
613
|
+
check: 'lockfile',
|
|
614
|
+
status: 'optional-missing',
|
|
615
|
+
detail: 'no .js-tooling.json — doctor cannot tell intentional opt-outs from drift',
|
|
616
|
+
hint: 'Run `npx @rtorcato/js-tooling fix lockfile` to record current choices',
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
if (lock.version > LOCKFILE_VERSION) {
|
|
620
|
+
return {
|
|
621
|
+
check: 'lockfile',
|
|
622
|
+
status: 'drift',
|
|
623
|
+
detail: `.js-tooling.json version ${lock.version} is newer than this CLI supports (v${LOCKFILE_VERSION})`,
|
|
624
|
+
hint: 'Upgrade @rtorcato/js-tooling to a release that supports this lockfile version',
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
return {
|
|
628
|
+
check: 'lockfile',
|
|
629
|
+
status: 'ok',
|
|
630
|
+
detail: `.js-tooling.json v${lock.version} (written by ${lock.writtenBy})`,
|
|
631
|
+
};
|
|
632
|
+
}
|
|
609
633
|
async function checkGitLabCI(dir) {
|
|
610
634
|
for (const candidate of ['.gitlab-ci.yml', '.gitlab-ci.yaml']) {
|
|
611
635
|
if (await fs.pathExists(path.join(dir, candidate))) {
|
|
@@ -626,9 +650,11 @@ async function checkGitLabCI(dir) {
|
|
|
626
650
|
export async function runDoctor(dir) {
|
|
627
651
|
const targetDir = path.resolve(dir);
|
|
628
652
|
const pkg = await readPackageJson(targetDir);
|
|
653
|
+
const lock = await readLockfile(targetDir);
|
|
629
654
|
const results = [];
|
|
630
655
|
results.push(evaluateNodeVersion(process.version));
|
|
631
656
|
results.push(checkPackageJson(pkg));
|
|
657
|
+
results.push(checkLockfile(lock));
|
|
632
658
|
results.push(checkEnginesNode(pkg));
|
|
633
659
|
results.push(await checkEditorConfig(targetDir));
|
|
634
660
|
results.push(await checkNodeVersionPin(targetDir));
|
|
@@ -647,6 +673,21 @@ export async function runDoctor(dir) {
|
|
|
647
673
|
results.push(await checkCodeQL(targetDir));
|
|
648
674
|
results.push(await checkGitLabCI(targetDir));
|
|
649
675
|
results.push(await checkTreeshakeSetup(targetDir, pkg));
|
|
676
|
+
// Lockfile-driven demotion: if the lock records an intentional opt-out for a
|
|
677
|
+
// check that's currently optional-missing, demote it to ok with a clear detail.
|
|
678
|
+
if (lock) {
|
|
679
|
+
return results.map((r) => {
|
|
680
|
+
if (r.status !== 'optional-missing')
|
|
681
|
+
return r;
|
|
682
|
+
if (!declinedInLock(lock, r.check))
|
|
683
|
+
return r;
|
|
684
|
+
return {
|
|
685
|
+
check: r.check,
|
|
686
|
+
status: 'ok',
|
|
687
|
+
detail: 'intentionally declined (.js-tooling.json)',
|
|
688
|
+
};
|
|
689
|
+
});
|
|
690
|
+
}
|
|
650
691
|
return results;
|
|
651
692
|
}
|
|
652
693
|
const STATUS_ICONS = {
|
|
@@ -20,7 +20,101 @@ export const FIX_TARGETS = {
|
|
|
20
20
|
'GitHub Actions': 'github-actions',
|
|
21
21
|
Dependabot: 'dependabot',
|
|
22
22
|
CodeQL: 'codeql',
|
|
23
|
+
lockfile: 'lockfile',
|
|
24
|
+
'.js-tooling.json': 'lockfile',
|
|
23
25
|
};
|
|
24
26
|
export function getFixTargetForCheck(checkName) {
|
|
25
27
|
return FIX_TARGETS[checkName] ?? null;
|
|
26
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* For a given doctor check name, returns true when the lockfile records that
|
|
31
|
+
* the user intentionally opted out of the tool that check covers. Used by
|
|
32
|
+
* doctor to demote `optional-missing` to `ok` and by fix to print a conflict
|
|
33
|
+
* warning before overriding the recorded choice.
|
|
34
|
+
*/
|
|
35
|
+
export function declinedInLock(lock, checkName) {
|
|
36
|
+
if (!lock)
|
|
37
|
+
return false;
|
|
38
|
+
const c = lock.config;
|
|
39
|
+
switch (checkName) {
|
|
40
|
+
case 'TypeScript':
|
|
41
|
+
return c.typescript?.enabled === false;
|
|
42
|
+
case 'Biome':
|
|
43
|
+
return c.linting?.tool !== 'biome' && c.linting?.tool !== 'both';
|
|
44
|
+
case 'ESLint':
|
|
45
|
+
return c.linting?.tool !== 'eslint' && c.linting?.tool !== 'both';
|
|
46
|
+
case 'Prettier':
|
|
47
|
+
return c.formatting?.tool !== 'prettier';
|
|
48
|
+
case 'Vitest':
|
|
49
|
+
return c.testing?.framework !== 'vitest';
|
|
50
|
+
case 'Commitlint':
|
|
51
|
+
return c.commitLint === false;
|
|
52
|
+
case 'Husky':
|
|
53
|
+
case 'lint-staged':
|
|
54
|
+
case 'Husky pre-push':
|
|
55
|
+
return c.gitHooks === false;
|
|
56
|
+
case 'verify script':
|
|
57
|
+
// Verify is derived from other tools; only "declined" if none of typecheck/lint/test are enabled.
|
|
58
|
+
return (c.typescript?.enabled === false &&
|
|
59
|
+
c.linting?.tool === 'none' &&
|
|
60
|
+
c.testing?.framework === 'none');
|
|
61
|
+
case 'semantic-release':
|
|
62
|
+
return c.semanticRelease === false;
|
|
63
|
+
case 'Dependabot':
|
|
64
|
+
case 'CodeQL':
|
|
65
|
+
return c.securityAutomation === false;
|
|
66
|
+
default:
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* When a fixer is about to scaffold a tool, return the patch to apply to the
|
|
72
|
+
* lockfile's recorded choices so intent stays in sync with reality. Returns
|
|
73
|
+
* null when the target either doesn't change any recorded choice (e.g. the
|
|
74
|
+
* `verify` fixer is derived, or `engines` writes a universal field) or when
|
|
75
|
+
* the lockfile already reflects the change.
|
|
76
|
+
*/
|
|
77
|
+
export function lockfilePatchForTarget(target, lock) {
|
|
78
|
+
const c = lock.config;
|
|
79
|
+
switch (target) {
|
|
80
|
+
case 'biome':
|
|
81
|
+
if (c.linting.tool === 'biome' || c.linting.tool === 'both')
|
|
82
|
+
return null;
|
|
83
|
+
return {
|
|
84
|
+
linting: { tool: 'biome' },
|
|
85
|
+
formatting: { tool: 'biome' },
|
|
86
|
+
};
|
|
87
|
+
case 'eslint':
|
|
88
|
+
if (c.linting.tool === 'eslint' || c.linting.tool === 'both')
|
|
89
|
+
return null;
|
|
90
|
+
return {
|
|
91
|
+
linting: { tool: 'eslint', eslintConfig: c.linting.eslintConfig ?? 'base' },
|
|
92
|
+
formatting: { tool: 'prettier' },
|
|
93
|
+
};
|
|
94
|
+
case 'prettier':
|
|
95
|
+
if (c.formatting.tool === 'prettier')
|
|
96
|
+
return null;
|
|
97
|
+
return { formatting: { tool: 'prettier' } };
|
|
98
|
+
case 'vitest':
|
|
99
|
+
if (c.testing.framework === 'vitest')
|
|
100
|
+
return null;
|
|
101
|
+
return {
|
|
102
|
+
testing: { framework: 'vitest', environment: c.testing.environment ?? 'node' },
|
|
103
|
+
};
|
|
104
|
+
case 'commitlint':
|
|
105
|
+
return c.commitLint ? null : { commitLint: true };
|
|
106
|
+
case 'husky':
|
|
107
|
+
return c.gitHooks ? null : { gitHooks: true };
|
|
108
|
+
case 'semantic-release':
|
|
109
|
+
return c.semanticRelease ? null : { semanticRelease: true };
|
|
110
|
+
case 'dependabot':
|
|
111
|
+
case 'codeql':
|
|
112
|
+
return c.securityAutomation ? null : { securityAutomation: true };
|
|
113
|
+
case 'tsconfig':
|
|
114
|
+
return c.typescript.enabled ? null : { typescript: { enabled: true, config: 'base' } };
|
|
115
|
+
case 'treeshake-check':
|
|
116
|
+
return c.treeshakeCheck ? null : { treeshakeCheck: true };
|
|
117
|
+
default:
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
}
|
package/dist/cli/commands/fix.js
CHANGED
|
@@ -8,11 +8,13 @@ import { generateGitHubActions } from '../generators/github-actions.js';
|
|
|
8
8
|
import { generateESLintConfig, generatePrettierConfig } from '../generators/linting.js';
|
|
9
9
|
import { ensureEnginesNode, generateEditorConfig, generateKnipConfig, generateNvmrc, generateSizeLimitConfig, } from '../generators/misc.js';
|
|
10
10
|
import { composeVerifyScriptFromPkg } from '../generators/package-json.js';
|
|
11
|
-
import { generateTreeshakeCheck, inferSubpathsFromExports } from '../generators/treeshake.js';
|
|
12
11
|
import { generateCodeQLWorkflow, generateDependabotConfig } from '../generators/security.js';
|
|
13
12
|
import { generateVitestConfig } from '../generators/testing.js';
|
|
13
|
+
import { generateTreeshakeCheck, inferSubpathsFromExports } from '../generators/treeshake.js';
|
|
14
14
|
import { copyPreset } from '../utils/copy-preset.js';
|
|
15
|
+
import { LOCKFILE_NAME, readLockfile, updateLockfileConfig, writeLockfile, } from '../utils/lockfile.js';
|
|
15
16
|
import { runDoctor } from './doctor.js';
|
|
17
|
+
import { declinedInLock, lockfilePatchForTarget } from './fix-targets.js';
|
|
16
18
|
function inferProjectConfig(pkg) {
|
|
17
19
|
const deps = {
|
|
18
20
|
...(pkg?.dependencies ?? {}),
|
|
@@ -344,6 +346,23 @@ const FIXERS = [
|
|
|
344
346
|
return { filesWritten: ['package.json'] };
|
|
345
347
|
},
|
|
346
348
|
},
|
|
349
|
+
{
|
|
350
|
+
target: 'lockfile',
|
|
351
|
+
description: `Scaffold ${LOCKFILE_NAME} recording current tool choices`,
|
|
352
|
+
appliesTo: ['lockfile'],
|
|
353
|
+
outputs: [LOCKFILE_NAME],
|
|
354
|
+
riskLevel: 'safe-add',
|
|
355
|
+
canFixDrift: false,
|
|
356
|
+
async run({ targetDir, pkg }) {
|
|
357
|
+
if (!pkg) {
|
|
358
|
+
console.log(chalk.yellow(' no package.json found — skipping'));
|
|
359
|
+
return { filesWritten: [] };
|
|
360
|
+
}
|
|
361
|
+
const config = inferProjectConfig(pkg);
|
|
362
|
+
await writeLockfile(targetDir, config);
|
|
363
|
+
return { filesWritten: [LOCKFILE_NAME] };
|
|
364
|
+
},
|
|
365
|
+
},
|
|
347
366
|
];
|
|
348
367
|
export function getFixers() {
|
|
349
368
|
return FIXERS;
|
|
@@ -361,17 +380,27 @@ function logTargets() {
|
|
|
361
380
|
console.log(` ${chalk.green('●')} ${chalk.bold(f.target)}: ${chalk.gray(f.description)}`);
|
|
362
381
|
}
|
|
363
382
|
}
|
|
364
|
-
async function applyFixer(fixer, result, targetDir, pkg, dryRun, silent) {
|
|
383
|
+
async function applyFixer(fixer, result, targetDir, pkg, lock, dryRun, silent) {
|
|
365
384
|
if (dryRun) {
|
|
366
385
|
if (!silent) {
|
|
367
386
|
console.log(chalk.cyan(` [dry-run] would write: ${fixer.outputs.join(', ')}`));
|
|
368
387
|
}
|
|
369
388
|
return { filesWritten: [], dryRun: true };
|
|
370
389
|
}
|
|
371
|
-
const { filesWritten } = await fixer.run({ targetDir, pkg, result });
|
|
390
|
+
const { filesWritten } = await fixer.run({ targetDir, pkg, result, lock });
|
|
372
391
|
if (!silent && filesWritten.length > 0) {
|
|
373
392
|
console.log(chalk.green(` ✅ wrote ${filesWritten.join(', ')}`));
|
|
374
393
|
}
|
|
394
|
+
// Auto-resync the lockfile when a fix changes a recorded choice.
|
|
395
|
+
if (lock && fixer.target !== 'lockfile') {
|
|
396
|
+
const patch = lockfilePatchForTarget(fixer.target, lock);
|
|
397
|
+
if (patch) {
|
|
398
|
+
const ok = await updateLockfileConfig(targetDir, patch);
|
|
399
|
+
if (ok && !silent) {
|
|
400
|
+
console.log(chalk.dim(` ↻ ${LOCKFILE_NAME} updated to reflect the new choice`));
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
375
404
|
return { filesWritten, dryRun: false };
|
|
376
405
|
}
|
|
377
406
|
function promptMessageFor(fixer, result) {
|
|
@@ -400,8 +429,11 @@ async function confirmApply(fixer, result, assumeYes) {
|
|
|
400
429
|
]);
|
|
401
430
|
return confirm === true;
|
|
402
431
|
}
|
|
403
|
-
function recordFor(target, check, doctorStatus, status, filesWritten) {
|
|
404
|
-
|
|
432
|
+
function recordFor(target, check, doctorStatus, status, filesWritten, lockfileConflict = false) {
|
|
433
|
+
const base = { target, check, status, doctorStatus, filesWritten };
|
|
434
|
+
if (lockfileConflict)
|
|
435
|
+
base.lockfileConflict = true;
|
|
436
|
+
return base;
|
|
405
437
|
}
|
|
406
438
|
export async function fixCommand(target, options = {}) {
|
|
407
439
|
const targetDir = path.resolve(options.directory ?? process.cwd());
|
|
@@ -411,8 +443,18 @@ export async function fixCommand(target, options = {}) {
|
|
|
411
443
|
const assumeYes = options.yes === true || json;
|
|
412
444
|
const silent = json;
|
|
413
445
|
const pkg = await readPackageJson(targetDir);
|
|
446
|
+
const lock = await readLockfile(targetDir);
|
|
414
447
|
const results = await runDoctor(targetDir);
|
|
415
448
|
const actions = [];
|
|
449
|
+
const noteLockConflict = (check) => {
|
|
450
|
+
if (!lock)
|
|
451
|
+
return false;
|
|
452
|
+
const conflict = declinedInLock(lock, check);
|
|
453
|
+
if (conflict && !silent) {
|
|
454
|
+
console.log(chalk.yellow(` ⚠ ${LOCKFILE_NAME} says this tool was declined — applying anyway will update the lockfile to reflect the new choice.`));
|
|
455
|
+
}
|
|
456
|
+
return conflict;
|
|
457
|
+
};
|
|
416
458
|
const emitJson = (resolvedTarget) => {
|
|
417
459
|
const payload = { directory: targetDir, target: resolvedTarget, actions };
|
|
418
460
|
console.log(JSON.stringify(payload, null, 2));
|
|
@@ -436,7 +478,12 @@ export async function fixCommand(target, options = {}) {
|
|
|
436
478
|
}
|
|
437
479
|
const result = results.find((r) => fixer.appliesTo.includes(r.check)) ??
|
|
438
480
|
{ check: fixer.appliesTo[0] ?? fixer.target, status: 'missing', detail: '' };
|
|
439
|
-
|
|
481
|
+
// A check that's `ok` because the lockfile records an opt-out should still be
|
|
482
|
+
// fixable when the user explicitly targets it — treat it as optional-missing
|
|
483
|
+
// so the override + lockfile resync paths run.
|
|
484
|
+
const lockfileDemoted = lock !== null && declinedInLock(lock, result.check);
|
|
485
|
+
const effectiveResult = result.status === 'ok' && lockfileDemoted ? { ...result, status: 'optional-missing' } : result;
|
|
486
|
+
if (effectiveResult.status === 'ok') {
|
|
440
487
|
actions.push(recordFor(fixer.target, result.check, 'ok', 'already-ok', []));
|
|
441
488
|
if (json)
|
|
442
489
|
return emitJson(fixer.target);
|
|
@@ -444,18 +491,19 @@ export async function fixCommand(target, options = {}) {
|
|
|
444
491
|
return;
|
|
445
492
|
}
|
|
446
493
|
if (!silent) {
|
|
447
|
-
console.log(chalk.cyan(`\n🔧 ${fixer.target} — ${chalk.bold(result.check)} is ${
|
|
494
|
+
console.log(chalk.cyan(`\n🔧 ${fixer.target} — ${chalk.bold(result.check)} is ${effectiveResult.status}\n`));
|
|
448
495
|
}
|
|
449
|
-
const
|
|
496
|
+
const conflict = noteLockConflict(result.check);
|
|
497
|
+
const ok = await confirmApply(fixer, effectiveResult, assumeYes);
|
|
450
498
|
if (!ok) {
|
|
451
|
-
actions.push(recordFor(fixer.target, result.check,
|
|
499
|
+
actions.push(recordFor(fixer.target, result.check, effectiveResult.status, 'skipped', [], conflict));
|
|
452
500
|
if (json)
|
|
453
501
|
return emitJson(fixer.target);
|
|
454
502
|
console.log(chalk.gray(' skipped\n'));
|
|
455
503
|
return;
|
|
456
504
|
}
|
|
457
|
-
const outcome = await applyFixer(fixer,
|
|
458
|
-
actions.push(recordFor(fixer.target, result.check,
|
|
505
|
+
const outcome = await applyFixer(fixer, effectiveResult, targetDir, pkg, lock, dryRun, silent);
|
|
506
|
+
actions.push(recordFor(fixer.target, result.check, effectiveResult.status, outcome.dryRun ? 'dry-run' : 'applied', outcome.filesWritten, conflict));
|
|
459
507
|
if (json)
|
|
460
508
|
return emitJson(fixer.target);
|
|
461
509
|
console.log();
|
|
@@ -486,16 +534,17 @@ export async function fixCommand(target, options = {}) {
|
|
|
486
534
|
if (!silent) {
|
|
487
535
|
console.log(` ${chalk.bold(result.check)} (${result.status}) → ${fixer.target}`);
|
|
488
536
|
}
|
|
537
|
+
const conflict = noteLockConflict(result.check);
|
|
489
538
|
const ok = await confirmApply(fixer, result, assumeYes);
|
|
490
539
|
if (!ok) {
|
|
491
|
-
actions.push(recordFor(fixer.target, result.check, result.status, 'skipped', []));
|
|
540
|
+
actions.push(recordFor(fixer.target, result.check, result.status, 'skipped', [], conflict));
|
|
492
541
|
if (!silent)
|
|
493
542
|
console.log(chalk.gray(' skipped'));
|
|
494
543
|
skippedCount++;
|
|
495
544
|
continue;
|
|
496
545
|
}
|
|
497
|
-
const outcome = await applyFixer(fixer, result, targetDir, pkg, dryRun, silent);
|
|
498
|
-
actions.push(recordFor(fixer.target, result.check, result.status, outcome.dryRun ? 'dry-run' : 'applied', outcome.filesWritten));
|
|
546
|
+
const outcome = await applyFixer(fixer, result, targetDir, pkg, lock, dryRun, silent);
|
|
547
|
+
actions.push(recordFor(fixer.target, result.check, result.status, outcome.dryRun ? 'dry-run' : 'applied', outcome.filesWritten, conflict));
|
|
499
548
|
appliedCount++;
|
|
500
549
|
}
|
|
501
550
|
if (json)
|
|
@@ -151,7 +151,7 @@ export function validateProjectConfig(input) {
|
|
|
151
151
|
return { valid: errors.length === 0, errors };
|
|
152
152
|
}
|
|
153
153
|
export function computeFileList(config) {
|
|
154
|
-
const files = ['package.json'];
|
|
154
|
+
const files = ['package.json', '.js-tooling.json'];
|
|
155
155
|
files.push('.editorconfig', '.nvmrc', 'knip.json');
|
|
156
156
|
if (config.typescript.enabled) {
|
|
157
157
|
files.push('tsconfig.json', 'reset.d.ts');
|
|
@@ -4,6 +4,7 @@ import fs from 'fs-extra';
|
|
|
4
4
|
import inquirer from 'inquirer';
|
|
5
5
|
import { generateConfigs } from '../generators/index.js';
|
|
6
6
|
import { installDependencies } from '../utils/install.js';
|
|
7
|
+
import { LOCKFILE_NAME, writeLockfile } from '../utils/lockfile.js';
|
|
7
8
|
import { buildPresetConfig, computeFileList, CONFIG_SCHEMA, PRESET_NAMES, validateProjectConfig, } from './setup-presets.js';
|
|
8
9
|
async function resolveConfig(options) {
|
|
9
10
|
if (options.config && options.preset) {
|
|
@@ -55,6 +56,7 @@ export async function setupProject(options) {
|
|
|
55
56
|
}
|
|
56
57
|
console.log(chalk.cyan('\n📝 Generating configuration files...\n'));
|
|
57
58
|
await generateConfigs(config, targetDir);
|
|
59
|
+
await writeLockfile(targetDir, config);
|
|
58
60
|
if (!options.skipInstall) {
|
|
59
61
|
console.log(chalk.cyan('\n📦 Installing dependencies...\n'));
|
|
60
62
|
await installDependencies(config, targetDir);
|
|
@@ -264,6 +266,7 @@ function showNextSteps(config, _targetDir) {
|
|
|
264
266
|
if (config.gitHooks) {
|
|
265
267
|
steps.push('🪝 Commit your changes to test the git hooks');
|
|
266
268
|
}
|
|
269
|
+
steps.push(`🔒 ${LOCKFILE_NAME} records your setup choices — doctor uses it to suppress intentional opt-outs`);
|
|
267
270
|
steps.push('📖 Check the generated README.md for more details');
|
|
268
271
|
steps.forEach((step, index) => {
|
|
269
272
|
console.log(` ${index + 1}. ${step}`);
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import packageJson from '../../../package.json' with { type: 'json' };
|
|
4
|
+
import { validateProjectConfig } from '../commands/setup-presets.js';
|
|
5
|
+
export const LOCKFILE_NAME = '.js-tooling.json';
|
|
6
|
+
export const LOCKFILE_VERSION = 1;
|
|
7
|
+
const LOCKFILE_SCHEMA_URL = 'https://rtorcato.github.io/js-tooling/schemas/lockfile.json';
|
|
8
|
+
export async function readLockfile(dir) {
|
|
9
|
+
const filepath = path.join(dir, LOCKFILE_NAME);
|
|
10
|
+
if (!(await fs.pathExists(filepath)))
|
|
11
|
+
return null;
|
|
12
|
+
try {
|
|
13
|
+
const raw = (await fs.readJson(filepath));
|
|
14
|
+
if (typeof raw !== 'object' || raw === null)
|
|
15
|
+
return null;
|
|
16
|
+
const obj = raw;
|
|
17
|
+
if (typeof obj.version !== 'number')
|
|
18
|
+
return null;
|
|
19
|
+
if (typeof obj.config !== 'object' || obj.config === null)
|
|
20
|
+
return null;
|
|
21
|
+
return obj;
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export async function writeLockfile(dir, config) {
|
|
28
|
+
const { valid, errors } = validateProjectConfig(config);
|
|
29
|
+
if (!valid) {
|
|
30
|
+
throw new Error(`Refusing to write invalid lockfile:\n - ${errors.join('\n - ')}`);
|
|
31
|
+
}
|
|
32
|
+
const filepath = path.join(dir, LOCKFILE_NAME);
|
|
33
|
+
const lockfile = {
|
|
34
|
+
$schema: LOCKFILE_SCHEMA_URL,
|
|
35
|
+
version: LOCKFILE_VERSION,
|
|
36
|
+
config,
|
|
37
|
+
writtenBy: `@rtorcato/js-tooling@${packageJson.version}`,
|
|
38
|
+
writtenAt: new Date().toISOString(),
|
|
39
|
+
};
|
|
40
|
+
await fs.writeJson(filepath, lockfile, { spaces: 2 });
|
|
41
|
+
return filepath;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Patch a subset of a lockfile's config in place, preserving everything else.
|
|
45
|
+
* Returns true when the file was rewritten, false when no lockfile exists.
|
|
46
|
+
*/
|
|
47
|
+
export async function updateLockfileConfig(dir, patch) {
|
|
48
|
+
const existing = await readLockfile(dir);
|
|
49
|
+
if (!existing)
|
|
50
|
+
return false;
|
|
51
|
+
const merged = { ...existing.config, ...patch };
|
|
52
|
+
await writeLockfile(dir, merged);
|
|
53
|
+
return true;
|
|
54
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rtorcato/js-tooling",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.11.0",
|
|
4
4
|
"description": "JavaScript and TypeScript tooling for Node.js, React, Next.js, and Vitest.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -188,7 +188,7 @@
|
|
|
188
188
|
"commitizen": "^4.3.1",
|
|
189
189
|
"conventional-changelog-conventionalcommits": "^9.3.1",
|
|
190
190
|
"cz-conventional-changelog": "^3.3.0",
|
|
191
|
-
"esbuild": "^0.
|
|
191
|
+
"esbuild": "^0.28.0",
|
|
192
192
|
"esbuild-node-externals": "^1.22.0",
|
|
193
193
|
"eslint": "9.38.0",
|
|
194
194
|
"eslint-plugin-import": "^2.32.0",
|