#!/usr/bin/env node /** * Generate CSS variables from design tokens JSON * * Usage: * node generate-tokens.cjs --config tokens.json -o tokens.css * node generate-tokens.cjs --config tokens.json --format tailwind */ const fs = require('fs'); const path = require('path'); /** * Parse command line arguments */ function parseArgs() { const args = process.argv.slice(2); const options = { config: null, output: null, format: 'css' // css | tailwind }; for (let i = 0; i < args.length; i++) { if (args[i] === '--config' || args[i] === '-c') { options.config = args[++i]; } else if (args[i] === '--output' || args[i] === '-o') { options.output = args[++i]; } else if (args[i] === '--format' || args[i] === '-f') { options.format = args[++i]; } else if (args[i] === '--help' || args[i] === '-h') { console.log(` Usage: node generate-tokens.cjs [options] Options: -c, --config Input JSON token file (required) -o, --output Output file (default: stdout) -f, --format Output format: css | tailwind (default: css) -h, --help Show this help `); process.exit(0); } } return options; } /** * Resolve token references like {primitive.color.blue.600} */ function resolveReference(value, tokens) { if (typeof value !== 'string' || !value.startsWith('{')) { return value; } const path = value.slice(1, -1).split('.'); let result = tokens; for (const key of path) { result = result?.[key]; } if (result?.$value) { return resolveReference(result.$value, tokens); } return result || value; } /** * Convert token name to CSS variable name */ function toCssVarName(path) { return '--' + path.join('-').replace(/\./g, '-'); } /** * Flatten tokens into CSS variables */ function flattenTokens(obj, tokens, prefix = [], result = {}) { for (const [key, value] of Object.entries(obj)) { const currentPath = [...prefix, key]; if (value && typeof value === 'object') { if (value.$value !== undefined) { // This is a token const cssVar = toCssVarName(currentPath); const resolvedValue = resolveReference(value.$value, tokens); result[cssVar] = resolvedValue; } else { // Recurse into nested object flattenTokens(value, tokens, currentPath, result); } } } return result; } /** * Generate CSS output */ function generateCSS(tokens) { const primitive = flattenTokens(tokens.primitive || {}, tokens, ['primitive']); const semantic = flattenTokens(tokens.semantic || {}, tokens, []); const component = flattenTokens(tokens.component || {}, tokens, []); const darkSemantic = flattenTokens(tokens.dark?.semantic || {}, tokens, []); let css = `/* Design Tokens - Auto-generated */ /* Do not edit directly - modify tokens.json instead */ /* === PRIMITIVES === */ :root { ${Object.entries(primitive).map(([k, v]) => ` ${k}: ${v};`).join('\n')} } /* === SEMANTIC === */ :root { ${Object.entries(semantic).map(([k, v]) => ` ${k}: ${v};`).join('\n')} } /* === COMPONENTS === */ :root { ${Object.entries(component).map(([k, v]) => ` ${k}: ${v};`).join('\n')} } `; if (Object.keys(darkSemantic).length > 0) { css += ` /* === DARK MODE === */ .dark { ${Object.entries(darkSemantic).map(([k, v]) => ` ${k}: ${v};`).join('\n')} } `; } return css; } /** * Generate Tailwind config output */ function generateTailwind(tokens) { const semantic = flattenTokens(tokens.semantic || {}, tokens, []); // Extract colors for Tailwind const colors = {}; for (const [key, value] of Object.entries(semantic)) { if (key.includes('color')) { const name = key.replace('--color-', '').replace(/-/g, '.'); colors[name] = `var(${key})`; } } return `// Tailwind color config - Auto-generated // Add to tailwind.config.ts theme.extend.colors module.exports = { colors: ${JSON.stringify(colors, null, 2).replace(/"/g, "'")} }; `; } /** * Main */ function main() { const options = parseArgs(); if (!options.config) { console.error('Error: --config is required'); process.exit(1); } // Resolve config path const configPath = path.resolve(process.cwd(), options.config); if (!fs.existsSync(configPath)) { console.error(`Error: Config file not found: ${configPath}`); process.exit(1); } // Read and parse tokens const tokens = JSON.parse(fs.readFileSync(configPath, 'utf-8')); // Generate output let output; if (options.format === 'tailwind') { output = generateTailwind(tokens); } else { output = generateCSS(tokens); } // Write output if (options.output) { const outputPath = path.resolve(process.cwd(), options.output); fs.mkdirSync(path.dirname(outputPath), { recursive: true }); fs.writeFileSync(outputPath, output); console.log(`Generated: ${outputPath}`); } else { console.log(output); } } main();