185 lines
5.3 KiB
JavaScript
Executable File
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 (e) {
|
|
// 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();
|