learning_ai_common_plat/packages/design-tokens/scripts/token-coverage.cjs
Saravana Achu Mac 8f541c9f87 chore(audit): unblock workspace lint pipeline + 13 mechanical fixes
The first `pnpm -r exec eslint .` run was bailing at the very first
package (design-tokens), hiding any lint state in the rest of the 69
workspace packages. This commit fixes the structural blockers so the
pipeline runs end-to-end, then sweeps the small, low-risk lint errors
in the next 4 packages it surfaces. Real lint debt that remains
(85 errors, mostly @typescript-eslint/no-unused-vars across many
unrelated packages) is cataloged in docs/AUDIT_PLATFORM.md for follow-
up by package owners.

Structural fixes (eslint config):
  - eslint.config.js (root):
      • New flat-config block for **/*.cjs and **/scripts/**/*.{js,cjs}
        with Node globals (process, console, require, module, __dirname)
        and no-console disabled. CLI scripts legitimately print to
        stdout. This alone clears the 45 errors in design-tokens'
        validate-tokens.cjs.
      • Added XMLHttpRequest + ProgressEvent to browser globals so
        feedback-client compiles.
  - packages/ui/eslint.config.js:
      • Added @typescript-eslint/parser — the package-local override
        replaced (didn't merge with) the root config, so TS syntax was
        being parsed by espree and erroring on every `interface` /
        type import.
      • Added ignores for dist/** (root's ignores aren't inherited).
      • Extended the files glob to .storybook/**/*.{ts,tsx}.

Mechanical lint fixes (no behaviour change):
  - design-tokens/scripts/{validate,token-coverage}.cjs: empty catch
    binding (catch (e) → catch).
  - feedback-client/src/index.ts:
      • captureScreen(): preserve caught error via `{ cause: err }`
        on the rethrown Error (preserve-caught-error rule, real bug —
        previous chain dropped the original stack).
      • captureElement(): rename unused parity params mimeType/quality
        to _mimeType/_quality and document why they exist.
  - logger/__tests__/logger.test.ts: drop unused `LoggerConfig` import.
  - extraction-service/{lib/circuit-breaker,modules/extract/{sidecar-
    monitor,usage}}.test.ts: drop 3 unused vitest/type imports.
  - tracker-web/__tests__/tracker-proxy.test.ts: rename unused local
    `url` → `_url`.

New: docs/AUDIT_PLATFORM.md
  Tooling-backed audit summary (pnpm install / typecheck / test / lint
  results), classification of remaining lint debt by rule, and an
  ordered hand-off plan for package owners to clear the rest with
  `pnpm --filter <pkg> lint:fix` followed by an eyeball review.

Verified before commit:
  - `pnpm typecheck` → pass (all 69 packages compile)
  - `pnpm test`      → pass (~2,200 tests across 18+ suites)
  - `pnpm lint`      → 85 pre-existing errors surfaced (none introduced
    by this commit; all in unrelated packages — see AUDIT_PLATFORM.md
    section P).

Out of scope (left untouched in working tree):
  - In-progress nomgap-on-Vercel migration: docker-compose.ecosystem.yml,
    products/nomgap/product.json, services/platform-service/src/
    modules/flags/seed.ts.
  - pnpm-lock.yaml: my `pnpm install -r` regenerated it (+2.9k/-8.5k
    lines) — not part of the audit, owner should commit deliberately.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 14:21:34 -07:00

185 lines
5.3 KiB
JavaScript
Executable File

#!/usr/bin/env node
/**
* Token coverage report — analyzes how well a product uses design tokens
*
* Usage: node scripts/token-coverage.js <product-path>
*/
const { readFileSync, readdirSync, statSync } = require('fs');
const { join, resolve } = require('path');
const EXCLUDED_DIRS = ['node_modules', 'dist', 'build', '.git', 'generated', '__mocks__'];
const INCLUDED_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.swift', '.kt'];
const TOKEN_PATTERNS = {
web: {
cssVar: /--ml-[a-z-]+/g,
tokensImport: /@bytelyst\/design-tokens/,
},
ios: {
mindLystColors: /MindLystColors\./g,
colorExtension: /Color\(hex:/g,
},
kmp: {
mindLystTokens: /MindLystTokens\./g,
},
};
function findFiles(dir, files = []) {
try {
const items = readdirSync(dir);
for (const item of items) {
const fullPath = join(dir, item);
if (EXCLUDED_DIRS.some(ex => fullPath.includes(ex))) continue;
const stat = statSync(fullPath);
if (stat.isDirectory()) {
findFiles(fullPath, files);
} else if (INCLUDED_EXTENSIONS.some(ext => item.endsWith(ext))) {
files.push(fullPath);
}
}
} catch {
// Directory might not exist
}
return files;
}
function detectPlatform(files) {
const hasSwift = files.some(f => f.endsWith('.swift'));
const hasKotlin = files.some(f => f.endsWith('.kt'));
const hasTSX = files.some(f => f.endsWith('.tsx'));
if (hasSwift) return 'ios';
if (hasKotlin) return 'kmp';
if (hasTSX) return 'web';
return 'unknown';
}
function analyzeCoverage(files, platform) {
let tokenUsages = 0;
let hardcodedColors = 0;
let filesUsingTokens = 0;
let filesWithHardcoded = 0;
const patterns = TOKEN_PATTERNS[platform] || {};
for (const file of files) {
const content = readFileSync(file, 'utf-8');
let hasTokens = false;
let hasHardcoded = false;
// Check token usage
if (patterns.cssVar) {
const matches = content.match(patterns.cssVar);
if (matches) {
tokenUsages += matches.length;
hasTokens = true;
}
}
if (patterns.mindLystColors) {
const matches = content.match(patterns.mindLystColors);
if (matches) {
tokenUsages += matches.length;
hasTokens = true;
}
}
if (patterns.mindLystTokens) {
const matches = content.match(patterns.mindLystTokens);
if (matches) {
tokenUsages += matches.length;
hasTokens = true;
}
}
// Check hardcoded colors
const colorMatches = content.match(/#[0-9A-Fa-f]{6}\b/g);
if (colorMatches) {
// Filter out likely non-color hex (like #FFFFFF in different contexts)
const likelyColors = colorMatches.filter(c => {
const hex = c.slice(1);
// Skip pure grays that might be intentional
if (hex[0] === hex[2] && hex[2] === hex[4]) return false;
return true;
});
if (likelyColors.length > 0) {
hardcodedColors += likelyColors.length;
hasHardcoded = true;
}
}
if (hasTokens) filesUsingTokens++;
if (hasHardcoded) filesWithHardcoded++;
}
return {
tokenUsages,
hardcodedColors,
filesUsingTokens,
filesWithHardcoded,
totalFiles: files.length,
};
}
function main() {
const targetPath = process.argv[2];
if (!targetPath) {
console.log('Usage: node scripts/token-coverage.js <product-path>');
console.log('');
console.log('Examples:');
console.log(' node scripts/token-coverage.js ../../mindlyst-native/iosApp');
console.log(' node scripts/token-coverage.js ../../learning_ai_clock/web/src');
process.exit(1);
}
const absolutePath = resolve(targetPath);
console.log(`📊 Analyzing token coverage for ${absolutePath}\n`);
const files = findFiles(absolutePath);
const platform = detectPlatform(files);
console.log(`Detected platform: ${platform}`);
console.log(`Total files: ${files.length}\n`);
if (files.length === 0) {
console.log('❌ No source files found');
process.exit(1);
}
const coverage = analyzeCoverage(files, platform);
console.log(`${'='.repeat(50)}`);
console.log('📈 Coverage Report');
console.log(`${'='.repeat(50)}`);
console.log(
`Files using tokens: ${coverage.filesUsingTokens}/${coverage.totalFiles} (${((coverage.filesUsingTokens / coverage.totalFiles) * 100).toFixed(1)}%)`
);
console.log(
`Files with hardcoded: ${coverage.filesWithHardcoded}/${coverage.totalFiles} (${((coverage.filesWithHardcoded / coverage.totalFiles) * 100).toFixed(1)}%)`
);
console.log(`Token usages: ${coverage.tokenUsages}`);
console.log(`Hardcoded colors: ${coverage.hardcodedColors}`);
console.log(`${'='.repeat(50)}`);
const tokenRatio = coverage.tokenUsages + coverage.hardcodedColors;
const tokenPercentage =
tokenRatio > 0 ? ((coverage.tokenUsages / tokenRatio) * 100).toFixed(1) : 'N/A';
console.log(`\n🎯 Token Adoption: ${tokenPercentage}%`);
if (coverage.hardcodedColors > 0) {
console.log(`\n⚠️ Found ${coverage.hardcodedColors} hardcoded colors.`);
console.log(' Run validate-tokens.js for details.');
}
if (coverage.filesUsingTokens === 0) {
console.log('\n❌ No token usage detected. Product needs token integration.');
process.exit(1);
}
console.log('\n✅ Coverage analysis complete.');
}
main();