@terra-symposium/standards-quality 0.5.0 → 0.7.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 +2 -0
- package/dist/files.d.ts.map +1 -1
- package/dist/files.js +8 -3
- package/dist/files.js.map +1 -1
- package/dist/naming.d.ts +2 -2
- package/dist/naming.d.ts.map +1 -1
- package/dist/naming.js +39 -8
- package/dist/naming.js.map +1 -1
- package/dist/patterns.d.ts.map +1 -1
- package/dist/patterns.js +17 -11
- package/dist/patterns.js.map +1 -1
- package/dist/runner.d.ts +9 -7
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +127 -46
- package/dist/runner.js.map +1 -1
- package/package.json +1 -1
- package/quality.schema.json +4 -2
package/dist/runner.js
CHANGED
|
@@ -10,16 +10,16 @@ IMPORTS
|
|
|
10
10
|
import ts from 'typescript';
|
|
11
11
|
import { aliasFindings, declarationScope, isCallbackDeclaration, isProtectedPath, isRenamableDeclaration, namingFindings, renameDeclaration, renameId } from './naming.js';
|
|
12
12
|
import { basename, dirname, isAbsolute, relative, resolve, sep } from 'node:path';
|
|
13
|
+
import { buildGitHubReport, formatGitHubReport, hasGitHubAuth } from './github-report.js';
|
|
13
14
|
import { checkImports } from './imports.js';
|
|
14
15
|
import { collapseInlineIfs } from './inline-if.js';
|
|
15
16
|
import { collapseTypesAndObjects } from './collapse.js';
|
|
16
|
-
import { existsSync, readFileSync, realpathSync, statSync, writeFileSync } from 'node:fs';
|
|
17
17
|
import { deadCodeEditsIntroduceTypeErrors, deadCodeTypeProgram, fixSingleUseDeclarations, singleUseFindings } from './single-use.js';
|
|
18
18
|
import { documentSrcFile, documentationFindings, duplicateImportFindings, maintainSrcFile, protectLongImports } from './documentation.js';
|
|
19
|
+
import { existsSync, readFileSync, realpathSync, statSync, writeFileSync } from 'node:fs';
|
|
19
20
|
import { fileURLToPath } from 'node:url';
|
|
20
|
-
import { buildGitHubReport, formatGitHubReport, hasGitHubAuth } from './github-report.js';
|
|
21
21
|
import { fixObjectForwarding, objectForwardingFindings } from './object-forwarding.js';
|
|
22
|
-
import { fixRegexDedup, fixRepeatedClasses, regexFindings, repeatedClassFindings, uiPatternFindings } from './patterns.js';
|
|
22
|
+
import { fixRegexDedup, fixRepeatedClasses, regexFindings, repeatedClassFindings, runTailwindCanonical, uiPatternFindings } from './patterns.js';
|
|
23
23
|
import { qualityProgress } from './progress.js';
|
|
24
24
|
import { securityFindings } from './security.js';
|
|
25
25
|
import { spawnSync } from 'node:child_process';
|
|
@@ -53,6 +53,7 @@ const CLI_CMDS = new Set([
|
|
|
53
53
|
'dead-code',
|
|
54
54
|
'security',
|
|
55
55
|
'repeated-classes',
|
|
56
|
+
'tailwind-canonical',
|
|
56
57
|
'regex-dedup',
|
|
57
58
|
'ui-patterns',
|
|
58
59
|
'imports',
|
|
@@ -190,6 +191,7 @@ export function createQualityCfg(cfg) {
|
|
|
190
191
|
const defaultSkipped = ['.git', 'node_modules', 'dist', 'build', 'coverage', 'test-results', '.next', '.astro', '.sanity', '.netlify', '.cache', '.output', '.vercel', '.nuxt', '.husky'];
|
|
191
192
|
return {
|
|
192
193
|
...cfg,
|
|
194
|
+
manualWarnings: cfg.manualWarnings ?? 'error',
|
|
193
195
|
renameExports: cfg.renameExports ?? false,
|
|
194
196
|
root: resolve(cfg.root),
|
|
195
197
|
sourceRoots: cfg.sourceRoots.map((p) => resolve(cfg.root, p)),
|
|
@@ -564,12 +566,10 @@ function focusedEslintCfgSrc(cfg, flatCfg, files) {
|
|
|
564
566
|
return `
|
|
565
567
|
import { createRequire } from 'node:module';
|
|
566
568
|
import { resolve } from 'node:path';
|
|
567
|
-
|
|
568
569
|
const root = ${JSON.stringify(cfg.root)};
|
|
569
570
|
const configPath = ${JSON.stringify(flatCfg)};
|
|
570
571
|
const sourceFiles = ${JSON.stringify(files)};
|
|
571
572
|
const filePatterns = ${JSON.stringify(patterns)};
|
|
572
|
-
|
|
573
573
|
try {
|
|
574
574
|
const requireFromRepo = createRequire(resolve(root, 'package.json'));
|
|
575
575
|
const { ESLint } = requireFromRepo('eslint');
|
|
@@ -1040,10 +1040,13 @@ function assertKnipPath(cfg, path, label) {
|
|
|
1040
1040
|
throw new Error(`Knip returned a ${label} symlink outside the repository: ${path}`);
|
|
1041
1041
|
return absolute;
|
|
1042
1042
|
}
|
|
1043
|
-
/**
|
|
1043
|
+
/**
|
|
1044
|
+
* Converts Knip's validated issue inventory into dead-code review findings.
|
|
1045
|
+
*
|
|
1044
1046
|
* @param cfg Quality configuration defining scope and explicit entry-point contracts.
|
|
1045
1047
|
* @param importedConsumers Export names referenced by configured source files.
|
|
1046
|
-
*
|
|
1048
|
+
*
|
|
1049
|
+
* @return Knip findings with safe export-modifier changes marked autofixable.
|
|
1047
1050
|
*/
|
|
1048
1051
|
function collectKnipFindings(cfg, importedConsumers = importedExportConsumers(cfg)) {
|
|
1049
1052
|
const issues = readKnipIssues(cfg);
|
|
@@ -1078,11 +1081,14 @@ function collectKnipFindings(cfg, importedConsumers = importedExportConsumers(cf
|
|
|
1078
1081
|
}
|
|
1079
1082
|
return findings;
|
|
1080
1083
|
}
|
|
1081
|
-
/**
|
|
1084
|
+
/**
|
|
1085
|
+
* Checks whether a name is a configured framework-discovered route export.
|
|
1086
|
+
*
|
|
1082
1087
|
* @param cfg Quality configuration containing framework entry exports.
|
|
1083
1088
|
* @param file Repository-relative source filename reported by Knip.
|
|
1084
1089
|
* @param name Exported identifier to check.
|
|
1085
|
-
*
|
|
1090
|
+
*
|
|
1091
|
+
* @return Whether the configured file or supported TypeScript route pattern includes the export.
|
|
1086
1092
|
*/
|
|
1087
1093
|
function isFrameworkEntryExport(cfg, file, name) {
|
|
1088
1094
|
const publicRoute = !basename(file).startsWith('_');
|
|
@@ -1093,9 +1099,12 @@ function isFrameworkEntryExport(cfg, file, name) {
|
|
|
1093
1099
|
return pathMatches && names.includes(name);
|
|
1094
1100
|
});
|
|
1095
1101
|
}
|
|
1096
|
-
/**
|
|
1102
|
+
/**
|
|
1103
|
+
* Selects the TypeScript parser mode from a source filename's extension.
|
|
1104
|
+
*
|
|
1097
1105
|
* @param file Source filename to classify.
|
|
1098
|
-
*
|
|
1106
|
+
*
|
|
1107
|
+
* @return The JavaScript, JSX, TypeScript, or TSX compiler script kind.
|
|
1099
1108
|
*/
|
|
1100
1109
|
function deadCodeScriptKind(file) {
|
|
1101
1110
|
if (/\.tsx$/i.test(file))
|
|
@@ -1106,10 +1115,13 @@ function deadCodeScriptKind(file) {
|
|
|
1106
1115
|
return ts.ScriptKind.JS;
|
|
1107
1116
|
return ts.ScriptKind.TS;
|
|
1108
1117
|
}
|
|
1109
|
-
/**
|
|
1118
|
+
/**
|
|
1119
|
+
* Checks whether Knip identified a direct named export that can become private.
|
|
1120
|
+
*
|
|
1110
1121
|
* @param file Absolute source filename containing the export.
|
|
1111
1122
|
* @param name Exported identifier reported by Knip.
|
|
1112
|
-
*
|
|
1123
|
+
*
|
|
1124
|
+
* @return Whether the export modifier can be removed without an ambiguous rewrite.
|
|
1113
1125
|
*/
|
|
1114
1126
|
function knipExportCanBeMadePrivate(file, name) {
|
|
1115
1127
|
if (!existsSync(file) || file.endsWith('.astro'))
|
|
@@ -1222,9 +1234,12 @@ function fixKnipExports(cfg, changedFiles) {
|
|
|
1222
1234
|
}
|
|
1223
1235
|
/** Finds export names that configured source files import and actually reference. */
|
|
1224
1236
|
const importedExportConsumerCache = new WeakMap();
|
|
1225
|
-
/**
|
|
1237
|
+
/**
|
|
1238
|
+
* Builds a cache of exported names referenced by configured source consumers.
|
|
1239
|
+
*
|
|
1226
1240
|
* @param cfg Quality configuration defining roots, aliases, and skipped paths.
|
|
1227
|
-
*
|
|
1241
|
+
*
|
|
1242
|
+
* @return Export names grouped by their resolved absolute source file.
|
|
1228
1243
|
*/
|
|
1229
1244
|
function importedExportConsumers(cfg) {
|
|
1230
1245
|
const srcFiles = filesFor(cfg, true).filter((path) => C_M_JS_TS_X_CLASS_2.test(path) || path.endsWith('.astro'));
|
|
@@ -1309,12 +1324,15 @@ function importedExportConsumers(cfg) {
|
|
|
1309
1324
|
importedExportConsumerCache.set(cfg, { signature, consumers });
|
|
1310
1325
|
return consumers;
|
|
1311
1326
|
}
|
|
1312
|
-
/**
|
|
1327
|
+
/**
|
|
1328
|
+
* Accepts proposed edits only when they add no TypeScript errors to changed files.
|
|
1329
|
+
*
|
|
1313
1330
|
* @param program TypeScript program representing the original source.
|
|
1314
1331
|
* @param proposed Proposed source text keyed by absolute filename.
|
|
1315
1332
|
* @param original Original source text for the same files.
|
|
1316
1333
|
* @param onBatch Optional progress callback for each validation batch.
|
|
1317
|
-
*
|
|
1334
|
+
*
|
|
1335
|
+
* @return Accepted source edits and filenames rejected by the type-safety check.
|
|
1318
1336
|
*/
|
|
1319
1337
|
function acceptTypeSafeEdits(program, proposed, original, onBatch) {
|
|
1320
1338
|
const rejected = new Set();
|
|
@@ -1342,11 +1360,14 @@ function acceptTypeSafeEdits(program, proposed, original, onBatch) {
|
|
|
1342
1360
|
rejected,
|
|
1343
1361
|
};
|
|
1344
1362
|
}
|
|
1345
|
-
/**
|
|
1363
|
+
/**
|
|
1364
|
+
* Prints dead-code findings and fails when errors or blocking manual findings remain.
|
|
1365
|
+
*
|
|
1346
1366
|
* @param findings Findings collected by the dead-code coordinator.
|
|
1367
|
+
* @param manualWarnings Policy for manual-only findings.
|
|
1347
1368
|
* @param typeSafetyVerified Whether the candidate autofixes passed TypeScript safety checks.
|
|
1348
1369
|
*/
|
|
1349
|
-
function printDeadCodeFindings(findings, typeSafetyVerified = true) {
|
|
1370
|
+
function printDeadCodeFindings(findings, manualWarnings, typeSafetyVerified = true) {
|
|
1350
1371
|
if (reportStages) {
|
|
1351
1372
|
const stage = reportStages.get('dead-code');
|
|
1352
1373
|
if (stage) {
|
|
@@ -1359,16 +1380,20 @@ function printDeadCodeFindings(findings, typeSafetyVerified = true) {
|
|
|
1359
1380
|
console.error(' ✓ dead-code checks passed; no autofixable findings or manual warnings remain');
|
|
1360
1381
|
return;
|
|
1361
1382
|
}
|
|
1362
|
-
const errs = findings.filter((finding) => finding.severity === 'error');
|
|
1363
|
-
const warnings = findings.filter((finding) => finding.severity === 'warning');
|
|
1383
|
+
const errs = findings.filter((finding) => finding.severity === 'error' || manualWarnings === 'error');
|
|
1384
|
+
const warnings = findings.filter((finding) => finding.severity === 'warning' && manualWarnings !== 'error');
|
|
1385
|
+
const manualCount = findings.filter((finding) => finding.severity === 'warning').length;
|
|
1364
1386
|
for (const finding of findings) {
|
|
1365
|
-
|
|
1387
|
+
const severity = finding.severity === 'warning' && manualWarnings === 'error' ? 'ERROR' : finding.severity.toUpperCase();
|
|
1388
|
+
console.error(` - [${severity}] ${finding.rule}: ${finding.file}:${finding.line} ${finding.name}`);
|
|
1366
1389
|
console.error(` Reason: ${finding.reason}`);
|
|
1367
1390
|
const autofix = !finding.autofixable ? 'not safe or not supported' : typeSafetyVerified ? 'available' : 'candidate; fix mode checks TypeScript before writing';
|
|
1368
1391
|
console.error(` Autofix: ${autofix}`);
|
|
1369
1392
|
console.error(` Agent action: ${finding.agentAction}`);
|
|
1370
1393
|
}
|
|
1371
|
-
console.error(` ${errs.length} error${errs.length === 1 ? '' : 's'}; ${warnings.length}
|
|
1394
|
+
console.error(` ${errs.length} error${errs.length === 1 ? '' : 's'}; ${manualCount} manual finding${manualCount === 1 ? '' : 's'}${manualWarnings === 'warning' ? ` (${warnings.length} warning${warnings.length === 1 ? '' : 's'})` : ''}.`);
|
|
1395
|
+
if (manualWarnings === 'error' && manualCount > 0)
|
|
1396
|
+
console.error(' Manual findings block quality by default. Fix each finding one by one, rerun quality, and continue only when all are resolved before committing.');
|
|
1372
1397
|
if (errs.length)
|
|
1373
1398
|
process.exitCode = 1;
|
|
1374
1399
|
}
|
|
@@ -1527,17 +1552,21 @@ function runDeadCode(cfg, fix, all, verify = true, emit = true) {
|
|
|
1527
1552
|
}
|
|
1528
1553
|
: finding);
|
|
1529
1554
|
if (emit)
|
|
1530
|
-
printDeadCodeFindings(finalFindings, fix);
|
|
1555
|
+
printDeadCodeFindings(finalFindings, cfg.manualWarnings ?? 'error', fix);
|
|
1531
1556
|
return finalFindings;
|
|
1532
1557
|
}
|
|
1533
|
-
/**
|
|
1558
|
+
/**
|
|
1559
|
+
* Runs dead-code detectors once without modifying the source tree.
|
|
1560
|
+
*
|
|
1534
1561
|
* @param cfg Quality configuration defining source scope and tools.
|
|
1535
1562
|
* @param all Whether to inspect all configured source files.
|
|
1536
1563
|
*/
|
|
1537
1564
|
function runDeadCodeReadOnly(cfg, all) {
|
|
1538
1565
|
runDeadCode(cfg, false, all);
|
|
1539
1566
|
}
|
|
1540
|
-
/**
|
|
1567
|
+
/**
|
|
1568
|
+
* Dispatches the dead-code coordinator in fix or read-only mode.
|
|
1569
|
+
*
|
|
1541
1570
|
* @param cfg Quality configuration defining source scope and tools.
|
|
1542
1571
|
* @param fix Whether eligible changes may be applied to source files.
|
|
1543
1572
|
* @param all Whether to inspect all configured source files.
|
|
@@ -1588,6 +1617,7 @@ function printFindings(findings, fmt) {
|
|
|
1588
1617
|
}
|
|
1589
1618
|
/**
|
|
1590
1619
|
* Run the optional, read-only GitHub analytics stage.
|
|
1620
|
+
*
|
|
1591
1621
|
* @param cfg Quality configuration containing repositories and expected settings.
|
|
1592
1622
|
* @param repos Repository overrides supplied to `terra-quality github`.
|
|
1593
1623
|
* @param explicit Whether GitHub reporting was requested as the command itself.
|
|
@@ -1595,8 +1625,8 @@ function printFindings(findings, fmt) {
|
|
|
1595
1625
|
*/
|
|
1596
1626
|
function runGitHubCheck(cfg, repos = [], explicit = false, warn = false) {
|
|
1597
1627
|
qualityHeader('github', 'Reading open issues, Dependabot, code scanning, and secret scanning alerts');
|
|
1598
|
-
const
|
|
1599
|
-
if (!
|
|
1628
|
+
const tgts = repos.length ? repos : (cfg.github?.repositories ?? [inferGitHubRepo(cfg.root)].filter((repo) => Boolean(repo)));
|
|
1629
|
+
if (!tgts.length) {
|
|
1600
1630
|
const detail = 'Set github.repositories in quality.json or pass --repository owner/name.';
|
|
1601
1631
|
reportGitHubData = { schemaVersion: 1, generatedAt: new Date().toISOString(), state: 'partial', detail, repositories: [] };
|
|
1602
1632
|
updateGitHubStage('failed', 0);
|
|
@@ -1614,7 +1644,7 @@ function runGitHubCheck(cfg, repos = [], explicit = false, warn = false) {
|
|
|
1614
1644
|
return;
|
|
1615
1645
|
}
|
|
1616
1646
|
try {
|
|
1617
|
-
reportGitHubData = buildGitHubReport(
|
|
1647
|
+
reportGitHubData = buildGitHubReport(tgts, cfg.github?.requiredFeatures, undefined, cfg.github?.featureExemptions);
|
|
1618
1648
|
const findings = reportGitHubData.repositories.flatMap((repo) => repo.findings);
|
|
1619
1649
|
const failed = reportGitHubData.state === 'partial';
|
|
1620
1650
|
updateGitHubStage(failed ? 'failed' : findings.length ? 'findings' : 'passed', findings.length);
|
|
@@ -1634,7 +1664,9 @@ function runGitHubCheck(cfg, repos = [], explicit = false, warn = false) {
|
|
|
1634
1664
|
process.exitCode = 1;
|
|
1635
1665
|
}
|
|
1636
1666
|
}
|
|
1637
|
-
/**
|
|
1667
|
+
/**
|
|
1668
|
+
* Update the JSON summary for the active GitHub report stage.
|
|
1669
|
+
*
|
|
1638
1670
|
* @param status Result state to record.
|
|
1639
1671
|
* @param findings Number of GitHub findings in the report.
|
|
1640
1672
|
*/
|
|
@@ -1645,8 +1677,11 @@ function updateGitHubStage(status, findings) {
|
|
|
1645
1677
|
stage.findings = findings;
|
|
1646
1678
|
}
|
|
1647
1679
|
}
|
|
1648
|
-
/**
|
|
1680
|
+
/**
|
|
1681
|
+
* Infer the current repository slug from its GitHub origin remote.
|
|
1682
|
+
*
|
|
1649
1683
|
* @param root Repository root to inspect.
|
|
1684
|
+
*
|
|
1650
1685
|
* @return The owner/name slug, or undefined for a non-GitHub remote.
|
|
1651
1686
|
*/
|
|
1652
1687
|
function inferGitHubRepo(root) {
|
|
@@ -1675,6 +1710,25 @@ function runGenericChecks(cfg, analysis) {
|
|
|
1675
1710
|
process.exitCode = 1;
|
|
1676
1711
|
}
|
|
1677
1712
|
}
|
|
1713
|
+
/**
|
|
1714
|
+
* Runs the configured Tailwind canonical-class check and records its CLI report stage.
|
|
1715
|
+
*
|
|
1716
|
+
* @param cfg Quality configuration containing the Tailwind stylesheet and source roots.
|
|
1717
|
+
* @param fix Whether to write Tailwind's canonical class suggestions.
|
|
1718
|
+
*
|
|
1719
|
+
* @return Promise resolving when the diagnostic or autofix completes.
|
|
1720
|
+
*/
|
|
1721
|
+
function runTailwindCanonicalCheck(cfg, fix) {
|
|
1722
|
+
qualityHeader('tailwind-canonical', `${fix ? 'Fixing and checking' : 'Checking'} canonical Tailwind classes`);
|
|
1723
|
+
return runTailwindCanonical(cfg, fix).then(() => undefined, (error) => {
|
|
1724
|
+
const stage = reportStages?.get('tailwind-canonical');
|
|
1725
|
+
if (stage) {
|
|
1726
|
+
stage.status = 'failed';
|
|
1727
|
+
stage.findings = Number(/failed with (\d+) suggestion/.exec(error instanceof Error ? error.message : '')?.[1] ?? 0);
|
|
1728
|
+
}
|
|
1729
|
+
throw error;
|
|
1730
|
+
});
|
|
1731
|
+
}
|
|
1678
1732
|
/**
|
|
1679
1733
|
* Renames safe repository-local declarations and repairs matching references.
|
|
1680
1734
|
*
|
|
@@ -2043,25 +2097,20 @@ function runDocumentation(cfg, opts, args, quiet) {
|
|
|
2043
2097
|
* @param cfg Normalized quality configuration for the current repository.
|
|
2044
2098
|
* @param args CLI arguments to dispatch.
|
|
2045
2099
|
*
|
|
2046
|
-
* @return
|
|
2100
|
+
* @return A completion promise for asynchronous checks, or nothing for synchronous commands.
|
|
2047
2101
|
*/
|
|
2048
2102
|
export function runQualityCli(cfg, args = process.argv.slice(2)) {
|
|
2049
2103
|
validCliArgs(args);
|
|
2050
2104
|
const reportIdx = args.indexOf('--report-json');
|
|
2051
2105
|
const reportPath = reportIdx < 0 ? undefined : args[reportIdx + 1];
|
|
2052
2106
|
const cmdArgs = reportIdx < 0 ? args : args.filter((_, i) => i !== reportIdx && i !== reportIdx + 1);
|
|
2053
|
-
if (!reportPath)
|
|
2054
|
-
dispatchQualityCli(cfg, cmdArgs);
|
|
2055
|
-
return;
|
|
2056
|
-
}
|
|
2107
|
+
if (!reportPath)
|
|
2108
|
+
return dispatchQualityCli(cfg, cmdArgs);
|
|
2057
2109
|
reportStages = new Map();
|
|
2058
2110
|
reportStageName = undefined;
|
|
2059
2111
|
reportGitHubData = undefined;
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
}
|
|
2063
|
-
finally {
|
|
2064
|
-
const executed = [...reportStages.values()].filter((s) => {
|
|
2112
|
+
const writeReport = () => {
|
|
2113
|
+
const executed = [...(reportStages?.values() ?? [])].filter((s) => {
|
|
2065
2114
|
if (s.name.includes('/') || s.name.startsWith('recommendation:'))
|
|
2066
2115
|
return false;
|
|
2067
2116
|
const names = { imports: 'import', naming: 'name', aliases: 'alias', forwarding: 'forward', documentation: 'document', typecheck: 'type' };
|
|
@@ -2072,6 +2121,8 @@ export function runQualityCli(cfg, args = process.argv.slice(2)) {
|
|
|
2072
2121
|
return false;
|
|
2073
2122
|
if (s.name === 'repeated-classes' && !cfg.repeatedClasses)
|
|
2074
2123
|
return false;
|
|
2124
|
+
if (s.name === 'tailwind-canonical' && !cfg.tailwindCanonical)
|
|
2125
|
+
return false;
|
|
2075
2126
|
if (s.name === 'regex-dedup' && !cfg.regexDedup)
|
|
2076
2127
|
return false;
|
|
2077
2128
|
if (s.name === 'ui-patterns' && !cfg.uiPatterns)
|
|
@@ -2091,6 +2142,7 @@ export function runQualityCli(cfg, args = process.argv.slice(2)) {
|
|
|
2091
2142
|
'aliases',
|
|
2092
2143
|
'security',
|
|
2093
2144
|
'repeated-classes',
|
|
2145
|
+
'tailwind-canonical',
|
|
2094
2146
|
'regex-dedup',
|
|
2095
2147
|
'ui-patterns',
|
|
2096
2148
|
'github',
|
|
@@ -2106,13 +2158,25 @@ export function runQualityCli(cfg, args = process.argv.slice(2)) {
|
|
|
2106
2158
|
writeFileSync(resolve(cfg.root, reportPath), `${JSON.stringify(report, null, 2)}\n`);
|
|
2107
2159
|
reportStages = undefined;
|
|
2108
2160
|
reportStageName = undefined;
|
|
2161
|
+
};
|
|
2162
|
+
try {
|
|
2163
|
+
const result = dispatchQualityCli(cfg, cmdArgs);
|
|
2164
|
+
if (result instanceof Promise)
|
|
2165
|
+
return result.finally(writeReport);
|
|
2166
|
+
}
|
|
2167
|
+
catch (err) {
|
|
2168
|
+
writeReport();
|
|
2169
|
+
throw err;
|
|
2109
2170
|
}
|
|
2171
|
+
writeReport();
|
|
2110
2172
|
}
|
|
2111
2173
|
/**
|
|
2112
2174
|
* Route validated arguments to the requested quality command.
|
|
2113
2175
|
*
|
|
2114
2176
|
* @param cfg Normalized repository quality configuration.
|
|
2115
2177
|
* @param args Validated CLI arguments.
|
|
2178
|
+
*
|
|
2179
|
+
* @return A completion promise for asynchronous checks, or nothing for synchronous commands.
|
|
2116
2180
|
*/
|
|
2117
2181
|
function dispatchQualityCli(cfg, args) {
|
|
2118
2182
|
validCliArgs(args);
|
|
@@ -2123,7 +2187,7 @@ function dispatchQualityCli(cfg, args) {
|
|
|
2123
2187
|
}
|
|
2124
2188
|
if (args.includes('--help') || args.includes('-h')) {
|
|
2125
2189
|
console.info('Usage: terra-quality <command> [options]');
|
|
2126
|
-
console.info('Commands include quality, history, github, document, format, lint, knip, types, single-use, dead-code, security, and domain.');
|
|
2190
|
+
console.info('Commands include quality, history, github, document, format, lint, knip, types, single-use, dead-code, tailwind-canonical, security, and domain.');
|
|
2127
2191
|
console.info('Options: --all, --warn, --strict, --skip-commands, --skip-recommendations, --report-json <path>, --repository owner/name; history also accepts --branch <name> and --base-sha <sha>.');
|
|
2128
2192
|
return;
|
|
2129
2193
|
}
|
|
@@ -2136,6 +2200,11 @@ function dispatchQualityCli(cfg, args) {
|
|
|
2136
2200
|
skipRecommendations: args.includes('--skip-recommendations'),
|
|
2137
2201
|
};
|
|
2138
2202
|
const fix = (check) => qualityAutofixEnabled(cfg, check, opts.fix);
|
|
2203
|
+
if (cfg.tailwindCanonical && qualityCheckEnabled(cfg, 'tailwind-canonical') && ['quality', 'quality:all', 'quality:baseline', 'imports', 'quality:patterns', 'types:architecture'].includes(cmd))
|
|
2204
|
+
return runTailwindCanonicalCheck(cfg, fix('tailwindCanonical')).then(() => {
|
|
2205
|
+
const checks = { ...cfg.enabledChecks, 'tailwind-canonical': false };
|
|
2206
|
+
return dispatchQualityCli({ ...cfg, enabledChecks: checks }, args);
|
|
2207
|
+
});
|
|
2139
2208
|
if (cmd === 'github') {
|
|
2140
2209
|
runGitHubCheck(cfg, optVals(args, '--repository'), true, args.includes('--warn'));
|
|
2141
2210
|
return;
|
|
@@ -2187,6 +2256,11 @@ function dispatchQualityCli(cfg, args) {
|
|
|
2187
2256
|
runDeadCodeCmd(cfg, qualityAutofixEnabled(cfg, 'dead-code', opts.fix), opts.all);
|
|
2188
2257
|
return;
|
|
2189
2258
|
}
|
|
2259
|
+
if (cmd === 'tailwind-canonical') {
|
|
2260
|
+
if (!qualityCheckEnabled(cfg, 'tailwind-canonical') || !cfg.tailwindCanonical)
|
|
2261
|
+
return;
|
|
2262
|
+
return runTailwindCanonicalCheck(cfg, fix('tailwindCanonical'));
|
|
2263
|
+
}
|
|
2190
2264
|
if (cmd === 'security' || cmd === 'repeated-classes' || cmd === 'regex-dedup' || cmd === 'ui-patterns') {
|
|
2191
2265
|
if (cmd === 'repeated-classes' && fix('repeatedClasses')) {
|
|
2192
2266
|
const result = fixRepeatedClasses(cfg);
|
|
@@ -2303,7 +2377,7 @@ function dispatchQualityCli(cfg, args) {
|
|
|
2303
2377
|
* @param file Optional path to the quality configuration file.
|
|
2304
2378
|
* @param args CLI arguments passed to the dispatcher.
|
|
2305
2379
|
*
|
|
2306
|
-
* @return
|
|
2380
|
+
* @return A completion promise for asynchronous checks, or nothing for synchronous commands.
|
|
2307
2381
|
*/
|
|
2308
2382
|
export function runQualityFromCfg(file, args = process.argv.slice(2)) {
|
|
2309
2383
|
validCliArgs(args);
|
|
@@ -2331,7 +2405,7 @@ export function runQualityFromCfg(file, args = process.argv.slice(2)) {
|
|
|
2331
2405
|
if (!flatCfg)
|
|
2332
2406
|
throw new Error(`ESLint flat config is required for ${cmd}. Add eslint.config.js, eslint.config.mjs, or eslint.config.ts at ${cfg.root} before running Terra Quality.`);
|
|
2333
2407
|
}
|
|
2334
|
-
runQualityCli(cfg, args);
|
|
2408
|
+
return runQualityCli(cfg, args);
|
|
2335
2409
|
}
|
|
2336
2410
|
/**
|
|
2337
2411
|
* Reject unknown commands and flags before loading configuration or invoking a fixer.
|
|
@@ -2364,15 +2438,19 @@ function validCliArgs(args) {
|
|
|
2364
2438
|
*
|
|
2365
2439
|
* @param args CLI arguments.
|
|
2366
2440
|
* @param flag Flag whose following value is returned.
|
|
2441
|
+
*
|
|
2367
2442
|
* @return The flag value, or undefined when the flag was not supplied.
|
|
2368
2443
|
*/
|
|
2369
2444
|
function optVal(args, flag) {
|
|
2370
2445
|
const idx = args.indexOf(flag);
|
|
2371
2446
|
return idx < 0 ? undefined : args[idx + 1];
|
|
2372
2447
|
}
|
|
2373
|
-
/**
|
|
2448
|
+
/**
|
|
2449
|
+
* Read each occurrence of a repeated CLI option.
|
|
2450
|
+
*
|
|
2374
2451
|
* @param args Validated command-line arguments.
|
|
2375
2452
|
* @param flag Repeated flag to read.
|
|
2453
|
+
*
|
|
2376
2454
|
* @return Values supplied after each occurrence of the flag.
|
|
2377
2455
|
*/
|
|
2378
2456
|
function optVals(args, flag) {
|
|
@@ -2477,5 +2555,8 @@ export function runTypecheck(cfg, args = []) {
|
|
|
2477
2555
|
console.error(` ${result.status ? '✗ failed' : '✓ passed'}`);
|
|
2478
2556
|
}
|
|
2479
2557
|
if (process.argv[1] && realpathSync(resolve(process.argv[1])) === realpathSync(fileURLToPath(import.meta.url)))
|
|
2480
|
-
runQualityFromCfg()
|
|
2558
|
+
Promise.resolve(runQualityFromCfg()).catch((error) => {
|
|
2559
|
+
console.error(error instanceof Error ? error.message : error);
|
|
2560
|
+
process.exitCode = 1;
|
|
2561
|
+
});
|
|
2481
2562
|
//# sourceMappingURL=runner.js.map
|