- validate-tokens.js: Scan source files for hardcoded colors - token-coverage.js: Report token adoption percentage per product - generate-react-native.ts: Generator for Expo/NomGap StyleSheet tokens All scripts include: - Cross-platform file detection (.ts, .tsx, .swift, .kt) - Product-specific token categorization - Exit codes for CI integration
144 lines
6.2 KiB
TypeScript
144 lines
6.2 KiB
TypeScript
/**
|
|
* React Native token generator for Expo/NomGap
|
|
* Reads bytelyst.tokens.json, outputs RN StyleSheet-compatible tokens
|
|
*
|
|
* Usage: tsx scripts/generate-react-native.ts
|
|
*/
|
|
|
|
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
import { dirname, resolve } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
const tokensPath = resolve(__dirname, '../tokens/bytelyst.tokens.json');
|
|
const outDir = resolve(__dirname, '../generated/react-native');
|
|
|
|
mkdirSync(outDir, { recursive: true });
|
|
|
|
const tokens = JSON.parse(readFileSync(tokensPath, 'utf-8'));
|
|
|
|
// ── Helpers ───────────────────────────────────────────────────────────
|
|
|
|
function generateReactNative(): string {
|
|
const lines: string[] = [
|
|
'/**',
|
|
' * React Native Design Tokens — Auto-generated from bytelyst.tokens.json',
|
|
' * Do not edit manually. Run: tsx scripts/generate-react-native.ts',
|
|
' */',
|
|
'',
|
|
'export const tokens = {',
|
|
'',
|
|
' // ── Semantic Colors (Dark Theme) ──────────────────────────────────',
|
|
' colors: {',
|
|
];
|
|
|
|
// Semantic dark colors
|
|
for (const [key, value] of Object.entries(tokens.color.semantic.dark)) {
|
|
if (typeof value === 'string' && value.startsWith('#')) {
|
|
lines.push(` ${key}: '${value}',`);
|
|
} else if (typeof value === 'string' && value.startsWith('rgba')) {
|
|
// Convert rgba to hex8 for React Native
|
|
const match = value.match(/rgba?\((\d+),\s*(\d+),\s*(\d+),\s*([\d.]+)\)/);
|
|
if (match) {
|
|
const [, r, g, b, a] = match;
|
|
const alpha = Math.round(parseFloat(a) * 255)
|
|
.toString(16)
|
|
.padStart(2, '0');
|
|
const hex =
|
|
`#${alpha}${parseInt(r).toString(16).padStart(2, '0')}${parseInt(g).toString(16).padStart(2, '0')}${parseInt(b).toString(16).padStart(2, '0')}`.toUpperCase();
|
|
lines.push(` ${key}: '${hex}',`);
|
|
}
|
|
}
|
|
}
|
|
lines.push(' },', '');
|
|
|
|
// NomGap-specific colors
|
|
lines.push(' // ── NomGap Product Colors ──────────────────────────────────────────');
|
|
lines.push(' nomgap: {');
|
|
for (const [key, value] of Object.entries(tokens.color.nomgap)) {
|
|
lines.push(` ${key}: '${value}',`);
|
|
}
|
|
lines.push(' },', '');
|
|
|
|
// Spacing
|
|
lines.push(' // ── Spacing (8pt grid) ──────────────────────────────────────────────');
|
|
lines.push(' spacing: {');
|
|
for (const [key, value] of Object.entries(tokens.spacing)) {
|
|
lines.push(` ${key}: ${value},`);
|
|
}
|
|
lines.push(' },', '');
|
|
|
|
// Radius
|
|
lines.push(' // ── Border Radius ───────────────────────────────────────────────────');
|
|
lines.push(' radius: {');
|
|
for (const [key, value] of Object.entries(tokens.radius)) {
|
|
lines.push(` ${key}: ${value},`);
|
|
}
|
|
lines.push(' },', '');
|
|
|
|
// Typography
|
|
lines.push(' // ── Typography ─────────────────────────────────────────────────────');
|
|
lines.push(' typography: {');
|
|
lines.push(' fontFamily: {');
|
|
for (const key of Object.keys(tokens.typography.fontFamily)) {
|
|
// Use system fonts for React Native
|
|
const systemFont = key === 'display' ? 'System' : key === 'mono' ? 'Courier' : 'System';
|
|
lines.push(` ${key}: '${systemFont}',`);
|
|
}
|
|
lines.push(' },');
|
|
lines.push(' fontSize: {');
|
|
for (const [key, value] of Object.entries(tokens.typography.fontSize)) {
|
|
lines.push(` ${key}: ${value},`);
|
|
}
|
|
lines.push(' },');
|
|
lines.push(' fontWeight: {');
|
|
for (const [key, value] of Object.entries(tokens.typography.fontWeight)) {
|
|
lines.push(` ${key}: '${value}',`);
|
|
}
|
|
lines.push(' },');
|
|
lines.push(' },', '');
|
|
|
|
// Icon sizes
|
|
lines.push(' // ── Icon Sizes ──────────────────────────────────────────────────────');
|
|
lines.push(' icon: {');
|
|
for (const [key, value] of Object.entries(tokens.icon)) {
|
|
lines.push(` ${key}: ${value},`);
|
|
}
|
|
lines.push(' },', '');
|
|
|
|
// Z-index (for RN zIndex style prop)
|
|
lines.push(' // ── Z-Index Layers ───────────────────────────────────────────────────');
|
|
lines.push(' zIndex: {');
|
|
for (const [key, value] of Object.entries(tokens.zIndex)) {
|
|
lines.push(` ${key}: ${value},`);
|
|
}
|
|
lines.push(' },', '');
|
|
|
|
// Opacity
|
|
lines.push(' // ── Opacity ─────────────────────────────────────────────────────────');
|
|
lines.push(' opacity: {');
|
|
for (const [key, value] of Object.entries(tokens.opacity)) {
|
|
lines.push(` ${key}: ${value},`);
|
|
}
|
|
lines.push(' },', '');
|
|
|
|
// Motion (duration in ms)
|
|
lines.push(' // ── Motion ────────────────────────────────────────────────────────────');
|
|
lines.push(' motion: {');
|
|
for (const [key, value] of Object.entries(tokens.motion.duration)) {
|
|
lines.push(` ${key}: ${value},`);
|
|
}
|
|
lines.push(' },', '');
|
|
|
|
lines.push('} as const;', '');
|
|
lines.push('');
|
|
lines.push('export type Tokens = typeof tokens;', '');
|
|
|
|
return lines.join('\n');
|
|
}
|
|
|
|
// ── Write ──────────────────────────────────────────────────────────
|
|
writeFileSync(resolve(outDir, 'tokens.ts'), generateReactNative());
|
|
// eslint-disable-next-line no-console
|
|
console.log('Generated React Native tokens in generated/react-native/');
|