1+ const fs = require ( 'fs' ) ;
2+ const path = require ( 'path' ) ;
3+
4+ // Ensure target directory exists
5+ function ensureDirectoryExists ( dirPath ) {
6+ if ( ! fs . existsSync ( dirPath ) ) {
7+ fs . mkdirSync ( dirPath , { recursive : true } ) ;
8+ console . log ( `Created directory: ${ dirPath } ` ) ;
9+ }
10+ }
11+
12+ // Read JSON file
13+ function readJsonFile ( filePath ) {
14+ try {
15+ const data = fs . readFileSync ( filePath , 'utf8' ) ;
16+ return JSON . parse ( data ) ;
17+ } catch ( error ) {
18+ console . error ( `Error reading file ${ filePath } :` , error ) ;
19+ process . exit ( 1 ) ;
20+ }
21+ }
22+
23+ // Resolve reference values
24+ function resolveReferences ( tokens , primitiveTokens ) {
25+ const resolvedTokens = JSON . parse ( JSON . stringify ( tokens ) ) ;
26+
27+ function resolveValue ( value , visitedRefs = new Set ( ) ) {
28+ if ( typeof value !== 'string' || ! value . startsWith ( '{' ) || ! value . endsWith ( '}' ) ) {
29+ return value ;
30+ }
31+
32+ const reference = value . slice ( 1 , - 1 ) ;
33+ if ( visitedRefs . has ( reference ) ) {
34+ console . warn ( `Circular reference detected: ${ reference } ` ) ;
35+ return value ;
36+ }
37+
38+ visitedRefs . add ( reference ) ;
39+
40+ // Split reference path
41+ const parts = reference . split ( '.' ) ;
42+ let current ;
43+
44+ // First look in primitive tokens
45+ if ( primitiveTokens ) {
46+ current = primitiveTokens ;
47+ for ( const part of parts ) {
48+ if ( current && current [ part ] ) {
49+ current = current [ part ] ;
50+ } else {
51+ current = null ;
52+ break ;
53+ }
54+ }
55+ }
56+
57+ // If not found in primitives, look in current tokens
58+ if ( ! current ) {
59+ current = resolvedTokens ;
60+ for ( const part of parts ) {
61+ if ( current && current [ part ] ) {
62+ current = current [ part ] ;
63+ } else {
64+ console . warn ( `Reference not found: ${ reference } ` ) ;
65+ return value ;
66+ }
67+ }
68+ }
69+
70+ if ( current && current . $value ) {
71+ return resolveValue ( current . $value , visitedRefs ) ;
72+ }
73+
74+ return current ;
75+ }
76+
77+ function processObject ( obj ) {
78+ for ( const key in obj ) {
79+ if ( typeof obj [ key ] === 'object' && obj [ key ] !== null ) {
80+ processObject ( obj [ key ] ) ;
81+ } else if ( key === '$value' ) {
82+ obj . $value = resolveValue ( obj . $value ) ;
83+ }
84+ }
85+ }
86+
87+ processObject ( resolvedTokens ) ;
88+ return resolvedTokens ;
89+ }
90+
91+ // Convert to CSS variables
92+ function convertToCssVariables ( tokens , prefix = '' , isDarkTheme = false , sourceFile ) {
93+ let variableNames = [ ] ; // Store all generated variable names
94+
95+ // Create CSS file header with warning comments
96+ let css = `/**
97+ * Design system ${ isDarkTheme ? 'dark' : 'light' } theme variables
98+ * AUTO-GENERATED FILE - DO NOT EDIT DIRECTLY
99+ *
100+ * Generated from: ${ sourceFile }
101+ * Generated time: ${ new Date ( ) . toISOString ( ) }
102+ *
103+ * To modify these values, edit the source JSON files and run the token conversion script:
104+ * node scripts/system-token/convert-tokens.cjs
105+ */
106+
107+ ` ;
108+
109+ // Select root selector based on theme
110+ const rootSelector = isDarkTheme ? ':root[data-dark-mode=true]' : ':root' ;
111+
112+ function processTokens ( obj , path = '' ) {
113+ for ( const key in obj ) {
114+ const newPath = path ? `${ path } -${ key } ` : key ;
115+
116+ // If object but not a value object (i.e., doesn't have $value)
117+ if ( typeof obj [ key ] === 'object' && obj [ key ] !== null && ! obj [ key ] . $value ) {
118+ processTokens ( obj [ key ] , newPath ) ;
119+ }
120+ // If it's a value object (has $value)
121+ else if ( typeof obj [ key ] === 'object' && obj [ key ] !== null && obj [ key ] . $value ) {
122+ const token = obj [ key ] ;
123+ const variableName = `--${ prefix } ${ newPath . toLowerCase ( ) . replace ( / _ / g, '-' ) } ` ;
124+
125+ let value = token . $value ;
126+
127+ // Add to variable names list
128+ variableNames . push ( variableName . substring ( 2 ) ) ; // Remove leading --
129+
130+ // Format value based on type
131+ if ( token . $type === 'dimension' ) {
132+ // Keep dimension values as is, they already include units like px
133+ css += ` ${ variableName } : ${ value } ;\n` ;
134+ } else if ( token . $type === 'color' ) {
135+ css += ` ${ variableName } : ${ value } ;\n` ;
136+ } else {
137+ css += ` ${ variableName } : ${ value } ;\n` ;
138+ }
139+ }
140+ }
141+ }
142+
143+ css += `${ rootSelector } {\n` ;
144+ processTokens ( tokens ) ;
145+ css += '}\n' ;
146+
147+ return { css, variableNames } ;
148+ }
149+
150+ // Generate Tailwind color config from CSS variable names
151+ function createTailwindColorsFromVariables ( variableNames ) {
152+ const colors = { } ;
153+
154+ // Group by prefix
155+ variableNames . forEach ( varName => {
156+ const parts = varName . split ( '-' ) ;
157+ const mainCategory = parts [ 0 ] ;
158+
159+ // Handle main category
160+ if ( ! colors [ mainCategory ] ) {
161+ colors [ mainCategory ] = { } ;
162+ }
163+
164+ // Generate key without main category
165+ const subKey = parts . slice ( 1 ) . join ( '-' ) ;
166+
167+ // Use 'DEFAULT' if empty
168+ const finalKey = subKey || 'DEFAULT' ;
169+
170+ // Set CSS variable reference
171+ colors [ mainCategory ] [ finalKey ] = `var(--${ varName } )` ;
172+ } ) ;
173+
174+ // Fix special categories to match Tailwind conventions
175+ delete colors [ 'spacing' ] ;
176+ delete colors [ 'border-radius' ] ;
177+ delete colors [ 'shadow' ] ;
178+
179+ // Generate CommonJS module code
180+ const tailwindCode = `/**
181+ * TailwindCSS color configuration
182+ * AUTO-GENERATED FILE - DO NOT EDIT DIRECTLY
183+ *
184+ * This file is auto-generated by convert-tokens.cjs script
185+ * Generation time: ${ new Date ( ) . toISOString ( ) }
186+ *
187+ * To modify these colors, edit the source JSON files and run the token conversion script:
188+ * node scripts/system-token/convert-tokens.cjs
189+ *
190+ * These colors reference CSS variables, ensure the corresponding CSS files are loaded
191+ */
192+
193+ module.exports = ${ JSON . stringify ( colors , null , 2 ) } ;
194+ ` ;
195+
196+ return tailwindCode ;
197+ }
198+
199+ // Main function
200+ function convertDesignTokens ( primitiveFilePath , semanticFilePath , outputFilePath , isDarkTheme = false ) {
201+ // Read files
202+ const primitiveTokens = readJsonFile ( primitiveFilePath ) ;
203+ const semanticTokens = readJsonFile ( semanticFilePath ) ;
204+
205+ // Resolve references
206+ const resolvedTokens = resolveReferences ( semanticTokens , primitiveTokens ) ;
207+
208+ // Convert to CSS with source file information
209+ const { css, variableNames } = convertToCssVariables (
210+ resolvedTokens ,
211+ '' ,
212+ isDarkTheme ,
213+ path . basename ( semanticFilePath ) ,
214+ ) ;
215+
216+ // Ensure output directory exists
217+ const outputDir = path . dirname ( outputFilePath ) ;
218+ ensureDirectoryExists ( outputDir ) ;
219+
220+ // Write file
221+ fs . writeFileSync ( outputFilePath , css , 'utf8' ) ;
222+ console . log ( `CSS variables written to ${ outputFilePath } ` ) ;
223+
224+ return { variableNames } ;
225+ }
226+
227+ // Define project root directory
228+ const projectRoot = path . resolve ( __dirname , '../..' ) ; // Go up two levels from scripts/system-token to project root
229+
230+ // Define input and output paths
231+ const inputDir = path . join ( __dirname ) ; // Current script directory
232+ const cssOutputDir = path . join ( projectRoot , 'src/styles/variables' ) ;
233+ const tailwindOutputDir = path . join ( projectRoot , 'tailwind' ) ;
234+
235+ // Ensure output directories exist
236+ ensureDirectoryExists ( cssOutputDir ) ;
237+ ensureDirectoryExists ( tailwindOutputDir ) ;
238+
239+ // Execute conversion
240+ console . log ( 'Converting design tokens to CSS variables...' ) ;
241+
242+ // Collect all variable names
243+ let allVariableNames = [ ] ;
244+
245+ // Convert light theme (default theme)
246+ const lightResult = convertDesignTokens (
247+ path . join ( inputDir , 'primitive.json' ) ,
248+ path . join ( inputDir , 'semantic.light.json' ) ,
249+ path . join ( cssOutputDir , 'semantic.light.css' ) ,
250+ false , // Light theme
251+ ) ;
252+ allVariableNames = allVariableNames . concat ( lightResult . variableNames ) ;
253+
254+ // Convert dark theme (using data-dark-mode attribute)
255+ const darkResult = convertDesignTokens (
256+ path . join ( inputDir , 'primitive.json' ) ,
257+ path . join ( inputDir , 'semantic.dark.json' ) ,
258+ path . join ( cssOutputDir , 'semantic.dark.css' ) ,
259+ true , // Dark theme
260+ ) ;
261+ // Dark theme variables are the same as light theme, no need to merge
262+
263+ // Generate Tailwind color configuration
264+ console . log ( 'Generating Tailwind color configuration with CSS variable references...' ) ;
265+ const tailwindColors = createTailwindColorsFromVariables ( allVariableNames ) ;
266+ fs . writeFileSync ( path . join ( tailwindOutputDir , 'new-colors.cjs' ) , tailwindColors ) ;
267+ console . log ( `Tailwind colors written to ${ path . join ( tailwindOutputDir , 'new-colors.cjs' ) } ` ) ;
268+
269+ console . log ( 'Conversion completed successfully!' ) ;
0 commit comments