fix(tests): Correct Figma ingest test
This commit is contained in:
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
|
||||
};
|
||||
Reference in New Issue
Block a user