import fs from 'fs' import path from 'path' import StyleDictionary from 'style-dictionary' import { logBrokenReferenceLevels, logVerbosityLevels, logWarningLevels, } from 'style-dictionary/enums' import { appendAnimations } from './actions/append-animations.js' import { composeMultiThemeCss } from './actions/compose-multi-theme.js' import { unityFileHeader, name as unityFileHeaderName, } from './file-headers/unity.js' import { unityThemeFormat, name as unityThemeFormatName, } from './formats/unity-theme.js' import * as oklchTransformer from './transforms/oklch.js' import * as tailwindAnimationTokenTransformer from './transforms/tailwind-animation-token.js' import * as tailwindColorTokenTransformer from './transforms/tailwind-color-token.js' import * as tailwindGridTokenTransformer from './transforms/tailwind-grid-token.js' import * as tailwindSpacingTokenTransformer from './transforms/tailwind-spacing-token.js' import * as tailwindTextTokenTransformer from './transforms/tailwind-text-token.js' import * as tailwindTypographyTokenTransformer from './transforms/tailwind-typography-token.js' // --------------------------------------------------------------------------- // Register extensions (shared by all SD instances) // --------------------------------------------------------------------------- StyleDictionary.registerFormat({ name: unityThemeFormatName, format: unityThemeFormat, }) StyleDictionary.registerTransform({ name: 'color/oklch', type: 'value', transitive: true, filter: oklchTransformer.filter, transform: oklchTransformer.transform, }) StyleDictionary.registerTransform({ name: 'name/tailwind/v4/typography', type: 'name', transitive: true, filter: tailwindTypographyTokenTransformer.filter, transform: tailwindTypographyTokenTransformer.transform, }) StyleDictionary.registerTransform({ name: 'name/tailwind/v4/grid', type: 'name', transitive: true, filter: tailwindGridTokenTransformer.filter, transform: tailwindGridTokenTransformer.transform, }) StyleDictionary.registerTransform({ name: 'name/tailwind/v4/text', type: 'name', transitive: true, filter: tailwindTextTokenTransformer.filter, transform: tailwindTextTokenTransformer.transform, }) StyleDictionary.registerTransform({ name: 'name/tailwind/v4/color', type: 'name', transitive: true, filter: tailwindColorTokenTransformer.filter, transform: tailwindColorTokenTransformer.transform, }) StyleDictionary.registerTransform({ name: 'name/tailwind/v4/spacing', type: 'name', transitive: true, filter: tailwindSpacingTokenTransformer.filter, transform: tailwindSpacingTokenTransformer.transform, }) StyleDictionary.registerTransform({ name: 'tailwind/v4/animations/shorthand', type: 'value', transitive: true, filter: tailwindAnimationTokenTransformer.filter, transform: tailwindAnimationTokenTransformer.transform, }) StyleDictionary.registerFileHeader({ name: unityFileHeaderName, fileHeader: unityFileHeader, }) // --------------------------------------------------------------------------- // Shared config // --------------------------------------------------------------------------- const sharedTransforms = [ 'attribute/cti', 'name/kebab', 'name/tailwind/v4/text', 'name/tailwind/v4/typography', 'name/tailwind/v4/color', 'name/tailwind/v4/spacing', 'name/tailwind/v4/grid', 'color/oklch', 'fontFamily/css', 'typography/css/shorthand', 'shadow/css/shorthand', 'cubicBezier/css', 'tailwind/v4/animations/shorthand', ] const sharedLogConfig = { warnings: logWarningLevels.warn, verbosity: logVerbosityLevels.verbose, errors: { brokenReferences: logBrokenReferenceLevels.throw, }, } as const const BUILD_PATH = './dist/css/' const OUTPUT_FILE = 'unity.css' const ANIMATIONS_PATH = './assets/animations/' const DIST_FONTS_PATH = './dist/css/fonts/' const DIST_ASSETS_FONTS_PATH = './dist/css/assets/fonts/' const GENERATED_FONTS_CSS_PATH = './dist/css/generated/proprietary-fonts.css' const PREFIX = 'uy' // --------------------------------------------------------------------------- // Logging helpers // --------------------------------------------------------------------------- const color = { cyan: (s: string) => `\x1b[36m${s}\x1b[0m`, green: (s: string) => `\x1b[32m${s}\x1b[0m`, yellow: (s: string) => `\x1b[33m${s}\x1b[0m`, red: (s: string) => `\x1b[31m${s}\x1b[0m`, dim: (s: string) => `\x1b[2m${s}\x1b[0m`, bold: (s: string) => `\x1b[1m${s}\x1b[0m`, } as const function logStep(step: number, total: number, message: string) { console.log(`${color.cyan(`[${step}/${total}]`)} ${color.bold(message)}`) } function logDetail(message: string) { console.log(` ${color.dim('→')} ${message}`) } // --------------------------------------------------------------------------- // Build // --------------------------------------------------------------------------- const TOTAL_STEPS = 7 async function build() { const startTime = performance.now() console.log(color.bold('\nšŸ— Unity Themes — Build\n')) // 1. Initialize SD instances logStep(1, TOTAL_STEPS, 'Initializing Style Dictionary instances') logDetail( `Legacy — tokens/common + tokens/legacy → ${color.yellow(':root')}`, ) const legacySD = new StyleDictionary({ source: ['./tokens/common/**/*.json', './tokens/legacy/**/*.json'], platforms: { css: { transforms: sharedTransforms, prefix: PREFIX, buildPath: BUILD_PATH, files: [ { destination: '_legacy.css', format: 'css/variables', options: { selector: ':root, [data-uy-theme="legacy"]', outputReferences: true, showFileHeader: false, }, }, ], }, }, log: sharedLogConfig, }) // 2. Rebrand SD — css/variables with [data-uy-theme="rebrand"] selector logDetail( `Rebrand — tokens/common + tokens/rebrand → ${color.yellow( '[data-uy-theme="rebrand"]', )}`, ) const rebrandSD = new StyleDictionary({ source: ['./tokens/common/**/*.json', './tokens/rebrand/**/*.json'], platforms: { css: { transforms: sharedTransforms, prefix: PREFIX, buildPath: BUILD_PATH, files: [ { destination: '_rebrand.css', format: 'css/variables', options: { selector: '[data-uy-theme="rebrand"]', outputReferences: true, showFileHeader: false, }, }, ], }, }, log: sharedLogConfig, }) // 3. Theme SD — css/unity-theme format for @theme block + utilities logDetail(`TW theme — → ${color.yellow('@theme')}`) const themeSD = new StyleDictionary({ source: [ './tokens/common/**/*.json', './tokens/legacy/**/*.json', './tokens/rebrand/**/*.json', ], platforms: { css: { transforms: sharedTransforms, prefix: PREFIX, buildPath: BUILD_PATH, files: [ { destination: '_theme.css', format: unityThemeFormatName, options: { prefix: PREFIX, selector: '@theme', outputReferences: true, showFileHeader: false, }, }, ], }, }, log: sharedLogConfig, }) // Fonts SD — built-in css/fonts format + copy_assets action logDetail( `Fonts — tokens/common/font-assets.json → ${color.yellow( 'generated/proprietary-fonts.css', )}`, ) const fontsSD = new StyleDictionary({ source: ['./tokens/common/font-assets.json'], platforms: { css: { buildPath: BUILD_PATH, actions: ['copy_assets'], files: [ { destination: 'generated/proprietary-fonts.css', format: 'css/fonts.css', options: { showFileHeader: false, }, }, ], }, }, log: sharedLogConfig, }) // 2. Build fonts css + copy assets, then format theme outputs in-memory logStep(2, TOTAL_STEPS, 'Building font assets and formatting tokens') const formatStart = performance.now() const [legacyOutputs, rebrandOutputs, themeOutputs] = await Promise.all([ legacySD.formatPlatform('css'), rebrandSD.formatPlatform('css'), themeSD.formatPlatform('css'), fontsSD.buildPlatform('css'), ]) // normalize copied assets path from dist/css/assets/fonts to dist/css/fonts if (!fs.existsSync(DIST_ASSETS_FONTS_PATH)) { throw new Error(`Missing copied font assets at ${DIST_ASSETS_FONTS_PATH}`) } fs.rmSync(DIST_FONTS_PATH, { recursive: true, force: true }) fs.cpSync(DIST_ASSETS_FONTS_PATH, DIST_FONTS_PATH, { recursive: true, filter: source => !source.includes(`${path.sep}LICENSES${path.sep}`), }) fs.rmSync('./dist/css/assets/', { recursive: true, force: true }) if (!fs.existsSync(GENERATED_FONTS_CSS_PATH)) { throw new Error(`Missing generated font CSS at ${GENERATED_FONTS_CSS_PATH}`) } const generatedFontsCss = fs.readFileSync(GENERATED_FONTS_CSS_PATH, 'utf8') fs.writeFileSync( GENERATED_FONTS_CSS_PATH, generatedFontsCss.replaceAll('},`@font-face`', '}\n\n@font-face'), 'utf8', ) const legacyCss = typeof legacyOutputs[0]?.output === 'string' ? legacyOutputs[0].output : '' const rebrandCss = typeof rebrandOutputs[0]?.output === 'string' ? rebrandOutputs[0].output : '' const themeCss = typeof themeOutputs[0]?.output === 'string' ? themeOutputs[0].output : '' const formatDuration = (performance.now() - formatStart).toFixed(0) logDetail( `Legacy CSS: ${color.green(`${legacyCss.split('\n').length} lines`)}`, ) logDetail( `Rebrand CSS: ${color.green(`${rebrandCss.split('\n').length} lines`)}`, ) logDetail( `Theme CSS: ${color.green(`${themeCss.split('\n').length} lines`)}`, ) logDetail(color.dim(`Formatted in ${formatDuration}ms`)) // 3. Generate header + custom variants logStep(3, TOTAL_STEPS, 'Generating file header and font imports') const headerLines = unityFileHeader() const header = [ '/**', ...headerLines.map(l => ` * ${l}`), ' */\n', '@import "@fontsource/inter";', '@import "@fontsource/inter/500.css";', '@import "@fontsource/inter/600.css";', '@import "@fontsource/inter/700.css";', '@import "@fontsource/source-serif-4";', '@import "@fontsource/source-serif-4/500.css";', '@import "@fontsource/source-serif-4/600.css";', '@import "@fontsource/source-serif-4/700.css";', '@import "@fontsource/roboto-mono";', '@import "./generated/proprietary-fonts.css";', `@import "tailwindcss" prefix(${PREFIX});`, ].join('\n') logStep(4, TOTAL_STEPS, 'Registering custom variants') const customVariants = [ '@custom-variant theme-legacy (&:where([data-uy-theme="legacy"], [data-uy-theme="legacy"] *));', '@custom-variant theme-rebrand (&:where([data-uy-theme="rebrand"], [data-uy-theme="rebrand"] *));', ].join('\n') logDetail('theme-legacy → [data-uy-theme="legacy"]') logDetail('theme-rebrand → [data-uy-theme="rebrand"]') // 5. Compose and write logStep(5, TOTAL_STEPS, 'Composing multi-theme CSS') const outputPath = path.join(BUILD_PATH, OUTPUT_FILE) logDetail(`Output: ${color.yellow(outputPath)}`) await composeMultiThemeCss({ legacyCss, rebrandCss, themeCss, header, customVariants, outputPath, }) // 6. Append animation keyframes + reset CSS logStep(6, TOTAL_STEPS, 'Appending animation keyframes') logDetail(`Source: ${color.yellow(ANIMATIONS_PATH)}`) const legacyDict = await legacySD.getPlatformTokens('css') appendAnimations( legacyDict, { animationsPath: ANIMATIONS_PATH, buildPath: BUILD_PATH, files: [{ destination: OUTPUT_FILE }], }, {}, // kept for SD compatibility ) // Done const totalDuration = (performance.now() - startTime).toFixed(0) logStep(7, TOTAL_STEPS, 'Build verification') const outputStat = fs.statSync(outputPath) logDetail( `File size: ${color.green(`${(outputStat.size / 1024).toFixed(1)} KB`)}`, ) logDetail(`Total time: ${color.green(`${totalDuration}ms`)}`) console.log(`\n${color.green('āœ… Build completed successfully')}\n`) } build().catch((error: unknown) => { console.error(`\n${color.red('āŒ Build failed')}\n`) if (error instanceof Error) { console.error(color.red(`Error: ${error.message}`)) if (error.stack) { console.error(color.dim(error.stack)) } } else { console.error(error) } process.exit(1) })