Ctrl K
ComunidadGuías…

scripts/check-design-token-usage.mjs

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========

/**
 * Fails if UI source contains hard-coded colors instead of design tokens
 * (CSS variables such as var(--ds-...), var(--colors-...), component vars, or
 * Tailwind classes that map to those vars — not raw #hex / rgb() / hsl()).
 *
 * Usage:
 *   node scripts/check-design-token-usage.mjs
 *   node scripts/check-design-token-usage.mjs src/a.tsx   # lint-staged (one or more files)
 *
 * Exemptions:
 *   - End-of-line comment: // ds:allow-hardcoded-color
 *   - scripts/design-token-usage.allowlist — repo-relative paths, one per line (# comments ok)
 */

import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
import { dirname, join, relative, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';

const __dirname = dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = resolve(__dirname, '..');

const EXT = new Set(['.ts', '.tsx', '.js', '.jsx']);

const SKIP_PREFIXES = ['src/lib/themeTokens/', 'docs/'];

const SKIP_FILE_RE =
  /\.(test|spec)\.(ts|tsx|js|jsx)$|vite-env\.d\.ts$|\.stories\.(ts|tsx)$/;

const HEX_RE = /#([0-9a-fA-F]{8}|[0-9a-fA-F]{6}|[0-9a-fA-F]{3})\b/g;

const RGB_NUM_RE = /\brgba?\(\s*[\d.]/;
const HSL_NUM_RE = /\bhsla?\(\s*[\d.]/;

const ARBITRARY_HEX_RE =
  /\[[^\]]*#([0-9a-fA-F]{8}|[0-9a-fA-F]{6}|[0-9a-fA-F]{3})\b[^\]]*\]/;
const ARBITRARY_RGB_RE = /\[[^\]]*rgba?\([^\]]*\]/;

function loadAllowlist() {
  const path = join(REPO_ROOT, 'scripts/design-token-usage.allowlist');
  const set = new Set();
  if (!existsSync(path)) return set;
  const raw = readFileSync(path, 'utf8');
  for (const line of raw.split('\n')) {
    const t = line.trim();
    if (!t || t.startsWith('#')) continue;
    set.add(t.replaceAll('\\', '/'));
  }
  return set;
}

function shouldSkipPath(relPosix, allowlist) {
  const norm = relPosix.replaceAll('\\', '/');
  if (allowlist.has(norm)) return true;
  for (const prefix of SKIP_PREFIXES) {
    if (norm.startsWith(prefix)) return true;
  }
  if (SKIP_FILE_RE.test(norm)) return true;
  return false;
}

function* walkSrcFiles(dir) {
  const entries = readdirSync(dir, { withFileTypes: true });
  for (const e of entries) {
    const p = join(dir, e.name);
    if (e.isDirectory()) {
      if (e.name === 'node_modules' || e.name === 'dist') continue;
      yield* walkSrcFiles(p);
    } else {
      const ext = e.name.slice(e.name.lastIndexOf('.'));
      if (EXT.has(ext)) yield p;
    }
  }
}

function stripTrailingLineComment(line) {
  const idx = line.indexOf('//');
  if (idx === -1) return line;
  return line.slice(0, idx);
}

function lineHasExemption(line) {
  return line.includes('//') && line.includes('ds:allow-hardcoded-color');
}

function checkLine(rawLine, lineNum, fileRel, out) {
  if (lineHasExemption(rawLine)) return;
  const line = stripTrailingLineComment(rawLine);

  if (RGB_NUM_RE.test(line) || HSL_NUM_RE.test(line)) {
    out.push({
      file: fileRel,
      line: lineNum,
      message:
        'Use design tokens (e.g. var(--ds-...), Tailwind semantic colors) instead of raw rgb/hsl.',
      snippet: rawLine.trim(),
    });
    return;
  }

  HEX_RE.lastIndex = 0;
  let m;
  while ((m = HEX_RE.exec(line)) !== null) {
    const start = m.index;
    const before = line.slice(Math.max(0, start - 4), start);
    if (start > 0 && line[start - 1] === '&') continue;
    if (/url\s*\(\s*$/i.test(before)) continue;
    out.push({
      file: fileRel,
      line: lineNum,
      message: `Hard-coded hex "${m[0]}" — use a design token or Tailwind color that maps to var(--...).`,
      snippet: rawLine.trim(),
    });
    return;
  }

  if (ARBITRARY_HEX_RE.test(line) || ARBITRARY_RGB_RE.test(line)) {
    out.push({
      file: fileRel,
      line: lineNum,
      message:
        'Tailwind arbitrary color value ([#...] / [rgb(...)]) — use ds-* or semantic utilities.',
      snippet: rawLine.trim(),
    });
  }
}

function checkFile(absPath, allowlist) {
  const rel = relative(REPO_ROOT, absPath);
  const relPosix = rel.replaceAll('\\', '/');
  if (shouldSkipPath(relPosix, allowlist)) return [];

  const text = readFileSync(absPath, 'utf8');
  const lines = text.split(/\r?\n/);
  const out = [];
  lines.forEach((ln, i) => checkLine(ln, i + 1, relPosix, out));
  return out;
}

function resolveCliFiles(argv) {
  const files = [];
  for (const a of argv) {
    if (a.startsWith('-')) continue;
    const abs = resolve(REPO_ROOT, a);
    if (existsSync(abs) && statSync(abs).isFile()) files.push(abs);
  }
  return files;
}

function main() {
  const allowlist = loadAllowlist();
  const argv = process.argv.slice(2).filter((a) => a !== '--');
  const explicit = resolveCliFiles(argv);

  const targets =
    explicit.length > 0 ? explicit : [...walkSrcFiles(join(REPO_ROOT, 'src'))];

  const violations = [];
  for (const abs of targets) {
    violations.push(...checkFile(abs, allowlist));
  }

  if (violations.length === 0) {
    console.log('check-design-token-usage: OK (no hard-coded colors found).');
    process.exit(0);
  }

  console.error('check-design-token-usage: hard-coded colors detected:\n');
  for (const v of violations) {
    console.error(`  ${v.file}:${v.line}`);
    console.error(`    ${v.message}`);
    console.error(
      `    ${v.snippet.slice(0, 200)}${v.snippet.length > 200 ? '…' : ''}\n`
    );
  }
  console.error(
    `Total: ${violations.length} finding(s). Fix or add // ds:allow-hardcoded-color on the line, or list the file in scripts/design-token-usage.allowlist (one path per line).`
  );
  process.exit(1);
}

main();