design-constraint-validator 1.0.0 → 1.1.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/LICENSE +21 -21
- package/README.md +215 -659
- package/adapters/README.md +46 -46
- package/adapters/css.ts +116 -116
- package/adapters/js.ts +14 -14
- package/adapters/json.ts +45 -45
- package/cli/build-css.ts +32 -32
- package/cli/commands/build.ts +65 -65
- package/cli/commands/graph.d.ts.map +1 -1
- package/cli/commands/graph.js +26 -10
- package/cli/commands/graph.ts +180 -137
- package/cli/commands/index.ts +7 -7
- package/cli/commands/patch-apply.ts +80 -80
- package/cli/commands/patch.ts +22 -22
- package/cli/commands/set.d.ts.map +1 -1
- package/cli/commands/set.js +12 -4
- package/cli/commands/set.ts +239 -225
- package/cli/commands/utils.ts +50 -50
- package/cli/commands/validate.d.ts.map +1 -1
- package/cli/commands/validate.js +86 -33
- package/cli/commands/validate.ts +176 -115
- package/cli/commands/why.d.ts.map +1 -1
- package/cli/commands/why.js +86 -20
- package/cli/commands/why.ts +158 -46
- package/cli/config-schema.ts +27 -27
- package/cli/config.ts +35 -35
- package/cli/constraint-registry.d.ts +101 -0
- package/cli/constraint-registry.d.ts.map +1 -0
- package/cli/constraint-registry.js +225 -0
- package/cli/constraint-registry.ts +304 -0
- package/cli/constraints-loader.d.ts +30 -0
- package/cli/constraints-loader.d.ts.map +1 -0
- package/cli/constraints-loader.js +58 -0
- package/cli/constraints-loader.ts +83 -0
- package/cli/cross-axis-loader.d.ts +91 -0
- package/cli/cross-axis-loader.d.ts.map +1 -0
- package/cli/cross-axis-loader.js +222 -0
- package/cli/cross-axis-loader.ts +289 -0
- package/cli/dcv.js +4 -0
- package/cli/dcv.ts +111 -107
- package/cli/engine-helpers.d.ts +33 -0
- package/cli/engine-helpers.d.ts.map +1 -1
- package/cli/engine-helpers.js +87 -22
- package/cli/engine-helpers.ts +133 -61
- package/cli/graph-poset.ts +74 -74
- package/cli/json-output.d.ts +64 -0
- package/cli/json-output.d.ts.map +1 -0
- package/cli/json-output.js +107 -0
- package/cli/json-output.ts +177 -0
- package/cli/result.ts +27 -27
- package/cli/run.ts +54 -54
- package/cli/smoke-test.ts +40 -40
- package/cli/types.d.ts +6 -0
- package/cli/types.d.ts.map +1 -1
- package/cli/types.ts +84 -78
- package/core/breakpoints.ts +50 -50
- package/core/cli-format.ts +31 -31
- package/core/color.ts +148 -148
- package/core/constraints/cross-axis.ts +114 -114
- package/core/constraints/monotonic-lightness.ts +38 -38
- package/core/constraints/monotonic.ts +74 -74
- package/core/constraints/threshold.ts +43 -43
- package/core/constraints/wcag.ts +70 -70
- package/core/cross-axis-config.d.ts +29 -0
- package/core/cross-axis-config.d.ts.map +1 -1
- package/core/cross-axis-config.js +29 -0
- package/core/cross-axis-config.ts +181 -151
- package/core/engine.d.ts +95 -0
- package/core/engine.d.ts.map +1 -1
- package/core/engine.js +22 -0
- package/core/engine.ts +167 -65
- package/core/flatten.ts +116 -116
- package/core/image-export.ts +48 -48
- package/core/index.d.ts +9 -30
- package/core/index.d.ts.map +1 -1
- package/core/index.js +7 -54
- package/core/index.ts +10 -72
- package/core/patch.ts +134 -134
- package/core/poset.ts +311 -311
- package/core/why.ts +63 -63
- package/package.json +96 -90
- package/themes/color.lg.order.json +15 -15
- package/themes/color.md.order.json +15 -15
- package/themes/color.order.json +15 -15
- package/themes/color.sm.order.json +15 -15
- package/themes/cross-axis.rules.json +35 -35
- package/themes/cross-axis.sm.rules.json +12 -12
- package/themes/layout.lg.order.json +18 -18
- package/themes/layout.md.order.json +18 -18
- package/themes/layout.order.json +18 -18
- package/themes/layout.sm.order.json +18 -18
- package/themes/spacing.order.json +14 -14
- package/themes/typography.lg.order.json +15 -15
- package/themes/typography.md.order.json +15 -15
- package/themes/typography.order.json +15 -15
- package/themes/typography.sm.order.json +15 -15
- package/dist/test-overrides-removal.json +0 -4
- package/dist/tmp.patch.json +0 -35
- package/tokens/overrides/base.json +0 -22
- package/tokens/overrides/lg.json +0 -20
- package/tokens/overrides/md.json +0 -16
- package/tokens/overrides/sm.json +0 -16
- package/tokens/overrides/viol.color.json +0 -6
- package/tokens/overrides/viol.typography.json +0 -6
- package/tokens/tokens.demo-violations.json +0 -116
- package/tokens/tokens.example.json +0 -128
- package/tokens/tokens.json +0 -67
- package/tokens/tokens.multi-violations.json +0 -21
- package/tokens/tokens.schema.d.ts +0 -2298
- package/tokens/tokens.schema.d.ts.map +0 -1
- package/tokens/tokens.schema.js +0 -148
- package/tokens/tokens.schema.ts +0 -196
- package/tokens/tokens.test.json +0 -38
- package/tokens/tokens.touch-violation.json +0 -8
- package/tokens/typography.classes.css +0 -11
- package/tokens/typography.css +0 -20
package/cli/commands/utils.ts
CHANGED
|
@@ -1,50 +1,50 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
2
|
-
import { resolve, dirname } from 'node:path';
|
|
3
|
-
import { valuesToCss } from '../../adapters/css.js';
|
|
4
|
-
import type { TokenNode, TokenValue } from '../../core/flatten.js';
|
|
5
|
-
|
|
6
|
-
// Shared helpers for command modules
|
|
7
|
-
|
|
8
|
-
export function loadTokens(tokensPath: string): TokenNode {
|
|
9
|
-
const resolvedPath = resolve(tokensPath);
|
|
10
|
-
if (!existsSync(resolvedPath)) {
|
|
11
|
-
throw new Error(`Token file not found: ${resolvedPath}`);
|
|
12
|
-
}
|
|
13
|
-
const data = JSON.parse(readFileSync(resolvedPath, 'utf8'));
|
|
14
|
-
if (typeof data !== 'object' || data === null) {
|
|
15
|
-
throw new Error(`Invalid token file: expected object, got ${typeof data}`);
|
|
16
|
-
}
|
|
17
|
-
return data as TokenNode;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function outputResult(data: unknown, format: string, outputPath?: string): void {
|
|
21
|
-
let content: string;
|
|
22
|
-
switch (format) {
|
|
23
|
-
case 'json':
|
|
24
|
-
content = JSON.stringify(data, null, 2);
|
|
25
|
-
break;
|
|
26
|
-
case 'css': {
|
|
27
|
-
if (data && typeof data === 'object' && 'patch' in (data as { patch?: unknown })) {
|
|
28
|
-
const patch = (data as { patch: Record<string, TokenValue> }).patch;
|
|
29
|
-
content = valuesToCss(patch);
|
|
30
|
-
} else if (data && typeof data === 'object') {
|
|
31
|
-
content = valuesToCss(data as Record<string, TokenValue>);
|
|
32
|
-
} else {
|
|
33
|
-
content = valuesToCss({});
|
|
34
|
-
}
|
|
35
|
-
break; }
|
|
36
|
-
case 'js':
|
|
37
|
-
content = `export default ${JSON.stringify(data, null, 2)};`;
|
|
38
|
-
break;
|
|
39
|
-
default:
|
|
40
|
-
content = JSON.stringify(data, null, 2);
|
|
41
|
-
}
|
|
42
|
-
if (outputPath) {
|
|
43
|
-
const dir = dirname(outputPath);
|
|
44
|
-
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
45
|
-
writeFileSync(outputPath, content, 'utf8');
|
|
46
|
-
console.log(`Output written to: ${outputPath}`);
|
|
47
|
-
} else {
|
|
48
|
-
console.log(content);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
2
|
+
import { resolve, dirname } from 'node:path';
|
|
3
|
+
import { valuesToCss } from '../../adapters/css.js';
|
|
4
|
+
import type { TokenNode, TokenValue } from '../../core/flatten.js';
|
|
5
|
+
|
|
6
|
+
// Shared helpers for command modules
|
|
7
|
+
|
|
8
|
+
export function loadTokens(tokensPath: string): TokenNode {
|
|
9
|
+
const resolvedPath = resolve(tokensPath);
|
|
10
|
+
if (!existsSync(resolvedPath)) {
|
|
11
|
+
throw new Error(`Token file not found: ${resolvedPath}`);
|
|
12
|
+
}
|
|
13
|
+
const data = JSON.parse(readFileSync(resolvedPath, 'utf8'));
|
|
14
|
+
if (typeof data !== 'object' || data === null) {
|
|
15
|
+
throw new Error(`Invalid token file: expected object, got ${typeof data}`);
|
|
16
|
+
}
|
|
17
|
+
return data as TokenNode;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function outputResult(data: unknown, format: string, outputPath?: string): void {
|
|
21
|
+
let content: string;
|
|
22
|
+
switch (format) {
|
|
23
|
+
case 'json':
|
|
24
|
+
content = JSON.stringify(data, null, 2);
|
|
25
|
+
break;
|
|
26
|
+
case 'css': {
|
|
27
|
+
if (data && typeof data === 'object' && 'patch' in (data as { patch?: unknown })) {
|
|
28
|
+
const patch = (data as { patch: Record<string, TokenValue> }).patch;
|
|
29
|
+
content = valuesToCss(patch);
|
|
30
|
+
} else if (data && typeof data === 'object') {
|
|
31
|
+
content = valuesToCss(data as Record<string, TokenValue>);
|
|
32
|
+
} else {
|
|
33
|
+
content = valuesToCss({});
|
|
34
|
+
}
|
|
35
|
+
break; }
|
|
36
|
+
case 'js':
|
|
37
|
+
content = `export default ${JSON.stringify(data, null, 2)};`;
|
|
38
|
+
break;
|
|
39
|
+
default:
|
|
40
|
+
content = JSON.stringify(data, null, 2);
|
|
41
|
+
}
|
|
42
|
+
if (outputPath) {
|
|
43
|
+
const dir = dirname(outputPath);
|
|
44
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
45
|
+
writeFileSync(outputPath, content, 'utf8');
|
|
46
|
+
console.log(`Output written to: ${outputPath}`);
|
|
47
|
+
} else {
|
|
48
|
+
console.log(content);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["validate.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["validate.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAMnD,wBAAsB,eAAe,CAAC,QAAQ,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAoK9E"}
|
package/cli/commands/validate.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { flattenTokens } from '../../core/flatten.js';
|
|
2
|
-
import {
|
|
2
|
+
import { Engine } from '../../core/engine.js';
|
|
3
3
|
import { loadConfig } from '../config.js';
|
|
4
|
-
import { parseBreakpoints, loadTokensWithBreakpoint } from '../../core/breakpoints.js';
|
|
5
|
-
import {
|
|
4
|
+
import { parseBreakpoints, loadTokensWithBreakpoint, mergeTokens } from '../../core/breakpoints.js';
|
|
5
|
+
import { createValidationResult, createValidationReceipt, writeJsonOutput } from '../json-output.js';
|
|
6
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { setupConstraints } from '../constraint-registry.js';
|
|
6
9
|
export async function validateCommand(_options) {
|
|
7
10
|
try {
|
|
8
11
|
const bps = parseBreakpoints(process.argv);
|
|
@@ -13,9 +16,13 @@ export async function validateCommand(_options) {
|
|
|
13
16
|
let totalWarnings = 0;
|
|
14
17
|
const argv = process.argv.slice(2);
|
|
15
18
|
const failOnIdx = argv.indexOf('--fail-on');
|
|
16
|
-
const failOn = failOnIdx >= 0 ? argv[failOnIdx + 1] : 'error';
|
|
19
|
+
const failOn = _options.failOn ?? (failOnIdx >= 0 ? argv[failOnIdx + 1] : 'error');
|
|
17
20
|
const sumIdx = argv.indexOf('--summary');
|
|
18
|
-
const summaryFmt = sumIdx >= 0 ? argv[sumIdx + 1] : 'none';
|
|
21
|
+
const summaryFmt = _options.summary ?? (sumIdx >= 0 ? argv[sumIdx + 1] : 'none');
|
|
22
|
+
const outputFormat = _options.format ?? 'text';
|
|
23
|
+
// Collect all issues for JSON output
|
|
24
|
+
const allErrors = [];
|
|
25
|
+
const allWarnings = [];
|
|
19
26
|
const rows = [];
|
|
20
27
|
function pushRow(bpLabel, stats) { rows.push({ bp: bpLabel, ...stats }); }
|
|
21
28
|
function printSummaryTable(rs) {
|
|
@@ -45,19 +52,31 @@ export async function validateCommand(_options) {
|
|
|
45
52
|
const tStartTotal = globalThis.performance.now();
|
|
46
53
|
for (const bp of plan) {
|
|
47
54
|
const tStart = globalThis.performance.now();
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
let tokens = loadTokensWithBreakpoint(bp);
|
|
56
|
+
// Optional theme overlay (tokens/themes/<name>.json), mirroring build behavior
|
|
57
|
+
if (_options.theme) {
|
|
58
|
+
const themePath = join('tokens/themes', `${_options.theme}.json`);
|
|
59
|
+
if (existsSync(themePath)) {
|
|
60
|
+
try {
|
|
61
|
+
const themeTokens = JSON.parse(readFileSync(themePath, 'utf8'));
|
|
62
|
+
tokens = mergeTokens(tokens, themeTokens);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
// If theme file is invalid JSON, ignore and proceed with base tokens
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// Create engine with flattened tokens
|
|
70
|
+
const { flat, edges } = flattenTokens(tokens);
|
|
71
|
+
const init = {};
|
|
72
|
+
for (const t of Object.values(flat)) {
|
|
73
|
+
init[t.id] = t.value;
|
|
57
74
|
}
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
75
|
+
const engine = new Engine(init, edges);
|
|
76
|
+
const knownIds = new Set(Object.keys(init));
|
|
77
|
+
// Discover and attach all constraints via centralized registry
|
|
78
|
+
setupConstraints(engine, { config, bp, constraintsDir: 'themes' }, { knownIds, crossAxisDebug });
|
|
79
|
+
const allIds = new Set(Object.keys(init));
|
|
61
80
|
const issues = engine.evaluate(allIds);
|
|
62
81
|
const errs = issues.filter((i) => i.level === 'error');
|
|
63
82
|
const warns = issues.filter((i) => i.level !== 'error');
|
|
@@ -65,14 +84,20 @@ export async function validateCommand(_options) {
|
|
|
65
84
|
anyErrors = true;
|
|
66
85
|
totalErrors += errs.length;
|
|
67
86
|
totalWarnings += warns.length;
|
|
87
|
+
// Collect for JSON output
|
|
88
|
+
allErrors.push(...errs);
|
|
89
|
+
allWarnings.push(...warns);
|
|
68
90
|
const rulesEvaluated = errs.length + warns.length;
|
|
69
91
|
pushRow(bp ?? 'global', { rules: rulesEvaluated, warnings: warns.length, errors: errs.length });
|
|
70
92
|
const dur = globalThis.performance.now() - tStart;
|
|
71
93
|
perBpTimings.push({ bp: bp ?? 'global', ms: dur });
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
94
|
+
// Only print text output if not in JSON mode
|
|
95
|
+
if (outputFormat !== 'json') {
|
|
96
|
+
console.log(`validate${bp ? ` [bp=${bp}]` : ''}: ${errs.length} error(s), ${warns.length} warning(s)${_options.perf ? ` (${dur.toFixed(2)}ms)` : ''}`);
|
|
97
|
+
for (const it of issues) {
|
|
98
|
+
const tag = it.level === 'error' ? 'ERROR' : 'WARN ';
|
|
99
|
+
console.log(`${tag} ${it.rule} ${it.id}${it.where ? ' @ ' + it.where : ''}${bp ? ` [${bp}]` : ''} — ${it.message}`);
|
|
100
|
+
}
|
|
76
101
|
}
|
|
77
102
|
}
|
|
78
103
|
const totalMs = globalThis.performance.now() - tStartTotal;
|
|
@@ -81,20 +106,48 @@ export async function validateCommand(_options) {
|
|
|
81
106
|
const agg = rows.reduce((a, b) => ({ rules: a.rules + b.rules, warnings: a.warnings + b.warnings, errors: a.errors + b.errors }), { rules: 0, warnings: 0, errors: 0 });
|
|
82
107
|
rows.push({ bp: 'TOTAL', ...agg });
|
|
83
108
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
109
|
+
// Get package version for stats
|
|
110
|
+
let engineVersion = '1.0.0';
|
|
111
|
+
try {
|
|
112
|
+
// eslint-disable-next-line no-undef
|
|
113
|
+
const pkgPath = new URL('../../package.json', import.meta.url);
|
|
114
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
115
|
+
engineVersion = pkg.version;
|
|
89
116
|
}
|
|
90
|
-
|
|
91
|
-
//
|
|
92
|
-
const totalRow = rows.find(r => r.bp === 'TOTAL');
|
|
93
|
-
const json = totalRow ? { rows, total: { rules: totalRow.rules, warnings: totalRow.warnings, errors: totalRow.errors } } : { rows };
|
|
94
|
-
console.log(JSON.stringify(json, null, 2));
|
|
117
|
+
catch {
|
|
118
|
+
// Ignore package.json read errors, use default version
|
|
95
119
|
}
|
|
96
|
-
|
|
97
|
-
|
|
120
|
+
// Handle JSON output mode
|
|
121
|
+
if (outputFormat === 'json') {
|
|
122
|
+
const result = createValidationResult(allErrors, allWarnings, totalMs, engineVersion);
|
|
123
|
+
// If receipt requested, generate full receipt
|
|
124
|
+
if (_options.receipt) {
|
|
125
|
+
const tokensFile = _options.tokens ?? 'tokens/tokens.example.json';
|
|
126
|
+
const constraintsDir = 'themes';
|
|
127
|
+
const receipt = createValidationReceipt(result, tokensFile, constraintsDir, bps[0], failOn);
|
|
128
|
+
writeJsonOutput(receipt, _options.receipt);
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
writeJsonOutput(result, _options.output);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
// Text output mode
|
|
136
|
+
if (_options.perf) {
|
|
137
|
+
console.log('[perf] per-breakpoint timings:');
|
|
138
|
+
for (const t of perBpTimings)
|
|
139
|
+
console.log(` ${t.bp}: ${t.ms.toFixed(2)}ms`);
|
|
140
|
+
console.log(`[perf] total: ${totalMs.toFixed(2)}ms`);
|
|
141
|
+
}
|
|
142
|
+
if (summaryFmt === 'json') {
|
|
143
|
+
// Provide machine-readable aggregate separate from rows if TOTAL present
|
|
144
|
+
const totalRow = rows.find(r => r.bp === 'TOTAL');
|
|
145
|
+
const json = totalRow ? { rows, total: { rules: totalRow.rules, warnings: totalRow.warnings, errors: totalRow.errors } } : { rows };
|
|
146
|
+
console.log(JSON.stringify(json, null, 2));
|
|
147
|
+
}
|
|
148
|
+
else if (summaryFmt === 'table') {
|
|
149
|
+
printSummaryTable(rows);
|
|
150
|
+
}
|
|
98
151
|
}
|
|
99
152
|
let code = anyErrors ? 1 : 0;
|
|
100
153
|
// Budget checks (do not override fail-on semantics unless budgets add failures)
|
package/cli/commands/validate.ts
CHANGED
|
@@ -1,115 +1,176 @@
|
|
|
1
|
-
import { flattenTokens } from '../../core/flatten.js';
|
|
2
|
-
import {
|
|
3
|
-
import { loadConfig } from '../config.js';
|
|
4
|
-
import { parseBreakpoints, loadTokensWithBreakpoint, type Breakpoint } from '../../core/breakpoints.js';
|
|
5
|
-
import {
|
|
6
|
-
import type {
|
|
7
|
-
import
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
type
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
if (
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
1
|
+
import { flattenTokens, type TokenNode, type FlatToken } from '../../core/flatten.js';
|
|
2
|
+
import { Engine } from '../../core/engine.js';
|
|
3
|
+
import { loadConfig } from '../config.js';
|
|
4
|
+
import { parseBreakpoints, loadTokensWithBreakpoint, mergeTokens, type Breakpoint } from '../../core/breakpoints.js';
|
|
5
|
+
import type { ConstraintIssue } from '../../core/engine.js';
|
|
6
|
+
import type { ValidateOptions } from '../types.js';
|
|
7
|
+
import { createValidationResult, createValidationReceipt, writeJsonOutput } from '../json-output.js';
|
|
8
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import { setupConstraints } from '../constraint-registry.js';
|
|
11
|
+
|
|
12
|
+
export async function validateCommand(_options: ValidateOptions): Promise<void> {
|
|
13
|
+
try {
|
|
14
|
+
const bps = parseBreakpoints(process.argv);
|
|
15
|
+
const crossAxisDebug = process.argv.includes('--cross-axis-debug');
|
|
16
|
+
const plan: (Breakpoint | undefined)[] = bps.length ? bps : [undefined];
|
|
17
|
+
let anyErrors = false; let totalErrors = 0; let totalWarnings = 0;
|
|
18
|
+
const argv = process.argv.slice(2);
|
|
19
|
+
const failOnIdx = argv.indexOf('--fail-on');
|
|
20
|
+
type FailOn = 'off' | 'warn' | 'error';
|
|
21
|
+
const failOn: FailOn = _options.failOn ?? (failOnIdx >= 0 ? (argv[failOnIdx + 1] as FailOn) : 'error');
|
|
22
|
+
const sumIdx = argv.indexOf('--summary');
|
|
23
|
+
type SummaryFmt = 'table' | 'json' | 'none';
|
|
24
|
+
const summaryFmt: SummaryFmt = _options.summary ?? (sumIdx >= 0 ? (argv[sumIdx + 1] as SummaryFmt) : 'none');
|
|
25
|
+
const outputFormat = _options.format ?? 'text';
|
|
26
|
+
|
|
27
|
+
// Collect all issues for JSON output
|
|
28
|
+
const allErrors: ConstraintIssue[] = [];
|
|
29
|
+
const allWarnings: ConstraintIssue[] = [];
|
|
30
|
+
type VRow = { bp: string; rules: number; warnings: number; errors: number };
|
|
31
|
+
const rows: VRow[] = [];
|
|
32
|
+
function pushRow(bpLabel: string, stats: { rules: number; warnings: number; errors: number }) { rows.push({ bp: bpLabel, ...stats }); }
|
|
33
|
+
function printSummaryTable(rs: VRow[]) {
|
|
34
|
+
if (!rs.length) return;
|
|
35
|
+
const showTotalLine = !rs.some(r => r.bp === 'TOTAL');
|
|
36
|
+
const cols = ['scope','rules','warnings','errors'] as const;
|
|
37
|
+
const data = rs.map(r => ({ scope: r.bp, rules: String(r.rules), warnings: String(r.warnings), errors: String(r.errors) }));
|
|
38
|
+
const widths = cols.map(c => Math.max(c.length, ...data.map(d => d[c].length)));
|
|
39
|
+
const line = (vals: string[]) => vals.map((v,i)=>v.padEnd(widths[i])).join(' ');
|
|
40
|
+
console.log(line(cols as unknown as string[]));
|
|
41
|
+
console.log(line(widths.map(w => '-'.repeat(w))));
|
|
42
|
+
for (const d of data) console.log(line(cols.map(c => d[c])));
|
|
43
|
+
if (showTotalLine && rs.length > 1) {
|
|
44
|
+
const tot = rs.reduce((a,b)=>({ rules:a.rules+b.rules, warnings:a.warnings+b.warnings, errors:a.errors+b.errors }), { rules:0,warnings:0,errors:0 });
|
|
45
|
+
console.log(line(['TOTAL', String(tot.rules), String(tot.warnings), String(tot.errors)]));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const cfgRes = loadConfig(_options.config);
|
|
49
|
+
if (!cfgRes.ok) { console.error(cfgRes.error); process.exit(2); }
|
|
50
|
+
const config = cfgRes.value;
|
|
51
|
+
const perBpTimings: Array<{ bp: string; ms: number }> = [];
|
|
52
|
+
const tStartTotal = globalThis.performance.now();
|
|
53
|
+
for (const bp of plan) {
|
|
54
|
+
const tStart = globalThis.performance.now();
|
|
55
|
+
let tokens: TokenNode = loadTokensWithBreakpoint(bp);
|
|
56
|
+
// Optional theme overlay (tokens/themes/<name>.json), mirroring build behavior
|
|
57
|
+
if (_options.theme) {
|
|
58
|
+
const themePath = join('tokens/themes', `${_options.theme}.json`);
|
|
59
|
+
if (existsSync(themePath)) {
|
|
60
|
+
try {
|
|
61
|
+
const themeTokens = JSON.parse(readFileSync(themePath, 'utf8'));
|
|
62
|
+
tokens = mergeTokens(tokens, themeTokens);
|
|
63
|
+
} catch {
|
|
64
|
+
// If theme file is invalid JSON, ignore and proceed with base tokens
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// Create engine with flattened tokens
|
|
69
|
+
const { flat, edges } = flattenTokens(tokens);
|
|
70
|
+
const init: Record<string, string | number> = {};
|
|
71
|
+
for (const t of Object.values(flat)) {
|
|
72
|
+
init[(t as FlatToken).id] = (t as FlatToken).value;
|
|
73
|
+
}
|
|
74
|
+
const engine = new Engine(init, edges);
|
|
75
|
+
const knownIds = new Set(Object.keys(init));
|
|
76
|
+
|
|
77
|
+
// Discover and attach all constraints via centralized registry
|
|
78
|
+
setupConstraints(
|
|
79
|
+
engine,
|
|
80
|
+
{ config, bp, constraintsDir: 'themes' },
|
|
81
|
+
{ knownIds, crossAxisDebug },
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
const allIds = new Set(Object.keys(init));
|
|
85
|
+
const issues = engine.evaluate(allIds);
|
|
86
|
+
const errs = issues.filter((i: ConstraintIssue) => i.level === 'error');
|
|
87
|
+
const warns = issues.filter((i: ConstraintIssue) => i.level !== 'error');
|
|
88
|
+
if (errs.length) anyErrors = true;
|
|
89
|
+
totalErrors += errs.length; totalWarnings += warns.length;
|
|
90
|
+
|
|
91
|
+
// Collect for JSON output
|
|
92
|
+
allErrors.push(...errs);
|
|
93
|
+
allWarnings.push(...warns);
|
|
94
|
+
|
|
95
|
+
const rulesEvaluated = errs.length + warns.length; pushRow(bp ?? 'global', { rules: rulesEvaluated, warnings: warns.length, errors: errs.length });
|
|
96
|
+
const dur = globalThis.performance.now() - tStart;
|
|
97
|
+
perBpTimings.push({ bp: bp ?? 'global', ms: dur });
|
|
98
|
+
|
|
99
|
+
// Only print text output if not in JSON mode
|
|
100
|
+
if (outputFormat !== 'json') {
|
|
101
|
+
console.log(`validate${bp ? ` [bp=${bp}]` : ''}: ${errs.length} error(s), ${warns.length} warning(s)${_options.perf ? ` (${dur.toFixed(2)}ms)` : ''}`);
|
|
102
|
+
for (const it of issues) { const tag = it.level === 'error' ? 'ERROR' : 'WARN '; console.log(`${tag} ${it.rule} ${it.id}${it.where ? ' @ ' + it.where : ''}${bp ? ` [${bp}]` : ''} — ${it.message}`); }
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const totalMs = globalThis.performance.now() - tStartTotal;
|
|
106
|
+
// Append aggregate total row if multiple scopes and not already added
|
|
107
|
+
if (rows.length > 1) {
|
|
108
|
+
const agg = rows.reduce((a,b)=>({ rules:a.rules+b.rules, warnings:a.warnings+b.warnings, errors:a.errors+b.errors }), { rules:0,warnings:0,errors:0 });
|
|
109
|
+
rows.push({ bp: 'TOTAL', ...agg });
|
|
110
|
+
}
|
|
111
|
+
// Get package version for stats
|
|
112
|
+
let engineVersion = '1.0.0';
|
|
113
|
+
try {
|
|
114
|
+
// eslint-disable-next-line no-undef
|
|
115
|
+
const pkgPath = new URL('../../package.json', import.meta.url);
|
|
116
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
117
|
+
engineVersion = pkg.version;
|
|
118
|
+
} catch {
|
|
119
|
+
// Ignore package.json read errors, use default version
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Handle JSON output mode
|
|
123
|
+
if (outputFormat === 'json') {
|
|
124
|
+
const result = createValidationResult(allErrors, allWarnings, totalMs, engineVersion);
|
|
125
|
+
|
|
126
|
+
// If receipt requested, generate full receipt
|
|
127
|
+
if (_options.receipt) {
|
|
128
|
+
const tokensFile = _options.tokens ?? 'tokens/tokens.example.json';
|
|
129
|
+
const constraintsDir = 'themes';
|
|
130
|
+
const receipt = createValidationReceipt(result, tokensFile, constraintsDir, bps[0], failOn);
|
|
131
|
+
writeJsonOutput(receipt, _options.receipt);
|
|
132
|
+
} else {
|
|
133
|
+
writeJsonOutput(result, _options.output);
|
|
134
|
+
}
|
|
135
|
+
} else {
|
|
136
|
+
// Text output mode
|
|
137
|
+
if (_options.perf) {
|
|
138
|
+
console.log('[perf] per-breakpoint timings:');
|
|
139
|
+
for (const t of perBpTimings) console.log(` ${t.bp}: ${t.ms.toFixed(2)}ms`);
|
|
140
|
+
console.log(`[perf] total: ${totalMs.toFixed(2)}ms`);
|
|
141
|
+
}
|
|
142
|
+
if (summaryFmt === 'json') {
|
|
143
|
+
// Provide machine-readable aggregate separate from rows if TOTAL present
|
|
144
|
+
const totalRow = rows.find(r => r.bp === 'TOTAL');
|
|
145
|
+
const json = totalRow ? { rows, total: { rules: totalRow.rules, warnings: totalRow.warnings, errors: totalRow.errors } } : { rows };
|
|
146
|
+
console.log(JSON.stringify(json, null, 2));
|
|
147
|
+
} else if (summaryFmt === 'table') {
|
|
148
|
+
printSummaryTable(rows);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
let code = anyErrors ? 1 : 0;
|
|
152
|
+
// Budget checks (do not override fail-on semantics unless budgets add failures)
|
|
153
|
+
const budgetTotal = (_options as any)['budget-total-ms'] ?? _options.budgetTotalMs;
|
|
154
|
+
const budgetPerBp = (_options as any)['budget-per-bp-ms'] ?? _options.budgetPerBpMs;
|
|
155
|
+
let budgetFailed = false;
|
|
156
|
+
if (budgetTotal != null && totalMs > budgetTotal) {
|
|
157
|
+
console.error(`[perf] total time ${totalMs.toFixed(2)}ms exceeded budget ${budgetTotal}ms`);
|
|
158
|
+
budgetFailed = true;
|
|
159
|
+
}
|
|
160
|
+
if (budgetPerBp != null) {
|
|
161
|
+
for (const t of perBpTimings) {
|
|
162
|
+
if (t.ms > budgetPerBp) {
|
|
163
|
+
console.error(`[perf] ${t.bp} time ${t.ms.toFixed(2)}ms exceeded per-breakpoint budget ${budgetPerBp}ms`);
|
|
164
|
+
budgetFailed = true;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (failOn === 'off') code = 0; else if (failOn === 'warn') code = (totalErrors + totalWarnings) > 0 ? 1 : 0; else code = totalErrors > 0 ? 1 : 0;
|
|
169
|
+
if (budgetFailed) code = Math.max(code, 1);
|
|
170
|
+
process.exit(code);
|
|
171
|
+
} catch (e) {
|
|
172
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
173
|
+
console.error('validate: failed:', msg);
|
|
174
|
+
process.exit(2);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"why.d.ts","sourceRoot":"","sources":["why.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"why.d.ts","sourceRoot":"","sources":["why.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAO9C,wBAAsB,UAAU,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAmJnE"}
|