design-tokens-converter.mjs 35 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028
  1. import fs from "node:fs/promises";
  2. import path from "node:path";
  3. import { fileURLToPath } from "node:url";
  4. const camelCase = (str) => {
  5. return str.replace(/-([a-z])/g, (g) => g[1].toUpperCase());
  6. };
  7. // Get current file directory for resolving paths
  8. const __filename = fileURLToPath(import.meta.url);
  9. const __dirname = path.dirname(__filename);
  10. const RAW_COLOR_VALUE_TOKENS = ["primary", "shadow", "outline", "surface", "accent", "background"];
  11. const shouldGenerateRawColorValue = (name) => {
  12. return RAW_COLOR_VALUE_TOKENS.some((token) => name.includes(token));
  13. };
  14. // Determine correct paths for the workspace
  15. const findWorkspaceRoot = () => {
  16. // We'll start with this file's directory and go up until we find the web directory
  17. let currentDir = __dirname;
  18. while (!currentDir.endsWith("web") && currentDir !== "/") {
  19. currentDir = path.dirname(currentDir);
  20. }
  21. if (!currentDir.endsWith("web")) {
  22. throw new Error("Could not find workspace root directory");
  23. }
  24. return currentDir;
  25. };
  26. const workspaceRoot = findWorkspaceRoot();
  27. // Paths
  28. const designVariablesPath = path.join(workspaceRoot, "design-tokens.json");
  29. const cssOutputPath = path.join(workspaceRoot, "libs/ui/src/tokens/tokens.scss");
  30. const jsOutputPath = path.join(workspaceRoot, "libs/ui/src/tokens/tokens.js");
  31. /**
  32. * Convert a value to rem units rounded to 4 decimal places no trailing zeros
  33. * @param {string} value - The value to convert
  34. * @returns {string} - The converted value in rem units
  35. */
  36. function convertToRem(value) {
  37. const remValue = (Number(value) / 16).toFixed(4).replace(/\.?0+$/, "");
  38. // Ensure 0 is returned as a unitless value
  39. if (remValue === "0") {
  40. return remValue;
  41. }
  42. return `${remValue}rem`;
  43. }
  44. /**
  45. * Process design variables and extract tokens
  46. * @param {Object} variables - The design variables object
  47. * @returns {Object} - Object containing tokens for CSS and JavaScript
  48. */
  49. function processDesignVariables(variables) {
  50. const result = {
  51. cssVariables: {
  52. light: [],
  53. dark: [],
  54. },
  55. jsTokens: {
  56. colors: {},
  57. spacing: {},
  58. typography: {},
  59. cornerRadius: {},
  60. },
  61. };
  62. // Process colors
  63. if (variables["@color"] && variables["@color"].$color) {
  64. processColorTokens(variables["@color"].$color, "", result, variables);
  65. }
  66. // Process primitives
  67. if (variables["@primitives"] && variables["@primitives"].$color) {
  68. processPrimitiveColors(variables["@primitives"].$color, result, variables);
  69. }
  70. // Process primitive spacing
  71. if (variables["@primitives"] && variables["@primitives"].$spacing) {
  72. processPrimitiveSpacing(variables["@primitives"].$spacing, result);
  73. }
  74. // Process primitive typography
  75. if (variables["@primitives"] && variables["@primitives"].$typography) {
  76. processPrimitiveTypography(variables["@primitives"].$typography, result);
  77. }
  78. // Process primitive corner-radius
  79. if (variables["@primitives"] && variables["@primitives"]["$corner-radius"]) {
  80. processPrimitiveCornerRadius(variables["@primitives"]["$corner-radius"], result);
  81. }
  82. // Process sizing tokens
  83. if (variables["@sizing"]) {
  84. processSizingTokens(variables["@sizing"], result, variables);
  85. }
  86. // Process typography
  87. if (variables["@typography"]) {
  88. processTypographyTokens(variables["@typography"], result, variables);
  89. }
  90. // Post-process for Tailwind compatibility
  91. result.jsTokens.colors = transformColorObjectForTailwind(result.jsTokens.colors);
  92. return result;
  93. }
  94. /**
  95. * Process primitive spacing tokens
  96. * @param {Object} spacingObj - The spacing object from primitives
  97. * @param {Object} result - The result object to populate
  98. */
  99. function processPrimitiveSpacing(spacingObj, result) {
  100. for (const key in spacingObj) {
  101. if (spacingObj[key].$type === "number" && spacingObj[key].$value !== undefined) {
  102. const name = key.replace("$", "");
  103. const value = spacingObj[key].$value;
  104. const cssVarName = `--spacing-${name}`;
  105. // Add to CSS variables
  106. result.cssVariables.light.push(`${cssVarName}: ${convertToRem(value)};`);
  107. // Add to JavaScript tokens
  108. if (!result.jsTokens.spacing.primitive) {
  109. result.jsTokens.spacing.primitive = {};
  110. }
  111. result.jsTokens.spacing.primitive[name] = `var(${cssVarName})`;
  112. }
  113. }
  114. }
  115. /**
  116. * Process primitive typography tokens
  117. * @param {Object} typographyObj - The typography object from primitives
  118. * @param {Object} result - The result object to populate
  119. */
  120. function processPrimitiveTypography(typographyObj, result) {
  121. // Process font sizes
  122. if (typographyObj["$font-size"]) {
  123. for (const key in typographyObj["$font-size"]) {
  124. if (
  125. typographyObj["$font-size"][key].$type === "number" &&
  126. typographyObj["$font-size"][key].$value !== undefined
  127. ) {
  128. const name = key.replace("$", "");
  129. const value = typographyObj["$font-size"][key].$value;
  130. const cssVarName = `--font-size-${name}`;
  131. // Add to CSS variables
  132. result.cssVariables.light.push(`${cssVarName}: ${convertToRem(value)};`);
  133. // Add to JavaScript tokens
  134. if (!result.jsTokens.typography.fontSize) {
  135. result.jsTokens.typography.fontSize = {};
  136. }
  137. if (!result.jsTokens.typography.fontSize.primitive) {
  138. result.jsTokens.typography.fontSize.primitive = {};
  139. }
  140. result.jsTokens.typography.fontSize.primitive[name] = `var(${cssVarName})`;
  141. }
  142. }
  143. }
  144. // Process font weights
  145. if (typographyObj["$font-weight"]) {
  146. for (const key in typographyObj["$font-weight"]) {
  147. if (
  148. typographyObj["$font-weight"][key].$type === "number" &&
  149. typographyObj["$font-weight"][key].$value !== undefined
  150. ) {
  151. const name = key.replace("$", "");
  152. const value = typographyObj["$font-weight"][key].$value;
  153. const cssVarName = `--font-weight-${name}`;
  154. // Add to CSS variables
  155. result.cssVariables.light.push(`${cssVarName}: ${value};`);
  156. // Add to JavaScript tokens
  157. if (!result.jsTokens.typography.fontWeight) {
  158. result.jsTokens.typography.fontWeight = {};
  159. }
  160. if (!result.jsTokens.typography.fontWeight.primitive) {
  161. result.jsTokens.typography.fontWeight.primitive = {};
  162. }
  163. result.jsTokens.typography.fontWeight.primitive[name] = `var(${cssVarName})`;
  164. }
  165. }
  166. }
  167. // Process line heights
  168. if (typographyObj["$line-height"]) {
  169. for (const key in typographyObj["$line-height"]) {
  170. if (
  171. typographyObj["$line-height"][key].$type === "number" &&
  172. typographyObj["$line-height"][key].$value !== undefined
  173. ) {
  174. const name = key.replace("$", "");
  175. const value = typographyObj["$line-height"][key].$value;
  176. const cssVarName = `--line-height-${name}`;
  177. // Add to CSS variables
  178. result.cssVariables.light.push(`${cssVarName}: ${convertToRem(value)};`);
  179. // Add to JavaScript tokens
  180. if (!result.jsTokens.typography.lineHeight) {
  181. result.jsTokens.typography.lineHeight = {};
  182. }
  183. if (!result.jsTokens.typography.lineHeight.primitive) {
  184. result.jsTokens.typography.lineHeight.primitive = {};
  185. }
  186. result.jsTokens.typography.lineHeight.primitive[name] = `var(${cssVarName})`;
  187. }
  188. }
  189. }
  190. // Process letter spacing
  191. if (typographyObj["$letter-spacing"]) {
  192. for (const key in typographyObj["$letter-spacing"]) {
  193. if (
  194. typographyObj["$letter-spacing"][key].$type === "number" &&
  195. typographyObj["$letter-spacing"][key].$value !== undefined
  196. ) {
  197. const name = key.replace("$", "");
  198. const value = typographyObj["$letter-spacing"][key].$value;
  199. const cssVarName = `--letter-spacing-${name}`;
  200. // Add to CSS variables
  201. result.cssVariables.light.push(`${cssVarName}: ${convertToRem(value)};`);
  202. // Add to JavaScript tokens
  203. if (!result.jsTokens.typography.letterSpacing) {
  204. result.jsTokens.typography.letterSpacing = {};
  205. }
  206. if (!result.jsTokens.typography.letterSpacing.primitive) {
  207. result.jsTokens.typography.letterSpacing.primitive = {};
  208. }
  209. result.jsTokens.typography.letterSpacing.primitive[name] = `var(${cssVarName})`;
  210. }
  211. }
  212. }
  213. // Process font families
  214. if (typographyObj["$font-family"]) {
  215. for (const key in typographyObj["$font-family"]) {
  216. if (typographyObj["$font-family"][key].$value !== undefined) {
  217. const name = key.replace("$", "");
  218. const value = typographyObj["$font-family"][key].$value;
  219. const cssVarName = `--font-family-${name}`;
  220. // Add to CSS variables
  221. result.cssVariables.light.push(`${cssVarName}: "${value}";`);
  222. // Add to JavaScript tokens
  223. if (!result.jsTokens.typography.fontFamily) {
  224. result.jsTokens.typography.fontFamily = {};
  225. }
  226. if (!result.jsTokens.typography.fontFamily.primitive) {
  227. result.jsTokens.typography.fontFamily.primitive = {};
  228. }
  229. result.jsTokens.typography.fontFamily.primitive[name] = `var(${cssVarName})`;
  230. }
  231. }
  232. }
  233. }
  234. /**
  235. * Process primitive corner radius tokens
  236. * @param {Object} cornerRadiusObj - The corner radius object from primitives
  237. * @param {Object} result - The result object to populate
  238. */
  239. function processPrimitiveCornerRadius(cornerRadiusObj, result) {
  240. for (const key in cornerRadiusObj) {
  241. if (cornerRadiusObj[key].$type === "number" && cornerRadiusObj[key].$value !== undefined) {
  242. const name = key.replace("$", "");
  243. const value = cornerRadiusObj[key].$value;
  244. const cssVarName = `--corner-radius-${name}`;
  245. let resolvedValue;
  246. if (typeof value === "string" && value.startsWith("{") && value.endsWith("}")) {
  247. const reference = value.substring(1, value.length - 1);
  248. const parts = reference.split(".");
  249. // If it's a reference to a primitive spacing value, directly use the corresponding CSS variable
  250. if (parts[0] === "@primitives") {
  251. const collectionKey = parts[1].replace("$", "");
  252. const valueKey = parts[2].replace("$", "");
  253. resolvedValue = `var(--${collectionKey}-${valueKey})`;
  254. result.cssVariables.light.push(`${cssVarName}: ${resolvedValue};`);
  255. } else {
  256. // Otherwise, try to resolve the value normally
  257. resolvedValue = resolveReference(value, variables);
  258. result.cssVariables.light.push(`${cssVarName}: ${convertToRem(resolvedValue)};`);
  259. }
  260. } else {
  261. // Not a reference, use directly
  262. resolvedValue = value;
  263. result.cssVariables.light.push(`${cssVarName}: ${convertToRem(resolvedValue)};`);
  264. }
  265. // Add to JavaScript tokens
  266. if (!result.jsTokens.cornerRadius.primitive) {
  267. result.jsTokens.cornerRadius.primitive = {};
  268. }
  269. result.jsTokens.cornerRadius.primitive[name] = `var(${cssVarName})`;
  270. }
  271. }
  272. }
  273. /**
  274. * Process sizing tokens from design variables
  275. * @param {Object} sizingObj - The spacing object from design variables
  276. * @param {Object} result - The result object to populate
  277. * @param {Object} variables - The variables object for reference resolution
  278. * @param {String} parentKey - The parent key for nested tokens
  279. */
  280. function processSizingTokens(sizingObj, result, variables, parentKey = "") {
  281. for (const key in sizingObj) {
  282. if (key.startsWith("$")) {
  283. // process nested keys
  284. processSizingTokens(
  285. sizingObj[key],
  286. result,
  287. variables,
  288. // Fix the variable name from corder to corner
  289. `${parentKey ? `${parentKey}-` : ""}${key.replace(/\$/g, "").replace("corder", "corner")}`,
  290. );
  291. continue;
  292. }
  293. if (sizingObj[key].$type === "number" && sizingObj[key].$value !== undefined) {
  294. const name = key.replace("$", "");
  295. const value = sizingObj[key].$value;
  296. const tokenKey = parentKey || key;
  297. const jsTokenKey = camelCase(tokenKey.replace("$", ""));
  298. const cssVarName = `--${tokenKey}-${name}`;
  299. let resolvedValue;
  300. if (typeof value === "string" && value.startsWith("{") && value.endsWith("}")) {
  301. const reference = value.substring(1, value.length - 1);
  302. const parts = reference.split(".");
  303. // If it's a reference to a primitive spacing value, directly use the corresponding CSS variable
  304. if (parts[0] === "@primitives") {
  305. const collectionKey = parts[1].replace("$", "");
  306. const valueKey = parts[2].replace("$", "");
  307. resolvedValue = `var(--${collectionKey}-${valueKey})`;
  308. result.cssVariables.light.push(`${cssVarName}: ${resolvedValue};`);
  309. } else {
  310. // Otherwise, try to resolve the value normally
  311. resolvedValue = resolveReference(value, variables);
  312. result.cssVariables.light.push(`${cssVarName}: ${convertToRem(resolvedValue)};`);
  313. }
  314. } else {
  315. // Not a reference, use directly
  316. resolvedValue = value;
  317. result.cssVariables.light.push(`${cssVarName}: ${convertToRem(resolvedValue)};`);
  318. }
  319. // Add to JavaScript tokens
  320. if (!result.jsTokens[jsTokenKey]) {
  321. result.jsTokens[jsTokenKey] = {};
  322. }
  323. result.jsTokens[jsTokenKey][name] = `var(${cssVarName})`;
  324. }
  325. }
  326. }
  327. function processTokenCollection(collectionKey, subCollectionKey) {
  328. const collectionJsKey = camelCase(collectionKey);
  329. const subCollectionJsKey = camelCase(subCollectionKey);
  330. const subCollectionKeyRegex = new RegExp(`${subCollectionKey}\\-?`, "g");
  331. return function process(tokenCollection, result, variables, parentKey = subCollectionKey) {
  332. for (const key in tokenCollection) {
  333. if (key.startsWith("$")) {
  334. // process nested keys
  335. process(tokenCollection[key], result, variables, `${parentKey ? `${parentKey}-` : ""}${key.replace("$", "")}`);
  336. continue;
  337. }
  338. if (tokenCollection[key].$value !== undefined) {
  339. const isNumber = tokenCollection[key].$type === "number";
  340. const name = key.replace("$", "");
  341. const value = tokenCollection[key].$value;
  342. const cssVarName = `--${parentKey}-${name}`;
  343. let resolvedValue;
  344. if (typeof value === "string" && value.startsWith("{") && value.endsWith("}")) {
  345. const reference = value.substring(1, value.length - 1);
  346. const parts = reference.replace(`$${collectionKey}.`, "").split(".");
  347. // If it's a reference to a primitive spacing value, directly use the corresponding CSS variable
  348. if (parts[0] === "@primitives") {
  349. const collectionKey = parts[1].replace("$", "");
  350. const valueKey = parts[2].replace("$", "");
  351. resolvedValue = `var(--${collectionKey}-${valueKey})`;
  352. result.cssVariables.light.push(`${cssVarName}: ${resolvedValue};`);
  353. } else {
  354. // Otherwise, try to resolve the value normally
  355. resolvedValue = resolveReference(value, variables);
  356. result.cssVariables.light.push(`${cssVarName}: ${isNumber ? convertToRem(resolvedValue) : resolvedValue};`);
  357. }
  358. } else {
  359. // Not a reference, use directly
  360. resolvedValue = value;
  361. result.cssVariables.light.push(`${cssVarName}: ${resolvedValue};`);
  362. }
  363. // Add to JavaScript tokens
  364. if (!result.jsTokens[collectionJsKey]) {
  365. result.jsTokens[collectionJsKey] = {};
  366. }
  367. if (!result.jsTokens[collectionJsKey][subCollectionJsKey]) {
  368. result.jsTokens[collectionJsKey][subCollectionJsKey] = {};
  369. }
  370. const jsKey = `${parentKey ? `${parentKey}-` : ""}${name}`;
  371. result.jsTokens[collectionJsKey][subCollectionJsKey][jsKey.replace(subCollectionKeyRegex, "")] =
  372. `var(${cssVarName})`;
  373. }
  374. }
  375. };
  376. }
  377. /**
  378. * Process font size tokens
  379. * @param {Object} fontSizeObj - The font size object from typography
  380. * @param {Object} result - The result object to populate
  381. * @param {Object} variables - The variables object for reference resolution
  382. * @param {String} parentKey - The parent key for nested tokens
  383. */
  384. const processFontSizeTokens = processTokenCollection("typography", "font-size");
  385. /**
  386. * Process font weight tokens
  387. * @param {Object} fontWeightObj - The font weight object from typography
  388. * @param {Object} result - The result object to populate
  389. * @param {Object} variables - The variables object for reference resolution
  390. */
  391. const processFontWeightTokens = processTokenCollection("typography", "font-weight");
  392. /**
  393. * Process line height tokens
  394. * @param {Object} lineHeightObj - The line height object from typography
  395. * @param {Object} result - The result object to populate
  396. * @param {Object} variables - The variables object for reference resolution
  397. */
  398. const processLineHeightTokens = processTokenCollection("typography", "line-height");
  399. /**
  400. * Process letter spacing tokens
  401. * @param {Object} letterSpacingObj - The letter spacing object from typography
  402. * @param {Object} result - The result object to populate
  403. * @param {Object} variables - The variables object for reference resolution
  404. */
  405. const processLetterSpacingTokens = processTokenCollection("typography", "letter-spacing");
  406. /**
  407. * Process font family tokens
  408. * @param {Object} fontObj - The font family object from typography
  409. * @param {Object} result - The result object to populate
  410. */
  411. const processFontFamilyTokens = processTokenCollection("typography", "font-family");
  412. /**
  413. * Process typography tokens from design variables
  414. * @param {Object} typographyObj - The typography object from design variables
  415. * @param {Object} result - The result object to populate
  416. * @param {Object} variables - The variables object for reference resolution
  417. */
  418. function processTypographyTokens(typographyObj, result, variables) {
  419. // Process font families
  420. if (typographyObj["$font-family"]) {
  421. processFontFamilyTokens(typographyObj["$font-family"], result, variables);
  422. }
  423. // Process font sizes
  424. if (typographyObj["$font-size"]) {
  425. processFontSizeTokens(typographyObj["$font-size"], result, variables);
  426. }
  427. // Process font weights
  428. if (typographyObj["$font-weight"]) {
  429. processFontWeightTokens(typographyObj["$font-weight"], result, variables);
  430. }
  431. // Process line heights
  432. if (typographyObj["$line-height"]) {
  433. processLineHeightTokens(typographyObj["$line-height"], result, variables);
  434. }
  435. // Process letter spacing
  436. if (typographyObj["$letter-spacing"]) {
  437. processLetterSpacingTokens(typographyObj["$letter-spacing"], result, variables);
  438. }
  439. }
  440. /**
  441. * Process color tokens from design variables
  442. * @param {Object} colorObj - The color object from design variables
  443. * @param {String} parentPath - The parent path for nesting
  444. * @param {Object} result - The result object to populate
  445. */
  446. function processColorTokens(colorObj, parentPath, result, variables) {
  447. for (const key in colorObj) {
  448. if (typeof colorObj[key] === "object" && !Array.isArray(colorObj[key])) {
  449. const newPath = parentPath ? `${parentPath}-${key.replace(/\$/g, "")}` : key.replace(/\$/g, "");
  450. // If this is a color token with value and type
  451. if (colorObj[key].$type === "color" && colorObj[key].$value) {
  452. const name = parentPath ? `${parentPath}-${key.replace(/\$/g, "")}` : key.replace(/\$/g, "");
  453. const value = colorObj[key].$value;
  454. const cssVarName = `--color-${name.replace(/\$/g, "")}`;
  455. // Add to CSS variables for light mode
  456. if (
  457. colorObj[key].$variable_metadata &&
  458. colorObj[key].$variable_metadata.modes &&
  459. colorObj[key].$variable_metadata.modes.light
  460. ) {
  461. const lightValue = resolveColor(colorObj[key].$variable_metadata.modes.light, variables);
  462. result.cssVariables.light.push(`${cssVarName}: ${lightValue};`);
  463. if (shouldGenerateRawColorValue(name)) {
  464. const rawRgbValues = hexToRgbRaw(colorObj[key].$variable_metadata.modes.light, variables);
  465. result.cssVariables.light.push(`${cssVarName}-raw: ${rawRgbValues};`);
  466. }
  467. } else {
  468. const resolvedValue = resolveColor(value, variables);
  469. result.cssVariables.light.push(`${cssVarName}: ${resolvedValue};`);
  470. if (shouldGenerateRawColorValue(name)) {
  471. const rawRgbValues = hexToRgbRaw(value, variables);
  472. result.cssVariables.light.push(`${cssVarName}-raw: ${rawRgbValues};`);
  473. }
  474. }
  475. // Add to CSS variables for dark mode
  476. if (
  477. colorObj[key].$variable_metadata &&
  478. colorObj[key].$variable_metadata.modes &&
  479. colorObj[key].$variable_metadata.modes.dark
  480. ) {
  481. const darkValue = resolveColor(colorObj[key].$variable_metadata.modes.dark, variables);
  482. result.cssVariables.dark.push(`${cssVarName}: ${darkValue};`);
  483. if (shouldGenerateRawColorValue(name)) {
  484. const rawRgbValues = hexToRgbRaw(colorObj[key].$variable_metadata.modes.dark, variables);
  485. result.cssVariables.dark.push(`${cssVarName}-raw: ${rawRgbValues};`);
  486. }
  487. }
  488. // Add to JavaScript tokens
  489. addToJsTokens(result.jsTokens.colors, name.replace(/\$/g, ""), cssVarName);
  490. } else {
  491. // Recursively process nested color objects
  492. processColorTokens(colorObj[key], newPath, result, variables);
  493. }
  494. }
  495. }
  496. }
  497. /**
  498. * Process primitive colors
  499. * @param {Object} primitiveColors - The primitive colors object
  500. * @param {Object} result - The result object to populate
  501. * @param {Object} variables - The variables object for reference resolution
  502. */
  503. function processPrimitiveColors(primitiveColors, result, variables) {
  504. for (const colorFamily in primitiveColors) {
  505. const familyName = colorFamily.replace("$", "");
  506. for (const shade in primitiveColors[colorFamily]) {
  507. try {
  508. if (primitiveColors[colorFamily][shade].$type === "color" && primitiveColors[colorFamily][shade].$value) {
  509. const name = `${familyName}-${shade}`;
  510. const value = primitiveColors[colorFamily][shade].$value;
  511. const cssVarName = `--color-${name}`;
  512. // Add to CSS variables, converting to RGB format for opacity support
  513. const rgbValue = hexToRgb(value);
  514. result.cssVariables.light.push(`${cssVarName}: ${rgbValue};`);
  515. // Add raw RGB values for primitives to support translucent colors
  516. // if dark mode is available
  517. if (
  518. primitiveColors[colorFamily][shade].$variable_metadata &&
  519. primitiveColors[colorFamily][shade].$variable_metadata.modes &&
  520. primitiveColors[colorFamily][shade].$variable_metadata.modes.dark
  521. ) {
  522. const rawRgbValues = hexToRgbRaw(value);
  523. result.cssVariables.light.push(`${cssVarName}-raw: ${rawRgbValues};`);
  524. const darkValue = primitiveColors[colorFamily][shade].$variable_metadata.modes.dark;
  525. const darkRgbValue = hexToRgb(darkValue);
  526. result.cssVariables.dark.push(`${cssVarName}: ${darkRgbValue};`);
  527. // Add raw RGB values for dark mode
  528. const darkRawRgbValues = hexToRgbRaw(darkValue);
  529. result.cssVariables.dark.push(`${cssVarName}-raw: ${darkRawRgbValues};`);
  530. }
  531. // Add to JavaScript tokens
  532. if (!result.jsTokens.colors.primitive) {
  533. result.jsTokens.colors.primitive = {};
  534. }
  535. if (!result.jsTokens.colors.primitive[familyName]) {
  536. result.jsTokens.colors.primitive[familyName] = {};
  537. }
  538. result.jsTokens.colors.primitive[familyName][shade] = `var(${cssVarName})`;
  539. }
  540. } catch (error) {
  541. console.warn(`Warning: Error processing primitive color ${colorFamily}.${shade}:`, error.message);
  542. }
  543. }
  544. }
  545. }
  546. /**
  547. * Convert hex color to RGB format for opacity support
  548. * @param {string} hex - Hex color code
  549. * @returns {string} - RGB color format as rgb(r, g, b) or raw r g b
  550. * @param {boolean} raw - Whether to return the raw RGB values
  551. */
  552. function hexToRgb(hex, raw = false) {
  553. // Check if it's already in rgb/rgba format
  554. if (hex.startsWith("rgb")) {
  555. return raw ? hex.replace("rgb(", "").replace(")", "") : hex;
  556. }
  557. // Remove # if present
  558. hex = hex.replace(/^#/, "");
  559. // Parse the hex values
  560. let r;
  561. let g;
  562. let b;
  563. if (hex.length === 3) {
  564. // Convert 3-digit hex to 6-digit
  565. r = Number.parseInt(hex[0] + hex[0], 16);
  566. g = Number.parseInt(hex[1] + hex[1], 16);
  567. b = Number.parseInt(hex[2] + hex[2], 16);
  568. } else if (hex.length === 6) {
  569. r = Number.parseInt(hex.substring(0, 2), 16);
  570. g = Number.parseInt(hex.substring(2, 4), 16);
  571. b = Number.parseInt(hex.substring(4, 6), 16);
  572. } else {
  573. // Invalid hex, return as is
  574. return hex;
  575. }
  576. // Return RGB format
  577. return raw ? `${r} ${g} ${b}` : `rgb(${r} ${g} ${b})`;
  578. }
  579. /**
  580. * Convert hex color to raw RGB values for translucent color support
  581. * @param {string} hex - Hex color code or reference
  582. * @param {Object} variables - Variables for resolving references
  583. * @returns {string} - Raw RGB values as "r g b"
  584. */
  585. function hexToRgbRaw(hex, variables) {
  586. // If it's a reference, try to resolve it
  587. if (typeof hex === "string" && hex.startsWith("{") && hex.endsWith("}") && variables) {
  588. const resolvedValue = resolveReference(hex, variables);
  589. if (resolvedValue !== hex) {
  590. return hexToRgbRaw(resolvedValue);
  591. }
  592. }
  593. // Check if it's already in rgb/rgba format
  594. if (typeof hex === "string" && hex.startsWith("rgb")) {
  595. // Extract the RGB values from the rgb() format
  596. const match = hex.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
  597. if (match) {
  598. return `${match[1]} ${match[2]} ${match[3]}`;
  599. }
  600. return hex;
  601. }
  602. if (typeof hex === "string") {
  603. return hexToRgb(hex, true);
  604. }
  605. return hex;
  606. }
  607. /**
  608. * Resolve color values, handling references to other variables
  609. * @param {String} value - The color value to resolve
  610. * @param {Object} variables - The variables object for reference resolution
  611. * @param {Boolean} asCssVariable - Whether to return the value as a CSS variable
  612. * @returns {String} - The resolved color value
  613. */
  614. function resolveColor(value, variables, asCssVariable = true) {
  615. if (typeof value !== "string") return value;
  616. // Handle references like "{@primitives.$color.$sand.100}"
  617. if (value.startsWith("{") && value.endsWith("}")) {
  618. const reference = value.substring(1, value.length - 1);
  619. const parts = reference.split(".");
  620. if (asCssVariable) {
  621. // Remove 'primitive' from CSS variable references
  622. if (reference.startsWith("@primitives.$color")) {
  623. return `var(--color-${reference.replace("@primitives.$color.", "").replace(/[$\.]/g, "-").substring(1)})`;
  624. }
  625. return `var(--color-${reference
  626. .replace("@primitives.", "")
  627. .replace("$color.", "")
  628. .replace(/[$\.]/g, "-")
  629. .substring(1)})`;
  630. }
  631. // Navigate through the object to find the referenced value
  632. let current = variables;
  633. for (const part of parts) {
  634. if (current[part]) {
  635. current = current[part];
  636. } else {
  637. // If we can't resolve, return the CSS variable equivalent
  638. if (reference.startsWith("@primitives.$color")) {
  639. return `var(--color-${reference.replace("@primitives.$color.", "").replace(/[$\.]/g, "-").substring(1)})`;
  640. }
  641. return `var(--color-${reference.replace(/[@$\.]/g, "-").substring(1)})`;
  642. }
  643. }
  644. if (current.$value) {
  645. return hexToRgb(current.$value);
  646. }
  647. return value;
  648. }
  649. // Convert direct color values to RGB
  650. return hexToRgb(value);
  651. }
  652. /**
  653. * Resolve references to other variables
  654. * @param {String|Number} value - The value to resolve
  655. * @param {Object} variables - The variables object for reference resolution
  656. * @returns {String|Number} - The resolved value
  657. */
  658. function resolveReference(value, variables) {
  659. if (typeof value !== "string") return value;
  660. // Handle references like "{@sizing.$spacing.base}"
  661. if (value.startsWith("{") && value.endsWith("}")) {
  662. const reference = value.substring(1, value.length - 1);
  663. const parts = reference.split(".");
  664. // Navigate through the object to find the referenced value
  665. let current = variables;
  666. for (const part of parts) {
  667. if (current[part]) {
  668. current = current[part];
  669. } else {
  670. // If we can't resolve, return the original value
  671. return value;
  672. }
  673. }
  674. if (current.$value !== undefined) {
  675. return current.$value;
  676. }
  677. return value;
  678. }
  679. return value;
  680. }
  681. /**
  682. * Add a token to the JavaScript tokens object
  683. * @param {Object} obj - The object to add to
  684. * @param {String} path - The path to add at
  685. * @param {String} cssVarName - The CSS variable name
  686. */
  687. function addToJsTokens(obj, path, cssVarName) {
  688. const parts = path.split("-");
  689. let current = obj;
  690. // Handle the case where we have nested properties like "surface-hover"
  691. // which should be transformed to obj.surface.hover
  692. for (let i = 0; i < parts.length - 1; i++) {
  693. const part = parts[i];
  694. // Check if this is a terminal value (string) that we're trying to add properties to
  695. if (typeof current[part] === "string") {
  696. // Create a new object to replace the string value
  697. const oldValue = current[part];
  698. current[part] = {
  699. DEFAULT: oldValue,
  700. };
  701. } else if (!current[part]) {
  702. current[part] = {};
  703. }
  704. current = current[part];
  705. }
  706. const lastPart = parts[parts.length - 1];
  707. // If lastPart is something like "hover" and we're dealing with "surface-hover",
  708. // we should set it as a property of the "surface" object
  709. current[lastPart] = `var(${cssVarName})`;
  710. }
  711. /**
  712. * Generate CSS content
  713. * @param {Object} result - The processed tokens
  714. * @returns {String} - The CSS content
  715. */
  716. function generateCssContent(result) {
  717. let content = "// Generated from design-tokens.json - DO NOT EDIT DIRECTLY\n\n";
  718. // Light mode variables (default)
  719. content += ":root {\n";
  720. result.cssVariables.light.forEach((variable) => {
  721. content += ` ${variable}\n`;
  722. });
  723. content += "}\n\n";
  724. // Dark mode variables
  725. content += '[data-color-scheme="dark"] {\n';
  726. result.cssVariables.dark.forEach((variable) => {
  727. content += ` ${variable}\n`;
  728. });
  729. content += "}\n";
  730. return content;
  731. }
  732. /**
  733. * Transform color object structure for better Tailwind CSS compatibility
  734. * @param {Object} colors - The color object to transform
  735. * @returns {Object} - The transformed color object
  736. */
  737. function transformColorObjectForTailwind(colors) {
  738. const transformed = {};
  739. // Process each color category
  740. for (const category in colors) {
  741. transformed[category] = {};
  742. const colorGroup = colors[category];
  743. // Group variants like "surface-hover" under their base name with variants as properties
  744. for (const key in colorGroup) {
  745. const parts = key.split("-");
  746. // Skip if already processed
  747. if (parts.length === 1) {
  748. transformed[category][key] = colorGroup[key];
  749. continue;
  750. }
  751. // Handle cases like "surface-hover", "surface-active", etc.
  752. const baseName = parts[0];
  753. const variantName = parts.slice(1).join("-");
  754. if (!transformed[category][baseName]) {
  755. // Check if base color exists in original object
  756. if (colorGroup[baseName]) {
  757. transformed[category][baseName] = {
  758. DEFAULT: colorGroup[baseName],
  759. };
  760. } else {
  761. transformed[category][baseName] = {};
  762. }
  763. } else if (typeof transformed[category][baseName] === "string") {
  764. // Convert string value to object with DEFAULT property
  765. transformed[category][baseName] = {
  766. DEFAULT: transformed[category][baseName],
  767. };
  768. }
  769. // Add the variant
  770. transformed[category][baseName][variantName] = colorGroup[key];
  771. }
  772. }
  773. return transformed;
  774. }
  775. /**
  776. * Process JS tokens for output, merging primitive values
  777. * @param {Object} tokens - The tokens to process
  778. * @returns {Object} - The processed tokens
  779. */
  780. function processJsTokens(tokens) {
  781. // Merge primitive values at all levels
  782. const merged = mergePrimitiveValues(tokens);
  783. // Then transform colors for Tailwind if they exist
  784. if (merged.colors) {
  785. merged.colors = transformColorObjectForTailwind(merged.colors);
  786. }
  787. return merged;
  788. }
  789. /**
  790. * Merge primitive values from nested structures
  791. * @param {Object} values - The object to process
  792. * @returns {Object} - The processed object with primitive values merged
  793. */
  794. function mergePrimitiveValues(values) {
  795. if (typeof values !== "object" || values === null || Array.isArray(values)) {
  796. return values;
  797. }
  798. const result = {};
  799. // First, process all non-primitive keys and add them to the result
  800. for (const [key, value] of Object.entries(values)) {
  801. if (key !== "primitive") {
  802. if (typeof value === "object" && value !== null && !Array.isArray(value)) {
  803. result[key] = mergePrimitiveValues(value);
  804. } else {
  805. result[key] = value;
  806. }
  807. }
  808. }
  809. // Then, if there's a primitive key, merge up its values into the result
  810. if (values.primitive) {
  811. if (typeof values.primitive === "object" && !Array.isArray(values.primitive)) {
  812. // Handle nested primitive objects (e.g., typography.primitive.fontSize)
  813. for (const [primKey, primValue] of Object.entries(values.primitive)) {
  814. result[primKey] = mergePrimitiveValues(primValue);
  815. }
  816. }
  817. }
  818. return result;
  819. }
  820. /**
  821. * Generate JS content from tokens
  822. * @param {Object} jsTokens - The JS tokens to convert to content
  823. * @returns {string} - The JS content
  824. */
  825. function generateJsContent(jsTokens) {
  826. const content = `// This file is generated by the design-tokens-converter tool.
  827. // Do not edit this file directly. Edit design-tokens.json instead.
  828. const designTokens = ${JSON.stringify(processJsTokens(jsTokens), null, 2)};
  829. module.exports = designTokens;
  830. `;
  831. return content;
  832. }
  833. /**
  834. * Main function to run the design tokens converter
  835. */
  836. const designTokensConverter = async () => {
  837. try {
  838. console.log("Reading design variables file from:", designVariablesPath);
  839. // Check if file exists before trying to read it
  840. try {
  841. await fs.access(designVariablesPath);
  842. } catch (error) {
  843. console.error(`Error: The design-tokens.json file does not exist at ${designVariablesPath}`);
  844. console.log("Please create this file by exporting your design tokens from Figma");
  845. return { success: false, error: "Design tokens file not found" };
  846. }
  847. const designVariablesData = await fs.readFile(designVariablesPath, "utf8");
  848. try {
  849. const variables = JSON.parse(designVariablesData);
  850. console.log("Processing design variables...");
  851. const processed = processDesignVariables(variables);
  852. console.log("Generating CSS...");
  853. const cssContent = generateCssContent(processed);
  854. console.log("Generating JavaScript...");
  855. const jsContent = generateJsContent(processed.jsTokens);
  856. // Ensure directory exists
  857. const cssDir = path.dirname(cssOutputPath);
  858. await fs.mkdir(cssDir, { recursive: true });
  859. // Write files
  860. await fs.writeFile(cssOutputPath, cssContent);
  861. await fs.writeFile(jsOutputPath, jsContent);
  862. console.log(`CSS variables written to ${cssOutputPath}`);
  863. console.log(`JavaScript tokens written to ${jsOutputPath}`);
  864. return { success: true };
  865. } catch (parseError) {
  866. console.error("Error parsing design tokens JSON:");
  867. console.trace(parseError);
  868. console.log("Please ensure your design-tokens.json file contains valid JSON");
  869. return { success: false, error: "JSON parsing error" };
  870. }
  871. } catch (error) {
  872. console.error("Error:", error);
  873. return { success: false, error: error.message };
  874. }
  875. };
  876. // Execute the function when this script is run directly
  877. if (import.meta.url === `file://${process.argv[1]}`) {
  878. designTokensConverter().then((result) => {
  879. if (!result.success) {
  880. process.exit(1);
  881. }
  882. console.log("Design tokens conversion complete");
  883. });
  884. }
  885. export default designTokensConverter;