fix(tests): Correct Figma ingest test
This commit is contained in:
99
packages/dss-rules/README.md
Normal file
99
packages/dss-rules/README.md
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
# @dss/rules
|
||||||
|
|
||||||
|
Versioned design system rules for enterprise DSS enforcement.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install @dss/rules
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### In Your Project
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const rules = require('@dss/rules');
|
||||||
|
|
||||||
|
// Load all rules
|
||||||
|
const allRules = rules.loadRules();
|
||||||
|
|
||||||
|
// Get specific category
|
||||||
|
const colorRules = rules.getRulesByCategory('colors');
|
||||||
|
|
||||||
|
// Validate a value
|
||||||
|
const result = rules.validateValue('colors/no-hardcoded-colors', '#ff0000');
|
||||||
|
// { valid: false, violations: [...] }
|
||||||
|
|
||||||
|
// Get required tokens
|
||||||
|
const required = rules.getRequiredTokens();
|
||||||
|
// { colors: ['colors.primary', ...], spacing: [...] }
|
||||||
|
```
|
||||||
|
|
||||||
|
### In CI/CD Pipeline
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- name: Validate DSS Rules
|
||||||
|
run: |
|
||||||
|
npx @dss/rules validate src/**/*.tsx --json > dss-report.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pre-commit Hook
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
npx @dss/rules validate $(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(tsx?|jsx?|css|scss)$')
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rule Categories
|
||||||
|
|
||||||
|
| Category | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| `colors` | Color token usage, semantic naming, contrast |
|
||||||
|
| `spacing` | Spacing scale compliance, consistent values |
|
||||||
|
| `typography` | Font size scale, semantic elements |
|
||||||
|
| `components` | Component usage, prop requirements |
|
||||||
|
| `accessibility` | WCAG 2.1 AA compliance |
|
||||||
|
|
||||||
|
## Rule Severities
|
||||||
|
|
||||||
|
- **error**: Blocks CI pipeline, must fix
|
||||||
|
- **warning**: Advisory, should fix
|
||||||
|
- **info**: Suggestion, optional
|
||||||
|
|
||||||
|
## Version Policy
|
||||||
|
|
||||||
|
Rules follow semantic versioning:
|
||||||
|
|
||||||
|
- **MAJOR**: Breaking changes (new errors that may fail existing code)
|
||||||
|
- **MINOR**: New rules added as warnings
|
||||||
|
- **PATCH**: Bug fixes, documentation
|
||||||
|
|
||||||
|
Projects should pin to minor version: `@dss/rules@^1.0.0`
|
||||||
|
|
||||||
|
## API Reference
|
||||||
|
|
||||||
|
### `loadRules()`
|
||||||
|
Returns all rule sets organized by category.
|
||||||
|
|
||||||
|
### `getRulesByCategory(category)`
|
||||||
|
Returns rules for a specific category.
|
||||||
|
|
||||||
|
### `getAllRuleIds()`
|
||||||
|
Returns array of all rule IDs in format `category/rule-id`.
|
||||||
|
|
||||||
|
### `getRule(ruleId)`
|
||||||
|
Returns a specific rule definition.
|
||||||
|
|
||||||
|
### `validateValue(ruleId, value)`
|
||||||
|
Validates a value against rule patterns.
|
||||||
|
|
||||||
|
### `getRequiredTokens()`
|
||||||
|
Returns required tokens organized by category.
|
||||||
|
|
||||||
|
### `getCIConfig()`
|
||||||
|
Returns configuration object for CI pipelines.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
79
packages/dss-rules/lib/index.d.ts
vendored
Normal file
79
packages/dss-rules/lib/index.d.ts
vendored
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
/**
|
||||||
|
* Type definitions for @dss/rules
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface Rule {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
severity?: 'error' | 'warning' | 'info';
|
||||||
|
wcag?: string;
|
||||||
|
patterns?: {
|
||||||
|
forbidden?: string[];
|
||||||
|
allowed?: string[];
|
||||||
|
};
|
||||||
|
validation?: Record<string, unknown>;
|
||||||
|
exceptions?: string[];
|
||||||
|
guidelines?: string[];
|
||||||
|
components?: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RuleSet {
|
||||||
|
id: string;
|
||||||
|
version: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
category: 'tokens' | 'components' | 'accessibility' | 'patterns' | 'naming';
|
||||||
|
severity?: 'error' | 'warning' | 'info';
|
||||||
|
rules: Rule[];
|
||||||
|
tokens?: {
|
||||||
|
required?: string[];
|
||||||
|
optional?: string[];
|
||||||
|
scale?: Record<string, string>;
|
||||||
|
};
|
||||||
|
components?: Record<string, unknown>;
|
||||||
|
compliance?: Record<string, unknown>;
|
||||||
|
adoption?: {
|
||||||
|
thresholds?: {
|
||||||
|
minimum?: number;
|
||||||
|
target?: number;
|
||||||
|
excellent?: number;
|
||||||
|
};
|
||||||
|
metrics?: string[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ValidationResult {
|
||||||
|
valid: boolean;
|
||||||
|
violations: Violation[];
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Violation {
|
||||||
|
rule: string;
|
||||||
|
pattern: string;
|
||||||
|
matches: string[];
|
||||||
|
severity: 'error' | 'warning' | 'info';
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CIConfig {
|
||||||
|
version: string;
|
||||||
|
categories: string[];
|
||||||
|
errorSeverities: string[];
|
||||||
|
warningSeverities: string[];
|
||||||
|
blockingRules: string[];
|
||||||
|
advisoryRules: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CATEGORIES: string[];
|
||||||
|
|
||||||
|
export function loadRules(): Record<string, RuleSet | null>;
|
||||||
|
export function getRulesByCategory(category: string): RuleSet | null;
|
||||||
|
export function getAllRuleIds(): string[];
|
||||||
|
export function getRule(ruleId: string): Rule | null;
|
||||||
|
export function validateValue(ruleId: string, value: string): ValidationResult;
|
||||||
|
export function getRuleSeverity(ruleId: string): 'error' | 'warning' | 'info';
|
||||||
|
export function getRequiredTokens(): Record<string, string[]>;
|
||||||
|
export function getVersion(): string;
|
||||||
|
export function getCIConfig(): CIConfig;
|
||||||
198
packages/dss-rules/lib/index.js
Normal file
198
packages/dss-rules/lib/index.js
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
/**
|
||||||
|
* @dss/rules - Design System Rules Package
|
||||||
|
*
|
||||||
|
* Provides versioned rule definitions for enterprise design system enforcement.
|
||||||
|
* Pull-based distribution via npm for consistent rule versions across 60+ projects.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
// Rule categories
|
||||||
|
const CATEGORIES = ['colors', 'spacing', 'typography', 'components', 'accessibility'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load all rules from the rules directory
|
||||||
|
* @returns {Object} Rules organized by category
|
||||||
|
*/
|
||||||
|
function loadRules() {
|
||||||
|
const rules = {};
|
||||||
|
const rulesDir = path.join(__dirname, '..', 'rules');
|
||||||
|
|
||||||
|
for (const category of CATEGORIES) {
|
||||||
|
const rulePath = path.join(rulesDir, `${category}.json`);
|
||||||
|
if (fs.existsSync(rulePath)) {
|
||||||
|
try {
|
||||||
|
const content = fs.readFileSync(rulePath, 'utf-8');
|
||||||
|
rules[category] = JSON.parse(content);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to load rules for ${category}:`, error.message);
|
||||||
|
rules[category] = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return rules;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get rules for a specific category
|
||||||
|
* @param {string} category - Rule category (colors, spacing, etc.)
|
||||||
|
* @returns {Object|null} Rule definitions or null if not found
|
||||||
|
*/
|
||||||
|
function getRulesByCategory(category) {
|
||||||
|
const rules = loadRules();
|
||||||
|
return rules[category] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all rule IDs across all categories
|
||||||
|
* @returns {string[]} Array of rule IDs in format "category/rule-id"
|
||||||
|
*/
|
||||||
|
function getAllRuleIds() {
|
||||||
|
const rules = loadRules();
|
||||||
|
const ids = [];
|
||||||
|
|
||||||
|
for (const [category, ruleSet] of Object.entries(rules)) {
|
||||||
|
if (ruleSet && ruleSet.rules) {
|
||||||
|
for (const rule of ruleSet.rules) {
|
||||||
|
ids.push(`${category}/${rule.id}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a specific rule by its full ID
|
||||||
|
* @param {string} ruleId - Full rule ID in format "category/rule-id"
|
||||||
|
* @returns {Object|null} Rule definition or null
|
||||||
|
*/
|
||||||
|
function getRule(ruleId) {
|
||||||
|
const [category, id] = ruleId.split('/');
|
||||||
|
const ruleSet = getRulesByCategory(category);
|
||||||
|
|
||||||
|
if (!ruleSet || !ruleSet.rules) return null;
|
||||||
|
|
||||||
|
return ruleSet.rules.find(r => r.id === id) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate a value against rule patterns
|
||||||
|
* @param {string} ruleId - Full rule ID
|
||||||
|
* @param {string} value - Value to validate
|
||||||
|
* @returns {Object} Validation result {valid, violations}
|
||||||
|
*/
|
||||||
|
function validateValue(ruleId, value) {
|
||||||
|
const rule = getRule(ruleId);
|
||||||
|
if (!rule) {
|
||||||
|
return { valid: true, violations: [], error: `Rule not found: ${ruleId}` };
|
||||||
|
}
|
||||||
|
|
||||||
|
const violations = [];
|
||||||
|
|
||||||
|
// Check forbidden patterns
|
||||||
|
if (rule.patterns?.forbidden) {
|
||||||
|
for (const pattern of rule.patterns.forbidden) {
|
||||||
|
const regex = new RegExp(pattern, 'gi');
|
||||||
|
const matches = value.match(regex);
|
||||||
|
if (matches) {
|
||||||
|
violations.push({
|
||||||
|
rule: ruleId,
|
||||||
|
pattern,
|
||||||
|
matches,
|
||||||
|
severity: rule.severity || 'warning',
|
||||||
|
message: `Found forbidden pattern: ${matches.join(', ')}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
valid: violations.length === 0,
|
||||||
|
violations
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get required tokens from all rule sets
|
||||||
|
* @returns {Object} Required tokens organized by category
|
||||||
|
*/
|
||||||
|
function getRequiredTokens() {
|
||||||
|
const rules = loadRules();
|
||||||
|
const required = {};
|
||||||
|
|
||||||
|
for (const [category, ruleSet] of Object.entries(rules)) {
|
||||||
|
if (ruleSet?.tokens?.required) {
|
||||||
|
required[category] = ruleSet.tokens.required;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return required;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get severity level for a rule
|
||||||
|
* @param {string} ruleId - Full rule ID
|
||||||
|
* @returns {string} Severity level (error, warning, info)
|
||||||
|
*/
|
||||||
|
function getRuleSeverity(ruleId) {
|
||||||
|
const rule = getRule(ruleId);
|
||||||
|
if (!rule) return 'warning';
|
||||||
|
|
||||||
|
// Rule-specific severity overrides category default
|
||||||
|
if (rule.severity) return rule.severity;
|
||||||
|
|
||||||
|
// Fall back to category default
|
||||||
|
const [category] = ruleId.split('/');
|
||||||
|
const ruleSet = getRulesByCategory(category);
|
||||||
|
return ruleSet?.severity || 'warning';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get package version
|
||||||
|
* @returns {string} Package version
|
||||||
|
*/
|
||||||
|
function getVersion() {
|
||||||
|
const packagePath = path.join(__dirname, '..', 'package.json');
|
||||||
|
const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf-8'));
|
||||||
|
return pkg.version;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export configuration for CI/CD integration
|
||||||
|
* @returns {Object} Configuration object for CI pipelines
|
||||||
|
*/
|
||||||
|
function getCIConfig() {
|
||||||
|
return {
|
||||||
|
version: getVersion(),
|
||||||
|
categories: CATEGORIES,
|
||||||
|
errorSeverities: ['error'],
|
||||||
|
warningSeverities: ['warning'],
|
||||||
|
blockingRules: getAllRuleIds().filter(id => getRuleSeverity(id) === 'error'),
|
||||||
|
advisoryRules: getAllRuleIds().filter(id => getRuleSeverity(id) !== 'error')
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
// Rule loading
|
||||||
|
loadRules,
|
||||||
|
getRulesByCategory,
|
||||||
|
getAllRuleIds,
|
||||||
|
getRule,
|
||||||
|
|
||||||
|
// Validation
|
||||||
|
validateValue,
|
||||||
|
getRuleSeverity,
|
||||||
|
|
||||||
|
// Token helpers
|
||||||
|
getRequiredTokens,
|
||||||
|
|
||||||
|
// Metadata
|
||||||
|
getVersion,
|
||||||
|
getCIConfig,
|
||||||
|
|
||||||
|
// Constants
|
||||||
|
CATEGORIES
|
||||||
|
};
|
||||||
266
packages/dss-rules/lib/validate.js
Normal file
266
packages/dss-rules/lib/validate.js
Normal file
@@ -0,0 +1,266 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* DSS Rules Validator
|
||||||
|
*
|
||||||
|
* CLI tool for validating files against DSS rules.
|
||||||
|
* Used by CI pipelines and pre-commit hooks.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const rules = require('./index');
|
||||||
|
|
||||||
|
// ANSI colors for output
|
||||||
|
const colors = {
|
||||||
|
red: '\x1b[31m',
|
||||||
|
yellow: '\x1b[33m',
|
||||||
|
green: '\x1b[32m',
|
||||||
|
blue: '\x1b[34m',
|
||||||
|
reset: '\x1b[0m',
|
||||||
|
bold: '\x1b[1m'
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate a single file against all applicable rules
|
||||||
|
* @param {string} filePath - Path to file to validate
|
||||||
|
* @param {Object} options - Validation options
|
||||||
|
* @returns {Object} Validation results
|
||||||
|
*/
|
||||||
|
function validateFile(filePath, options = {}) {
|
||||||
|
const results = {
|
||||||
|
file: filePath,
|
||||||
|
errors: [],
|
||||||
|
warnings: [],
|
||||||
|
info: [],
|
||||||
|
passed: true
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!fs.existsSync(filePath)) {
|
||||||
|
results.errors.push({ message: `File not found: ${filePath}` });
|
||||||
|
results.passed = false;
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = fs.readFileSync(filePath, 'utf-8');
|
||||||
|
const ext = path.extname(filePath).toLowerCase();
|
||||||
|
|
||||||
|
// Determine which rule categories apply based on file type
|
||||||
|
const applicableCategories = getApplicableCategories(ext);
|
||||||
|
|
||||||
|
for (const category of applicableCategories) {
|
||||||
|
const ruleSet = rules.getRulesByCategory(category);
|
||||||
|
if (!ruleSet?.rules) continue;
|
||||||
|
|
||||||
|
for (const rule of ruleSet.rules) {
|
||||||
|
// Skip if file matches exception patterns
|
||||||
|
if (rule.exceptions?.some(exc => filePath.includes(exc.replace('*', '')))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check forbidden patterns
|
||||||
|
if (rule.patterns?.forbidden) {
|
||||||
|
for (const pattern of rule.patterns.forbidden) {
|
||||||
|
try {
|
||||||
|
const regex = new RegExp(pattern, 'gm');
|
||||||
|
let match;
|
||||||
|
while ((match = regex.exec(content)) !== null) {
|
||||||
|
const lineNumber = content.substring(0, match.index).split('\n').length;
|
||||||
|
const violation = {
|
||||||
|
rule: `${category}/${rule.id}`,
|
||||||
|
name: rule.name,
|
||||||
|
line: lineNumber,
|
||||||
|
match: match[0],
|
||||||
|
message: rule.description || `Violation of ${rule.name}`
|
||||||
|
};
|
||||||
|
|
||||||
|
const severity = rule.severity || ruleSet.severity || 'warning';
|
||||||
|
if (severity === 'error') {
|
||||||
|
results.errors.push(violation);
|
||||||
|
results.passed = false;
|
||||||
|
} else if (severity === 'warning') {
|
||||||
|
results.warnings.push(violation);
|
||||||
|
} else {
|
||||||
|
results.info.push(violation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Invalid regex, skip
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine which rule categories apply to a file type
|
||||||
|
* @param {string} ext - File extension
|
||||||
|
* @returns {string[]} Applicable category names
|
||||||
|
*/
|
||||||
|
function getApplicableCategories(ext) {
|
||||||
|
const cssTypes = ['.css', '.scss', '.sass', '.less', '.styl'];
|
||||||
|
const jsTypes = ['.js', '.jsx', '.ts', '.tsx', '.vue', '.svelte'];
|
||||||
|
const htmlTypes = ['.html', '.htm', '.vue', '.svelte', '.jsx', '.tsx'];
|
||||||
|
|
||||||
|
const categories = [];
|
||||||
|
|
||||||
|
if (cssTypes.includes(ext)) {
|
||||||
|
categories.push('colors', 'spacing', 'typography');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jsTypes.includes(ext)) {
|
||||||
|
categories.push('colors', 'spacing', 'components');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (htmlTypes.includes(ext)) {
|
||||||
|
categories.push('accessibility', 'components');
|
||||||
|
}
|
||||||
|
|
||||||
|
return categories;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate multiple files and aggregate results
|
||||||
|
* @param {string[]} files - Array of file paths
|
||||||
|
* @param {Object} options - Validation options
|
||||||
|
* @returns {Object} Aggregated results
|
||||||
|
*/
|
||||||
|
function validateFiles(files, options = {}) {
|
||||||
|
const results = {
|
||||||
|
totalFiles: files.length,
|
||||||
|
passedFiles: 0,
|
||||||
|
failedFiles: 0,
|
||||||
|
totalErrors: 0,
|
||||||
|
totalWarnings: 0,
|
||||||
|
fileResults: []
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const fileResult = validateFile(file, options);
|
||||||
|
results.fileResults.push(fileResult);
|
||||||
|
|
||||||
|
if (fileResult.passed) {
|
||||||
|
results.passedFiles++;
|
||||||
|
} else {
|
||||||
|
results.failedFiles++;
|
||||||
|
}
|
||||||
|
|
||||||
|
results.totalErrors += fileResult.errors.length;
|
||||||
|
results.totalWarnings += fileResult.warnings.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Print validation results to console
|
||||||
|
* @param {Object} results - Validation results
|
||||||
|
*/
|
||||||
|
function printResults(results) {
|
||||||
|
console.log('\n' + colors.bold + '=== DSS Rules Validation ===' + colors.reset);
|
||||||
|
console.log(`Rules version: ${rules.getVersion()}\n`);
|
||||||
|
|
||||||
|
for (const fileResult of results.fileResults) {
|
||||||
|
const statusIcon = fileResult.passed ? colors.green + '✓' : colors.red + '✗';
|
||||||
|
console.log(`${statusIcon}${colors.reset} ${fileResult.file}`);
|
||||||
|
|
||||||
|
for (const error of fileResult.errors) {
|
||||||
|
console.log(` ${colors.red}ERROR${colors.reset} [${error.rule}] Line ${error.line}: ${error.message}`);
|
||||||
|
console.log(` Found: ${colors.yellow}${error.match}${colors.reset}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const warning of fileResult.warnings) {
|
||||||
|
console.log(` ${colors.yellow}WARN${colors.reset} [${warning.rule}] Line ${warning.line}: ${warning.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('\n' + colors.bold + '=== Summary ===' + colors.reset);
|
||||||
|
console.log(`Files: ${results.passedFiles}/${results.totalFiles} passed`);
|
||||||
|
console.log(`Errors: ${colors.red}${results.totalErrors}${colors.reset}`);
|
||||||
|
console.log(`Warnings: ${colors.yellow}${results.totalWarnings}${colors.reset}`);
|
||||||
|
|
||||||
|
if (results.totalErrors > 0) {
|
||||||
|
console.log(`\n${colors.red}${colors.bold}Validation failed!${colors.reset}`);
|
||||||
|
} else {
|
||||||
|
console.log(`\n${colors.green}${colors.bold}Validation passed!${colors.reset}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Self-test to verify rules package is correctly structured
|
||||||
|
*/
|
||||||
|
function selfTest() {
|
||||||
|
console.log('Running @dss/rules self-test...\n');
|
||||||
|
|
||||||
|
const allRules = rules.loadRules();
|
||||||
|
let passed = true;
|
||||||
|
|
||||||
|
for (const [category, ruleSet] of Object.entries(allRules)) {
|
||||||
|
if (!ruleSet) {
|
||||||
|
console.log(`${colors.red}✗${colors.reset} ${category}: Failed to load`);
|
||||||
|
passed = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ruleCount = ruleSet.rules?.length || 0;
|
||||||
|
console.log(`${colors.green}✓${colors.reset} ${category}: ${ruleCount} rules, version ${ruleSet.version}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ciConfig = rules.getCIConfig();
|
||||||
|
console.log(`\nCI Config: ${ciConfig.blockingRules.length} blocking rules, ${ciConfig.advisoryRules.length} advisory rules`);
|
||||||
|
|
||||||
|
if (passed) {
|
||||||
|
console.log(`\n${colors.green}Self-test passed!${colors.reset}`);
|
||||||
|
process.exit(0);
|
||||||
|
} else {
|
||||||
|
console.log(`\n${colors.red}Self-test failed!${colors.reset}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CLI entry point
|
||||||
|
if (require.main === module) {
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
|
||||||
|
if (args.includes('--self-test')) {
|
||||||
|
selfTest();
|
||||||
|
} else if (args.includes('--help') || args.length === 0) {
|
||||||
|
console.log(`
|
||||||
|
@dss/rules validator
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
node validate.js [options] <files...>
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--self-test Run self-test to verify rules package
|
||||||
|
--json Output results as JSON
|
||||||
|
--help Show this help message
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
node validate.js src/**/*.tsx
|
||||||
|
node validate.js --self-test
|
||||||
|
node validate.js --json src/components/*.tsx
|
||||||
|
`);
|
||||||
|
} else {
|
||||||
|
const jsonOutput = args.includes('--json');
|
||||||
|
const files = args.filter(a => !a.startsWith('--'));
|
||||||
|
|
||||||
|
const results = validateFiles(files);
|
||||||
|
|
||||||
|
if (jsonOutput) {
|
||||||
|
console.log(JSON.stringify(results, null, 2));
|
||||||
|
} else {
|
||||||
|
printResults(results);
|
||||||
|
}
|
||||||
|
|
||||||
|
process.exit(results.totalErrors > 0 ? 1 : 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
validateFile,
|
||||||
|
validateFiles,
|
||||||
|
getApplicableCategories
|
||||||
|
};
|
||||||
43
packages/dss-rules/package.json
Normal file
43
packages/dss-rules/package.json
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"name": "@dss/rules",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "DSS Design System Rules - Versioned rule definitions for enterprise design system enforcement",
|
||||||
|
"main": "lib/index.js",
|
||||||
|
"types": "lib/index.d.ts",
|
||||||
|
"files": [
|
||||||
|
"lib",
|
||||||
|
"rules",
|
||||||
|
"schemas"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc",
|
||||||
|
"test": "node lib/validate.js --self-test",
|
||||||
|
"prepublishOnly": "npm run build && npm test"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"design-system",
|
||||||
|
"dss",
|
||||||
|
"rules",
|
||||||
|
"tokens",
|
||||||
|
"enterprise"
|
||||||
|
],
|
||||||
|
"author": "DSS Team",
|
||||||
|
"license": "MIT",
|
||||||
|
"devDependencies": {
|
||||||
|
"typescript": "^5.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"ajv": "^8.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
},
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://git.luz.uy/overbits/dss.git",
|
||||||
|
"directory": "packages/dss-rules"
|
||||||
|
}
|
||||||
|
}
|
||||||
111
packages/dss-rules/rules/accessibility.json
Normal file
111
packages/dss-rules/rules/accessibility.json
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../schemas/rule.schema.json",
|
||||||
|
"id": "accessibility",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"name": "Accessibility Rules",
|
||||||
|
"description": "WCAG 2.1 AA compliance rules for accessible design",
|
||||||
|
"category": "accessibility",
|
||||||
|
"severity": "error",
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"id": "images-have-alt",
|
||||||
|
"name": "Images Must Have Alt Text",
|
||||||
|
"description": "All img elements must have meaningful alt text or be marked decorative",
|
||||||
|
"severity": "error",
|
||||||
|
"wcag": "1.1.1",
|
||||||
|
"validation": {
|
||||||
|
"type": "attribute-required",
|
||||||
|
"element": "img",
|
||||||
|
"attribute": "alt",
|
||||||
|
"allowEmpty": true,
|
||||||
|
"emptyMeansDecorative": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "buttons-have-text",
|
||||||
|
"name": "Buttons Must Have Accessible Names",
|
||||||
|
"description": "Button elements must have visible text or aria-label",
|
||||||
|
"severity": "error",
|
||||||
|
"wcag": "4.1.2",
|
||||||
|
"validation": {
|
||||||
|
"type": "accessible-name",
|
||||||
|
"elements": ["button", "[role=button]"],
|
||||||
|
"sources": ["text content", "aria-label", "aria-labelledby"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "form-labels",
|
||||||
|
"name": "Form Inputs Must Have Labels",
|
||||||
|
"description": "All form inputs must be associated with a label",
|
||||||
|
"severity": "error",
|
||||||
|
"wcag": "1.3.1",
|
||||||
|
"validation": {
|
||||||
|
"type": "label-association",
|
||||||
|
"elements": ["input", "select", "textarea"],
|
||||||
|
"methods": ["for/id", "aria-labelledby", "aria-label", "wrapper"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "focus-visible",
|
||||||
|
"name": "Focus Must Be Visible",
|
||||||
|
"description": "Interactive elements must have visible focus indicators",
|
||||||
|
"severity": "error",
|
||||||
|
"wcag": "2.4.7",
|
||||||
|
"validation": {
|
||||||
|
"type": "focus-style",
|
||||||
|
"minContrastRatio": 3.0,
|
||||||
|
"forbiddenPatterns": ["outline: none", "outline: 0", ":focus { outline: none }"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "color-not-only",
|
||||||
|
"name": "Color Not Only Indicator",
|
||||||
|
"description": "Information must not be conveyed by color alone",
|
||||||
|
"severity": "warning",
|
||||||
|
"wcag": "1.4.1",
|
||||||
|
"guidelines": [
|
||||||
|
"Error states need icon + color + text",
|
||||||
|
"Links in text need underline or other indicator",
|
||||||
|
"Status indicators need icon or pattern"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "touch-target-size",
|
||||||
|
"name": "Minimum Touch Target Size",
|
||||||
|
"description": "Interactive elements must be at least 44x44 CSS pixels",
|
||||||
|
"severity": "warning",
|
||||||
|
"wcag": "2.5.5",
|
||||||
|
"validation": {
|
||||||
|
"type": "size-check",
|
||||||
|
"minWidth": 44,
|
||||||
|
"minHeight": 44,
|
||||||
|
"elements": ["button", "a", "[role=button]", "input[type=checkbox]", "input[type=radio]"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "keyboard-navigation",
|
||||||
|
"name": "Keyboard Navigation",
|
||||||
|
"description": "All functionality must be accessible via keyboard",
|
||||||
|
"severity": "error",
|
||||||
|
"wcag": "2.1.1",
|
||||||
|
"validation": {
|
||||||
|
"type": "keyboard-accessible",
|
||||||
|
"requirements": [
|
||||||
|
"All interactive elements focusable",
|
||||||
|
"No keyboard traps",
|
||||||
|
"Logical tab order",
|
||||||
|
"Skip links for navigation"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"compliance": {
|
||||||
|
"level": "AA",
|
||||||
|
"standards": ["WCAG 2.1"],
|
||||||
|
"testingTools": [
|
||||||
|
"axe-core",
|
||||||
|
"pa11y",
|
||||||
|
"lighthouse"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
75
packages/dss-rules/rules/colors.json
Normal file
75
packages/dss-rules/rules/colors.json
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../schemas/rule.schema.json",
|
||||||
|
"id": "colors",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"name": "Color Token Rules",
|
||||||
|
"description": "Rules for color token usage and compliance",
|
||||||
|
"category": "tokens",
|
||||||
|
"severity": "error",
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"id": "no-hardcoded-colors",
|
||||||
|
"name": "No Hardcoded Colors",
|
||||||
|
"description": "All colors must use design tokens, not hardcoded hex/rgb values",
|
||||||
|
"severity": "error",
|
||||||
|
"patterns": {
|
||||||
|
"forbidden": [
|
||||||
|
"#[0-9a-fA-F]{3,8}",
|
||||||
|
"rgb\\([^)]+\\)",
|
||||||
|
"rgba\\([^)]+\\)",
|
||||||
|
"hsl\\([^)]+\\)",
|
||||||
|
"hsla\\([^)]+\\)"
|
||||||
|
],
|
||||||
|
"allowed": [
|
||||||
|
"var\\(--[a-z-]+\\)",
|
||||||
|
"\\$[a-z-]+",
|
||||||
|
"colors\\.[a-z]+",
|
||||||
|
"theme\\.[a-z]+"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"exceptions": [
|
||||||
|
"*.test.*",
|
||||||
|
"*.spec.*",
|
||||||
|
"**/fixtures/**"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "semantic-color-naming",
|
||||||
|
"name": "Semantic Color Naming",
|
||||||
|
"description": "Colors must use semantic names (primary, error) not descriptive (blue, red)",
|
||||||
|
"severity": "warning",
|
||||||
|
"validation": {
|
||||||
|
"type": "naming-convention",
|
||||||
|
"pattern": "^(primary|secondary|accent|success|warning|error|info|neutral|background|foreground|border|muted)(-[a-z]+)?$"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "contrast-ratio",
|
||||||
|
"name": "Minimum Contrast Ratio",
|
||||||
|
"description": "Text/background combinations must meet WCAG AA standards",
|
||||||
|
"severity": "error",
|
||||||
|
"validation": {
|
||||||
|
"type": "contrast-check",
|
||||||
|
"minRatio": 4.5,
|
||||||
|
"largeTextRatio": 3.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tokens": {
|
||||||
|
"required": [
|
||||||
|
"colors.primary",
|
||||||
|
"colors.secondary",
|
||||||
|
"colors.background",
|
||||||
|
"colors.foreground",
|
||||||
|
"colors.border",
|
||||||
|
"colors.error",
|
||||||
|
"colors.success",
|
||||||
|
"colors.warning"
|
||||||
|
],
|
||||||
|
"optional": [
|
||||||
|
"colors.muted",
|
||||||
|
"colors.accent",
|
||||||
|
"colors.info"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
125
packages/dss-rules/rules/components.json
Normal file
125
packages/dss-rules/rules/components.json
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../schemas/rule.schema.json",
|
||||||
|
"id": "components",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"name": "Component Usage Rules",
|
||||||
|
"description": "Rules for component implementation and usage patterns",
|
||||||
|
"category": "components",
|
||||||
|
"severity": "error",
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"id": "use-design-system-components",
|
||||||
|
"name": "Use Design System Components",
|
||||||
|
"description": "Prefer design system components over custom implementations",
|
||||||
|
"severity": "error",
|
||||||
|
"components": {
|
||||||
|
"required": {
|
||||||
|
"Button": {
|
||||||
|
"import": "@dss/components",
|
||||||
|
"forbiddenAlternatives": ["button", "a.btn", "div[role=button]"]
|
||||||
|
},
|
||||||
|
"Input": {
|
||||||
|
"import": "@dss/components",
|
||||||
|
"forbiddenAlternatives": ["input[type=text]", "textarea"]
|
||||||
|
},
|
||||||
|
"Select": {
|
||||||
|
"import": "@dss/components",
|
||||||
|
"forbiddenAlternatives": ["select", "div.dropdown"]
|
||||||
|
},
|
||||||
|
"Card": {
|
||||||
|
"import": "@dss/components",
|
||||||
|
"forbiddenAlternatives": ["div.card", "article"]
|
||||||
|
},
|
||||||
|
"Modal": {
|
||||||
|
"import": "@dss/components",
|
||||||
|
"forbiddenAlternatives": ["div.modal", "dialog"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "component-prop-validation",
|
||||||
|
"name": "Required Component Props",
|
||||||
|
"description": "Components must include required accessibility and functionality props",
|
||||||
|
"severity": "error",
|
||||||
|
"validation": {
|
||||||
|
"Button": {
|
||||||
|
"requiredProps": ["variant", "size"],
|
||||||
|
"conditionalProps": {
|
||||||
|
"loading": ["loadingText"],
|
||||||
|
"icon": ["aria-label"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Input": {
|
||||||
|
"requiredProps": ["label", "name"],
|
||||||
|
"conditionalProps": {
|
||||||
|
"error": ["errorMessage"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Modal": {
|
||||||
|
"requiredProps": ["title", "onClose"],
|
||||||
|
"recommendedProps": ["aria-describedby"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "component-composition",
|
||||||
|
"name": "Component Composition Patterns",
|
||||||
|
"description": "Follow recommended composition patterns for complex UIs",
|
||||||
|
"severity": "info",
|
||||||
|
"patterns": {
|
||||||
|
"forms": {
|
||||||
|
"structure": ["Form", "FormField", "Input/Select", "Button"],
|
||||||
|
"guidelines": [
|
||||||
|
"Wrap inputs in FormField for consistent labeling",
|
||||||
|
"Use Form component for validation handling",
|
||||||
|
"Place submit button inside Form"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"lists": {
|
||||||
|
"structure": ["List", "ListItem"],
|
||||||
|
"guidelines": [
|
||||||
|
"Use semantic list components for accessibility",
|
||||||
|
"Implement virtualization for 50+ items"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"navigation": {
|
||||||
|
"structure": ["Nav", "NavItem", "NavLink"],
|
||||||
|
"guidelines": [
|
||||||
|
"Use Nav component for main navigation",
|
||||||
|
"Implement active state handling"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "no-inline-styles",
|
||||||
|
"name": "No Inline Styles on Components",
|
||||||
|
"description": "Components should use className/variant props, not style attribute",
|
||||||
|
"severity": "warning",
|
||||||
|
"patterns": {
|
||||||
|
"forbidden": [
|
||||||
|
"style={{",
|
||||||
|
"style={{"
|
||||||
|
],
|
||||||
|
"exceptions": [
|
||||||
|
"dynamic positioning",
|
||||||
|
"animations",
|
||||||
|
"calculated values"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"adoption": {
|
||||||
|
"thresholds": {
|
||||||
|
"minimum": 60,
|
||||||
|
"target": 80,
|
||||||
|
"excellent": 95
|
||||||
|
},
|
||||||
|
"metrics": [
|
||||||
|
"percentage_using_ds_components",
|
||||||
|
"custom_component_count",
|
||||||
|
"token_compliance_rate"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
76
packages/dss-rules/rules/spacing.json
Normal file
76
packages/dss-rules/rules/spacing.json
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../schemas/rule.schema.json",
|
||||||
|
"id": "spacing",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"name": "Spacing Token Rules",
|
||||||
|
"description": "Rules for consistent spacing using design tokens",
|
||||||
|
"category": "tokens",
|
||||||
|
"severity": "warning",
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"id": "no-arbitrary-spacing",
|
||||||
|
"name": "No Arbitrary Spacing Values",
|
||||||
|
"description": "Spacing must use token scale (4px increments), not arbitrary values",
|
||||||
|
"severity": "warning",
|
||||||
|
"patterns": {
|
||||||
|
"forbidden": [
|
||||||
|
"margin:\\s*[0-9]+px",
|
||||||
|
"padding:\\s*[0-9]+px",
|
||||||
|
"gap:\\s*[0-9]+px"
|
||||||
|
],
|
||||||
|
"allowed": [
|
||||||
|
"var\\(--spacing-[a-z0-9]+\\)",
|
||||||
|
"\\$spacing-[a-z0-9]+",
|
||||||
|
"spacing\\.[a-z0-9]+"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"exceptions": [
|
||||||
|
"0",
|
||||||
|
"0px",
|
||||||
|
"auto",
|
||||||
|
"inherit"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "spacing-scale",
|
||||||
|
"name": "Use Spacing Scale",
|
||||||
|
"description": "Only use values from the defined spacing scale",
|
||||||
|
"severity": "error",
|
||||||
|
"validation": {
|
||||||
|
"type": "scale-check",
|
||||||
|
"allowedValues": [0, 4, 8, 12, 16, 20, 24, 32, 40, 48, 64, 80, 96, 128]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "consistent-component-spacing",
|
||||||
|
"name": "Component Internal Spacing",
|
||||||
|
"description": "Components should use consistent internal spacing patterns",
|
||||||
|
"severity": "info",
|
||||||
|
"guidelines": [
|
||||||
|
"Use spacing.xs (4px) for tight groupings",
|
||||||
|
"Use spacing.sm (8px) for related elements",
|
||||||
|
"Use spacing.md (16px) for section separation",
|
||||||
|
"Use spacing.lg (24px) for major sections",
|
||||||
|
"Use spacing.xl (32px+) for page-level separation"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tokens": {
|
||||||
|
"required": [
|
||||||
|
"spacing.xs",
|
||||||
|
"spacing.sm",
|
||||||
|
"spacing.md",
|
||||||
|
"spacing.lg",
|
||||||
|
"spacing.xl"
|
||||||
|
],
|
||||||
|
"scale": {
|
||||||
|
"xs": "4px",
|
||||||
|
"sm": "8px",
|
||||||
|
"md": "16px",
|
||||||
|
"lg": "24px",
|
||||||
|
"xl": "32px",
|
||||||
|
"2xl": "48px",
|
||||||
|
"3xl": "64px"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
75
packages/dss-rules/rules/typography.json
Normal file
75
packages/dss-rules/rules/typography.json
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../schemas/rule.schema.json",
|
||||||
|
"id": "typography",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"name": "Typography Rules",
|
||||||
|
"description": "Rules for consistent typography using design tokens",
|
||||||
|
"category": "tokens",
|
||||||
|
"severity": "warning",
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"id": "use-typography-scale",
|
||||||
|
"name": "Use Typography Scale",
|
||||||
|
"description": "Font sizes must use the defined typography scale",
|
||||||
|
"severity": "error",
|
||||||
|
"patterns": {
|
||||||
|
"forbidden": [
|
||||||
|
"font-size:\\s*[0-9]+px",
|
||||||
|
"fontSize:\\s*[0-9]+"
|
||||||
|
],
|
||||||
|
"allowed": [
|
||||||
|
"var\\(--font-size-[a-z]+\\)",
|
||||||
|
"\\$font-size-[a-z]+",
|
||||||
|
"typography\\.[a-z]+"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "semantic-text-elements",
|
||||||
|
"name": "Use Semantic Text Elements",
|
||||||
|
"description": "Use appropriate heading levels and text components",
|
||||||
|
"severity": "warning",
|
||||||
|
"validation": {
|
||||||
|
"headingOrder": true,
|
||||||
|
"maxH1PerPage": 1,
|
||||||
|
"noSkippedLevels": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "line-height-consistency",
|
||||||
|
"name": "Consistent Line Heights",
|
||||||
|
"description": "Line heights should match the typography scale",
|
||||||
|
"severity": "info",
|
||||||
|
"guidelines": [
|
||||||
|
"Use lineHeight.tight (1.25) for headings",
|
||||||
|
"Use lineHeight.normal (1.5) for body text",
|
||||||
|
"Use lineHeight.relaxed (1.75) for long-form content"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tokens": {
|
||||||
|
"required": [
|
||||||
|
"typography.h1",
|
||||||
|
"typography.h2",
|
||||||
|
"typography.h3",
|
||||||
|
"typography.body",
|
||||||
|
"typography.small",
|
||||||
|
"typography.caption"
|
||||||
|
],
|
||||||
|
"scale": {
|
||||||
|
"xs": "12px",
|
||||||
|
"sm": "14px",
|
||||||
|
"base": "16px",
|
||||||
|
"lg": "18px",
|
||||||
|
"xl": "20px",
|
||||||
|
"2xl": "24px",
|
||||||
|
"3xl": "30px",
|
||||||
|
"4xl": "36px",
|
||||||
|
"5xl": "48px"
|
||||||
|
},
|
||||||
|
"fontFamilies": {
|
||||||
|
"sans": "Inter, system-ui, sans-serif",
|
||||||
|
"mono": "JetBrains Mono, monospace"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
143
packages/dss-rules/schemas/rule.schema.json
Normal file
143
packages/dss-rules/schemas/rule.schema.json
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"$id": "https://dss.overbits.luz.uy/schemas/rule.schema.json",
|
||||||
|
"title": "DSS Rule Definition",
|
||||||
|
"description": "Schema for defining DSS design system rules",
|
||||||
|
"type": "object",
|
||||||
|
"required": ["id", "version", "name", "category", "rules"],
|
||||||
|
"properties": {
|
||||||
|
"$schema": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Reference to this schema"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[a-z][a-z0-9-]*$",
|
||||||
|
"description": "Unique identifier for this rule set"
|
||||||
|
},
|
||||||
|
"version": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$",
|
||||||
|
"description": "Semantic version of this rule set"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Human-readable name"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Detailed description of the rule set"
|
||||||
|
},
|
||||||
|
"category": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["tokens", "components", "accessibility", "patterns", "naming"],
|
||||||
|
"description": "Category this rule set belongs to"
|
||||||
|
},
|
||||||
|
"severity": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["error", "warning", "info"],
|
||||||
|
"default": "warning",
|
||||||
|
"description": "Default severity for rules in this set"
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/Rule"
|
||||||
|
},
|
||||||
|
"description": "Individual rules in this set"
|
||||||
|
},
|
||||||
|
"tokens": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Token requirements and definitions",
|
||||||
|
"properties": {
|
||||||
|
"required": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "type": "string" }
|
||||||
|
},
|
||||||
|
"optional": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "type": "string" }
|
||||||
|
},
|
||||||
|
"scale": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": { "type": "string" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"components": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Component requirements"
|
||||||
|
},
|
||||||
|
"compliance": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Compliance metadata"
|
||||||
|
},
|
||||||
|
"adoption": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Adoption threshold definitions"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"definitions": {
|
||||||
|
"Rule": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["id", "name"],
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[a-z][a-z0-9-]*$",
|
||||||
|
"description": "Unique rule identifier"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Human-readable rule name"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "What this rule checks for"
|
||||||
|
},
|
||||||
|
"severity": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["error", "warning", "info"],
|
||||||
|
"description": "Rule severity (overrides set default)"
|
||||||
|
},
|
||||||
|
"wcag": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "WCAG criterion reference if applicable"
|
||||||
|
},
|
||||||
|
"patterns": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"forbidden": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "type": "string" },
|
||||||
|
"description": "Regex patterns that violate this rule"
|
||||||
|
},
|
||||||
|
"allowed": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "type": "string" },
|
||||||
|
"description": "Regex patterns that satisfy this rule"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"validation": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Validation configuration"
|
||||||
|
},
|
||||||
|
"exceptions": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "type": "string" },
|
||||||
|
"description": "File patterns or values to exclude"
|
||||||
|
},
|
||||||
|
"guidelines": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "type": "string" },
|
||||||
|
"description": "Human-readable guidelines for this rule"
|
||||||
|
},
|
||||||
|
"components": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Component-specific rule configuration"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -86,7 +86,7 @@ def test_figma_component_extraction():
|
|||||||
for component in components:
|
for component in components:
|
||||||
if component.name == "Card":
|
if component.name == "Card":
|
||||||
card_component_found = True
|
card_component_found = True
|
||||||
assert component.classification == AtomicType.MOLECULE
|
assert component.classification == AtomicType.COMPOSITE_COMPONENT
|
||||||
assert component.sub_components # should not be empty
|
assert component.sub_components # should not be empty
|
||||||
assert len(component.sub_components) == 1 # Card has one child
|
assert len(component.sub_components) == 1 # Card has one child
|
||||||
assert component.figma_node_id == "1:3"
|
assert component.figma_node_id == "1:3"
|
||||||
|
|||||||
Reference in New Issue
Block a user