#!/usr/bin/env node /** * generate-tokens.mjs * * Reads design-tokens/*.json and generates platform-specific code: * - web/src/theme/tokens.ts * - iOS/Kordant/Theme/GeneratedTokens.swift * - android/app/src/main/res/values/generated_tokens.xml * * Usage: node scripts/generate-tokens.mjs */ import { readFileSync, writeFileSync, mkdirSync } from "node:fs"; import { join, dirname } from "node:path"; import { fileURLToPath } from "node:url"; const __dirname = dirname(fileURLToPath(import.meta.url)); const root = join(__dirname, ".."); const tokensDir = join(root, "design-tokens"); function load(name) { return JSON.parse(readFileSync(join(tokensDir, `${name}.json`), "utf-8")); } // ─── Helpers ──────────────────────────────────────────────────────────────── function toCamel(key) { return key.replace(/([A-Z])/g, (_, c) => `_${c}`).toLowerCase(); } function hexToRgb(hex) { const h = hex.replace("#", ""); return { r: parseInt(h.substring(0, 2), 16), g: parseInt(h.substring(2, 4), 16), b: parseInt(h.substring(4, 6), 16), }; } // ─── Web (TypeScript) ────────────────────────────────────────────────────── function generateWebTokens(colors, typography, spacing, shadows, radius) { const lines = [ "// Auto-generated from design-tokens/*.json — DO NOT EDIT MANUALLY", "// Run: node scripts/generate-tokens.mjs", "", ]; // Colors lines.push("export const tokenColors = {"); // Brand lines.push(" brand: {"); for (const [key, val] of Object.entries(colors.brand)) { lines.push(` ${toCamel(key)}: "${val.value}",`); } lines.push(" },"); // Semantic lines.push(" semantic: {"); for (const [key, val] of Object.entries(colors.semantic)) { if (typeof val === "string") { lines.push(` ${toCamel(key)}: "${val}",`); } else if (val.light !== undefined) { lines.push(` ${toCamel(key)}: { light: "${val.light}", dark: "${val.dark}" },`); } } lines.push(" },"); // Background lines.push(" background: {"); for (const [key, val] of Object.entries(colors.background)) { lines.push(` ${toCamel(key)}: { light: "${val.light}", dark: "${val.dark}" },`); } lines.push(" },"); // Text lines.push(" text: {"); for (const [key, val] of Object.entries(colors.text)) { lines.push(` ${toCamel(key)}: { light: "${val.light}", dark: "${val.dark}" },`); } lines.push(" },"); // Border lines.push(" border: {"); for (const [key, val] of Object.entries(colors.border)) { lines.push(` ${toCamel(key)}: { light: "${val.light}", dark: "${val.dark}" },`); } lines.push(" },"); lines.push("};"); // Typography lines.push(""); lines.push("export const tokenTypography = {"); lines.push(` fontFamily: "${typography.fontFamily.value}",`); lines.push(` fallback: "${typography.fontFamily.fallback}",`); lines.push(" scale: {"); for (const [key, val] of Object.entries(typography.scale)) { lines.push(` ${toCamel(key)}: { size: "${val.size}", lineHeight: "${val.lineHeight}" },`); } lines.push(" },"); lines.push(" weights: {"); for (const [key, val] of Object.entries(typography.weights)) { lines.push(` ${key}: ${val.value},`); } lines.push(" },"); lines.push("};"); // Spacing lines.push(""); lines.push("export const tokenSpacing = {"); for (const [key, val] of Object.entries(spacing.scale)) { lines.push(` ${key}: "${val.value}",`); } lines.push("};"); // Shadows lines.push(""); lines.push("export const tokenShadows = {"); for (const [key, val] of Object.entries(shadows.scale)) { const shadow = `${val.x}px ${val.y}px ${val.blur}px ${val.spread}px ${val.color}`; lines.push(` ${key}: "${shadow}",`); } lines.push("};"); // Radius lines.push(""); lines.push("export const tokenRadius = {"); for (const [key, val] of Object.entries(radius.scale)) { lines.push(` ${key}: "${val.value}",`); } lines.push("};"); return lines.join("\n") + "\n"; } // ─── iOS (Swift) ──────────────────────────────────────────────────────────── function generateSwiftTokens(colors, typography, spacing) { const lines = [ "// Auto-generated from design-tokens/*.json — DO NOT EDIT MANUALLY", "// Run: node scripts/generate-tokens.mjs", "", "import SwiftUI", "", ]; // Color extension lines.push("extension Color {"); lines.push(" // MARK: - Brand"); for (const [key, val] of Object.entries(colors.brand)) { const { r, g, b } = hexToRgb(val.value); const swiftKey = key.charAt(0).toLowerCase() + key.slice(1); lines.push( ` static let ${swiftKey} = Color(red: ${r} / 255, green: ${g} / 255, blue: ${b} / 255)` ); } lines.push(""); lines.push(" // MARK: - Semantic"); for (const [key, val] of Object.entries(colors.semantic)) { if (val.value) { const { r, g, b } = hexToRgb(val.value); const swiftKey = key.charAt(0).toLowerCase() + key.slice(1); lines.push( ` static let ${swiftKey} = Color(red: ${r} / 255, green: ${g} / 255, blue: ${b} / 255)` ); } } lines.push("}"); // AdaptiveColor helper lines.push(""); lines.push("extension UIColor {"); lines.push(" convenience init(hex: String) {"); lines.push( ' var hexSanitized = hex.trimmingCharacters(in: .whitespacesAndNewlines)' ); lines.push( ' hexSanitized = hexSanitized.replacingOccurrences(of: "#", with: "")' ); lines.push(" var rgb: UInt64 = 0"); lines.push(" Scanner(string: hexSanitized).scanHexInt64(&rgb)"); lines.push(" let r = CGFloat((rgb & 0xFF0000) >> 16) / 255.0"); lines.push(" let g = CGFloat((rgb & 0x00FF00) >> 8) / 255.0"); lines.push(" let b = CGFloat(rgb & 0x0000FF) / 255.0"); lines.push(" self.init(red: r, green: g, blue: b, alpha: 1.0)"); lines.push(" }"); lines.push("}"); // Spacing lines.push(""); lines.push("enum DesignTokens {"); lines.push(" enum Spacing {"); for (const [key, val] of Object.entries(spacing.scale)) { if (key === "0") continue; // skip numeric key — invalid in Swift const px = parseInt(val.value); lines.push(` static let ${key}: CGFloat = ${px}`); } lines.push(" }"); lines.push("}"); return lines.join("\n") + "\n"; } // ─── Android (XML) ────────────────────────────────────────────────────────── function generateAndroidTokens(colors, typography, spacing, shadows, radius) { const lines = [ '', "", "", "", ]; // Brand colors lines.push(" "); for (const [key, val] of Object.entries(colors.brand)) { const name = "brand_" + toCamel(key); lines.push(` ${val.value}`); } // Semantic colors lines.push(" "); for (const [key, val] of Object.entries(colors.semantic)) { if (typeof val === "string") { const name = "sem_" + toCamel(key); lines.push(` ${val}`); } } // Light theme colors lines.push(" "); lines.push( ` ${colors.background.bg.light}` ); lines.push( ` ${colors.background.bgSecondary.light}` ); lines.push( ` ${colors.text.textPrimary.light}` ); lines.push( ` ${colors.text.textSecondary.light}` ); lines.push( ` ${colors.border.border.light}` ); // Dark theme colors lines.push(" "); lines.push( ` ${colors.background.bg.dark}` ); lines.push( ` ${colors.background.bgSecondary.dark}` ); lines.push( ` ${colors.text.textPrimary.dark}` ); lines.push( ` ${colors.text.textSecondary.dark}` ); lines.push( ` ${colors.border.border.dark}` ); // Spacing dimensions lines.push(" "); for (const [key, val] of Object.entries(spacing.scale)) { const dp = val.value.replace("px", "dp"); lines.push(` ${dp}`); } // Border radius lines.push(" "); for (const [key, val] of Object.entries(radius.scale)) { const name = key === "full" ? "corner_full" : `corner_${key}`; const dp = val.value.replace("px", "dp"); lines.push(` ${dp}`); } // Font sizes lines.push(" "); for (const [key, val] of Object.entries(typography.scale)) { lines.push(` ${val.size.replace("px", "sp")}`); lines.push(` ${val.lineHeight.replace("px", "sp")}`); } lines.push(""); return lines.join("\n") + "\n"; } // ─── Main ─────────────────────────────────────────────────────────────────── function main() { const colors = load("colors"); const typography = load("typography"); const spacing = load("spacing"); const shadows = load("shadows"); const radius = load("radius"); // Web const webPath = join(root, "web", "src", "theme", "tokens.ts"); mkdirSync(dirname(webPath), { recursive: true }); writeFileSync(webPath, generateWebTokens(colors, typography, spacing, shadows, radius)); console.log(`✅ ${webPath}`); // iOS const iosPath = join(root, "iOS", "Kordant", "Theme", "GeneratedTokens.swift"); mkdirSync(dirname(iosPath), { recursive: true }); writeFileSync(iosPath, generateSwiftTokens(colors, typography, spacing)); console.log(`✅ ${iosPath}`); // Android const androidPath = join( root, "android", "app", "src", "main", "res", "values", "generated_tokens.xml" ); mkdirSync(dirname(androidPath), { recursive: true }); writeFileSync(androidPath, generateAndroidTokens(colors, typography, spacing, shadows, radius)); console.log(`✅ ${androidPath}`); console.log("🎨 Token generation complete."); } main();