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>
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 {
|
|
// 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();
|