All files / packages/design-tokens/src/validators dtcg-compliance.js

85.71% Statements 42/49
87.5% Branches 28/32
100% Functions 3/3
85.1% Lines 40/47

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 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 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120                                    1x                       44x 44x   40x 54x 54x 10x               54x 54x 25x     40x                 7x 7x 7x   7x 7x 7x 7x 7x 7x     7x                   3x 3x   3x 3x   3x               3x 1x 1x 1x   2x 2x 2x 2x   2x 2x 2x     3x       1x 1x          
#!/usr/bin/env node
/**
 * DTCG Compliance Validator
 *
 * Scans token source JSON files and fails if any token uses non-DTCG property
 * names where a DTCG equivalent exists (e.g. "deprecated" instead of "$deprecated").
 *
 * Usage:
 *   node src/validators/dtcg-compliance.js             # Summary only
 *   node src/validators/dtcg-compliance.js --verbose   # Detailed issues with suggestions
 *   node src/validators/dtcg-compliance.js --report    # Full table of all scanned files
 */
 
import fs from 'fs';
import path from 'node:path';
import { glob } from 'glob';
import { parseValidatorArgs } from './validator-utils.js';
 
const DTCG_VIOLATIONS = new Map([
  ['deprecated', '$deprecated'],
  ['value', '$value'],
  ['type', '$type'],
  ['description', '$description'],
]);
 
/**
 * Recursively walk a parsed token tree looking for non-DTCG keys
 * where a $-prefixed equivalent exists.
 */
export function findViolations(obj, filePath, currentPath = []) {
  const violations = [];
  if (!obj || typeof obj !== 'object' || Array.isArray(obj)) return violations;
 
  for (const [key, value] of Object.entries(obj)) {
    const dtcgEquivalent = DTCG_VIOLATIONS.get(key);
    if (dtcgEquivalent && !obj[dtcgEquivalent]) {
      violations.push({
        file: filePath,
        path: [...currentPath, key].join('.'),
        key,
        expected: dtcgEquivalent,
      });
    }
 
    const isValuePayload = key === '$value' || key === 'original';
    if (typeof value === 'object' && value !== null && !Array.isArray(value) && !isValuePayload) {
      violations.push(...findViolations(value, filePath, [...currentPath, key]));
    }
  }
  return violations;
}
 
/**
 * Scan all token JSON files for DTCG compliance violations.
 * @param {string} tokensDir - Absolute path to the tokens directory
 * @returns {{ violations: Array, filesScanned: number, fileResults: Array }}
 */
export function validateDtcgCompliance(tokensDir) {
  const files = glob.sync('**/*.json', { cwd: tokensDir, absolute: true });
  const allViolations = [];
  const fileResults = [];
 
  for (const file of files) {
    const content = JSON.parse(fs.readFileSync(file, 'utf8'));
    const relativePath = path.relative(tokensDir, file);
    const fileViolations = findViolations(content, relativePath);
    allViolations.push(...fileViolations);
    fileResults.push({ file: relativePath, violationCount: fileViolations.length });
  }
 
  return { violations: allViolations, filesScanned: files.length, fileResults };
}
 
/**
 * Run the validator and print results.
 * @param {string} tokensDir - Absolute path to the tokens directory
 * @param {{ isVerbose?: boolean, isReport?: boolean, exitFn?: Function }} options
 * @returns {{ violations: Array, filesScanned: number }}
 */
export function run(tokensDir, { isVerbose = false, isReport = false, exitFn = process.exit } = {}) {
  console.log(`\nDTCG Compliance Validation`);
  console.log('─'.repeat(60));
 
  const { violations, filesScanned, fileResults } = validateDtcgCompliance(tokensDir);
  console.log(`Scanned ${filesScanned} token files.\n`);
 
  Iif (isReport) {
    for (const { file, violationCount } of fileResults) {
      const status = violationCount === 0 ? '✅' : `❌ ${violationCount} violation(s)`;
      console.log(`  ${file} — ${status}`);
    }
    console.log('');
  }
 
  if (violations.length === 0) {
    console.log('─'.repeat(60));
    console.log('✅ All tokens use DTCG-compliant property names.\n');
    exitFn(0);
  } else {
    console.log(`❌ Found ${violations.length} non-DTCG property name(s):\n`);
    for (const v of violations) {
      console.log(`  ${v.file} → "${v.key}" at ${v.path}`);
      if (isVerbose) console.log(`    Use "${v.expected}" instead.\n`);
    }
    console.log('─'.repeat(60));
    console.log(`❌ Validation failed: ${violations.length} violation(s) found.\n`);
    exitFn(1);
  }
 
  return { violations, filesScanned };
}
 
// CLI entry point
const isCli = import.meta.url === `file://${process.argv[1]}`;
Iif (isCli) {
  const { isVerbose, isReport } = parseValidatorArgs();
  const tokensDir = path.resolve(process.cwd(), 'src/tokens');
  run(tokensDir, { isVerbose, isReport });
}