| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028 |
- import fs from "node:fs/promises";
- import path from "node:path";
- import { fileURLToPath } from "node:url";
- const camelCase = (str) => {
- return str.replace(/-([a-z])/g, (g) => g[1].toUpperCase());
- };
- // Get current file directory for resolving paths
- const __filename = fileURLToPath(import.meta.url);
- const __dirname = path.dirname(__filename);
- const RAW_COLOR_VALUE_TOKENS = ["primary", "shadow", "outline", "surface", "accent", "background"];
- const shouldGenerateRawColorValue = (name) => {
- return RAW_COLOR_VALUE_TOKENS.some((token) => name.includes(token));
- };
- // Determine correct paths for the workspace
- const findWorkspaceRoot = () => {
- // We'll start with this file's directory and go up until we find the web directory
- let currentDir = __dirname;
- while (!currentDir.endsWith("web") && currentDir !== "/") {
- currentDir = path.dirname(currentDir);
- }
- if (!currentDir.endsWith("web")) {
- throw new Error("Could not find workspace root directory");
- }
- return currentDir;
- };
- const workspaceRoot = findWorkspaceRoot();
- // Paths
- const designVariablesPath = path.join(workspaceRoot, "design-tokens.json");
- const cssOutputPath = path.join(workspaceRoot, "libs/ui/src/tokens/tokens.scss");
- const jsOutputPath = path.join(workspaceRoot, "libs/ui/src/tokens/tokens.js");
- /**
- * Convert a value to rem units rounded to 4 decimal places no trailing zeros
- * @param {string} value - The value to convert
- * @returns {string} - The converted value in rem units
- */
- function convertToRem(value) {
- const remValue = (Number(value) / 16).toFixed(4).replace(/\.?0+$/, "");
- // Ensure 0 is returned as a unitless value
- if (remValue === "0") {
- return remValue;
- }
- return `${remValue}rem`;
- }
- /**
- * Process design variables and extract tokens
- * @param {Object} variables - The design variables object
- * @returns {Object} - Object containing tokens for CSS and JavaScript
- */
- function processDesignVariables(variables) {
- const result = {
- cssVariables: {
- light: [],
- dark: [],
- },
- jsTokens: {
- colors: {},
- spacing: {},
- typography: {},
- cornerRadius: {},
- },
- };
- // Process colors
- if (variables["@color"] && variables["@color"].$color) {
- processColorTokens(variables["@color"].$color, "", result, variables);
- }
- // Process primitives
- if (variables["@primitives"] && variables["@primitives"].$color) {
- processPrimitiveColors(variables["@primitives"].$color, result, variables);
- }
- // Process primitive spacing
- if (variables["@primitives"] && variables["@primitives"].$spacing) {
- processPrimitiveSpacing(variables["@primitives"].$spacing, result);
- }
- // Process primitive typography
- if (variables["@primitives"] && variables["@primitives"].$typography) {
- processPrimitiveTypography(variables["@primitives"].$typography, result);
- }
- // Process primitive corner-radius
- if (variables["@primitives"] && variables["@primitives"]["$corner-radius"]) {
- processPrimitiveCornerRadius(variables["@primitives"]["$corner-radius"], result);
- }
- // Process sizing tokens
- if (variables["@sizing"]) {
- processSizingTokens(variables["@sizing"], result, variables);
- }
- // Process typography
- if (variables["@typography"]) {
- processTypographyTokens(variables["@typography"], result, variables);
- }
- // Post-process for Tailwind compatibility
- result.jsTokens.colors = transformColorObjectForTailwind(result.jsTokens.colors);
- return result;
- }
- /**
- * Process primitive spacing tokens
- * @param {Object} spacingObj - The spacing object from primitives
- * @param {Object} result - The result object to populate
- */
- function processPrimitiveSpacing(spacingObj, result) {
- for (const key in spacingObj) {
- if (spacingObj[key].$type === "number" && spacingObj[key].$value !== undefined) {
- const name = key.replace("$", "");
- const value = spacingObj[key].$value;
- const cssVarName = `--spacing-${name}`;
- // Add to CSS variables
- result.cssVariables.light.push(`${cssVarName}: ${convertToRem(value)};`);
- // Add to JavaScript tokens
- if (!result.jsTokens.spacing.primitive) {
- result.jsTokens.spacing.primitive = {};
- }
- result.jsTokens.spacing.primitive[name] = `var(${cssVarName})`;
- }
- }
- }
- /**
- * Process primitive typography tokens
- * @param {Object} typographyObj - The typography object from primitives
- * @param {Object} result - The result object to populate
- */
- function processPrimitiveTypography(typographyObj, result) {
- // Process font sizes
- if (typographyObj["$font-size"]) {
- for (const key in typographyObj["$font-size"]) {
- if (
- typographyObj["$font-size"][key].$type === "number" &&
- typographyObj["$font-size"][key].$value !== undefined
- ) {
- const name = key.replace("$", "");
- const value = typographyObj["$font-size"][key].$value;
- const cssVarName = `--font-size-${name}`;
- // Add to CSS variables
- result.cssVariables.light.push(`${cssVarName}: ${convertToRem(value)};`);
- // Add to JavaScript tokens
- if (!result.jsTokens.typography.fontSize) {
- result.jsTokens.typography.fontSize = {};
- }
- if (!result.jsTokens.typography.fontSize.primitive) {
- result.jsTokens.typography.fontSize.primitive = {};
- }
- result.jsTokens.typography.fontSize.primitive[name] = `var(${cssVarName})`;
- }
- }
- }
- // Process font weights
- if (typographyObj["$font-weight"]) {
- for (const key in typographyObj["$font-weight"]) {
- if (
- typographyObj["$font-weight"][key].$type === "number" &&
- typographyObj["$font-weight"][key].$value !== undefined
- ) {
- const name = key.replace("$", "");
- const value = typographyObj["$font-weight"][key].$value;
- const cssVarName = `--font-weight-${name}`;
- // Add to CSS variables
- result.cssVariables.light.push(`${cssVarName}: ${value};`);
- // Add to JavaScript tokens
- if (!result.jsTokens.typography.fontWeight) {
- result.jsTokens.typography.fontWeight = {};
- }
- if (!result.jsTokens.typography.fontWeight.primitive) {
- result.jsTokens.typography.fontWeight.primitive = {};
- }
- result.jsTokens.typography.fontWeight.primitive[name] = `var(${cssVarName})`;
- }
- }
- }
- // Process line heights
- if (typographyObj["$line-height"]) {
- for (const key in typographyObj["$line-height"]) {
- if (
- typographyObj["$line-height"][key].$type === "number" &&
- typographyObj["$line-height"][key].$value !== undefined
- ) {
- const name = key.replace("$", "");
- const value = typographyObj["$line-height"][key].$value;
- const cssVarName = `--line-height-${name}`;
- // Add to CSS variables
- result.cssVariables.light.push(`${cssVarName}: ${convertToRem(value)};`);
- // Add to JavaScript tokens
- if (!result.jsTokens.typography.lineHeight) {
- result.jsTokens.typography.lineHeight = {};
- }
- if (!result.jsTokens.typography.lineHeight.primitive) {
- result.jsTokens.typography.lineHeight.primitive = {};
- }
- result.jsTokens.typography.lineHeight.primitive[name] = `var(${cssVarName})`;
- }
- }
- }
- // Process letter spacing
- if (typographyObj["$letter-spacing"]) {
- for (const key in typographyObj["$letter-spacing"]) {
- if (
- typographyObj["$letter-spacing"][key].$type === "number" &&
- typographyObj["$letter-spacing"][key].$value !== undefined
- ) {
- const name = key.replace("$", "");
- const value = typographyObj["$letter-spacing"][key].$value;
- const cssVarName = `--letter-spacing-${name}`;
- // Add to CSS variables
- result.cssVariables.light.push(`${cssVarName}: ${convertToRem(value)};`);
- // Add to JavaScript tokens
- if (!result.jsTokens.typography.letterSpacing) {
- result.jsTokens.typography.letterSpacing = {};
- }
- if (!result.jsTokens.typography.letterSpacing.primitive) {
- result.jsTokens.typography.letterSpacing.primitive = {};
- }
- result.jsTokens.typography.letterSpacing.primitive[name] = `var(${cssVarName})`;
- }
- }
- }
- // Process font families
- if (typographyObj["$font-family"]) {
- for (const key in typographyObj["$font-family"]) {
- if (typographyObj["$font-family"][key].$value !== undefined) {
- const name = key.replace("$", "");
- const value = typographyObj["$font-family"][key].$value;
- const cssVarName = `--font-family-${name}`;
- // Add to CSS variables
- result.cssVariables.light.push(`${cssVarName}: "${value}";`);
- // Add to JavaScript tokens
- if (!result.jsTokens.typography.fontFamily) {
- result.jsTokens.typography.fontFamily = {};
- }
- if (!result.jsTokens.typography.fontFamily.primitive) {
- result.jsTokens.typography.fontFamily.primitive = {};
- }
- result.jsTokens.typography.fontFamily.primitive[name] = `var(${cssVarName})`;
- }
- }
- }
- }
- /**
- * Process primitive corner radius tokens
- * @param {Object} cornerRadiusObj - The corner radius object from primitives
- * @param {Object} result - The result object to populate
- */
- function processPrimitiveCornerRadius(cornerRadiusObj, result) {
- for (const key in cornerRadiusObj) {
- if (cornerRadiusObj[key].$type === "number" && cornerRadiusObj[key].$value !== undefined) {
- const name = key.replace("$", "");
- const value = cornerRadiusObj[key].$value;
- const cssVarName = `--corner-radius-${name}`;
- let resolvedValue;
- if (typeof value === "string" && value.startsWith("{") && value.endsWith("}")) {
- const reference = value.substring(1, value.length - 1);
- const parts = reference.split(".");
- // If it's a reference to a primitive spacing value, directly use the corresponding CSS variable
- if (parts[0] === "@primitives") {
- const collectionKey = parts[1].replace("$", "");
- const valueKey = parts[2].replace("$", "");
- resolvedValue = `var(--${collectionKey}-${valueKey})`;
- result.cssVariables.light.push(`${cssVarName}: ${resolvedValue};`);
- } else {
- // Otherwise, try to resolve the value normally
- resolvedValue = resolveReference(value, variables);
- result.cssVariables.light.push(`${cssVarName}: ${convertToRem(resolvedValue)};`);
- }
- } else {
- // Not a reference, use directly
- resolvedValue = value;
- result.cssVariables.light.push(`${cssVarName}: ${convertToRem(resolvedValue)};`);
- }
- // Add to JavaScript tokens
- if (!result.jsTokens.cornerRadius.primitive) {
- result.jsTokens.cornerRadius.primitive = {};
- }
- result.jsTokens.cornerRadius.primitive[name] = `var(${cssVarName})`;
- }
- }
- }
- /**
- * Process sizing tokens from design variables
- * @param {Object} sizingObj - The spacing object from design variables
- * @param {Object} result - The result object to populate
- * @param {Object} variables - The variables object for reference resolution
- * @param {String} parentKey - The parent key for nested tokens
- */
- function processSizingTokens(sizingObj, result, variables, parentKey = "") {
- for (const key in sizingObj) {
- if (key.startsWith("$")) {
- // process nested keys
- processSizingTokens(
- sizingObj[key],
- result,
- variables,
- // Fix the variable name from corder to corner
- `${parentKey ? `${parentKey}-` : ""}${key.replace(/\$/g, "").replace("corder", "corner")}`,
- );
- continue;
- }
- if (sizingObj[key].$type === "number" && sizingObj[key].$value !== undefined) {
- const name = key.replace("$", "");
- const value = sizingObj[key].$value;
- const tokenKey = parentKey || key;
- const jsTokenKey = camelCase(tokenKey.replace("$", ""));
- const cssVarName = `--${tokenKey}-${name}`;
- let resolvedValue;
- if (typeof value === "string" && value.startsWith("{") && value.endsWith("}")) {
- const reference = value.substring(1, value.length - 1);
- const parts = reference.split(".");
- // If it's a reference to a primitive spacing value, directly use the corresponding CSS variable
- if (parts[0] === "@primitives") {
- const collectionKey = parts[1].replace("$", "");
- const valueKey = parts[2].replace("$", "");
- resolvedValue = `var(--${collectionKey}-${valueKey})`;
- result.cssVariables.light.push(`${cssVarName}: ${resolvedValue};`);
- } else {
- // Otherwise, try to resolve the value normally
- resolvedValue = resolveReference(value, variables);
- result.cssVariables.light.push(`${cssVarName}: ${convertToRem(resolvedValue)};`);
- }
- } else {
- // Not a reference, use directly
- resolvedValue = value;
- result.cssVariables.light.push(`${cssVarName}: ${convertToRem(resolvedValue)};`);
- }
- // Add to JavaScript tokens
- if (!result.jsTokens[jsTokenKey]) {
- result.jsTokens[jsTokenKey] = {};
- }
- result.jsTokens[jsTokenKey][name] = `var(${cssVarName})`;
- }
- }
- }
- function processTokenCollection(collectionKey, subCollectionKey) {
- const collectionJsKey = camelCase(collectionKey);
- const subCollectionJsKey = camelCase(subCollectionKey);
- const subCollectionKeyRegex = new RegExp(`${subCollectionKey}\\-?`, "g");
- return function process(tokenCollection, result, variables, parentKey = subCollectionKey) {
- for (const key in tokenCollection) {
- if (key.startsWith("$")) {
- // process nested keys
- process(tokenCollection[key], result, variables, `${parentKey ? `${parentKey}-` : ""}${key.replace("$", "")}`);
- continue;
- }
- if (tokenCollection[key].$value !== undefined) {
- const isNumber = tokenCollection[key].$type === "number";
- const name = key.replace("$", "");
- const value = tokenCollection[key].$value;
- const cssVarName = `--${parentKey}-${name}`;
- let resolvedValue;
- if (typeof value === "string" && value.startsWith("{") && value.endsWith("}")) {
- const reference = value.substring(1, value.length - 1);
- const parts = reference.replace(`$${collectionKey}.`, "").split(".");
- // If it's a reference to a primitive spacing value, directly use the corresponding CSS variable
- if (parts[0] === "@primitives") {
- const collectionKey = parts[1].replace("$", "");
- const valueKey = parts[2].replace("$", "");
- resolvedValue = `var(--${collectionKey}-${valueKey})`;
- result.cssVariables.light.push(`${cssVarName}: ${resolvedValue};`);
- } else {
- // Otherwise, try to resolve the value normally
- resolvedValue = resolveReference(value, variables);
- result.cssVariables.light.push(`${cssVarName}: ${isNumber ? convertToRem(resolvedValue) : resolvedValue};`);
- }
- } else {
- // Not a reference, use directly
- resolvedValue = value;
- result.cssVariables.light.push(`${cssVarName}: ${resolvedValue};`);
- }
- // Add to JavaScript tokens
- if (!result.jsTokens[collectionJsKey]) {
- result.jsTokens[collectionJsKey] = {};
- }
- if (!result.jsTokens[collectionJsKey][subCollectionJsKey]) {
- result.jsTokens[collectionJsKey][subCollectionJsKey] = {};
- }
- const jsKey = `${parentKey ? `${parentKey}-` : ""}${name}`;
- result.jsTokens[collectionJsKey][subCollectionJsKey][jsKey.replace(subCollectionKeyRegex, "")] =
- `var(${cssVarName})`;
- }
- }
- };
- }
- /**
- * Process font size tokens
- * @param {Object} fontSizeObj - The font size object from typography
- * @param {Object} result - The result object to populate
- * @param {Object} variables - The variables object for reference resolution
- * @param {String} parentKey - The parent key for nested tokens
- */
- const processFontSizeTokens = processTokenCollection("typography", "font-size");
- /**
- * Process font weight tokens
- * @param {Object} fontWeightObj - The font weight object from typography
- * @param {Object} result - The result object to populate
- * @param {Object} variables - The variables object for reference resolution
- */
- const processFontWeightTokens = processTokenCollection("typography", "font-weight");
- /**
- * Process line height tokens
- * @param {Object} lineHeightObj - The line height object from typography
- * @param {Object} result - The result object to populate
- * @param {Object} variables - The variables object for reference resolution
- */
- const processLineHeightTokens = processTokenCollection("typography", "line-height");
- /**
- * Process letter spacing tokens
- * @param {Object} letterSpacingObj - The letter spacing object from typography
- * @param {Object} result - The result object to populate
- * @param {Object} variables - The variables object for reference resolution
- */
- const processLetterSpacingTokens = processTokenCollection("typography", "letter-spacing");
- /**
- * Process font family tokens
- * @param {Object} fontObj - The font family object from typography
- * @param {Object} result - The result object to populate
- */
- const processFontFamilyTokens = processTokenCollection("typography", "font-family");
- /**
- * Process typography tokens from design variables
- * @param {Object} typographyObj - The typography object from design variables
- * @param {Object} result - The result object to populate
- * @param {Object} variables - The variables object for reference resolution
- */
- function processTypographyTokens(typographyObj, result, variables) {
- // Process font families
- if (typographyObj["$font-family"]) {
- processFontFamilyTokens(typographyObj["$font-family"], result, variables);
- }
- // Process font sizes
- if (typographyObj["$font-size"]) {
- processFontSizeTokens(typographyObj["$font-size"], result, variables);
- }
- // Process font weights
- if (typographyObj["$font-weight"]) {
- processFontWeightTokens(typographyObj["$font-weight"], result, variables);
- }
- // Process line heights
- if (typographyObj["$line-height"]) {
- processLineHeightTokens(typographyObj["$line-height"], result, variables);
- }
- // Process letter spacing
- if (typographyObj["$letter-spacing"]) {
- processLetterSpacingTokens(typographyObj["$letter-spacing"], result, variables);
- }
- }
- /**
- * Process color tokens from design variables
- * @param {Object} colorObj - The color object from design variables
- * @param {String} parentPath - The parent path for nesting
- * @param {Object} result - The result object to populate
- */
- function processColorTokens(colorObj, parentPath, result, variables) {
- for (const key in colorObj) {
- if (typeof colorObj[key] === "object" && !Array.isArray(colorObj[key])) {
- const newPath = parentPath ? `${parentPath}-${key.replace(/\$/g, "")}` : key.replace(/\$/g, "");
- // If this is a color token with value and type
- if (colorObj[key].$type === "color" && colorObj[key].$value) {
- const name = parentPath ? `${parentPath}-${key.replace(/\$/g, "")}` : key.replace(/\$/g, "");
- const value = colorObj[key].$value;
- const cssVarName = `--color-${name.replace(/\$/g, "")}`;
- // Add to CSS variables for light mode
- if (
- colorObj[key].$variable_metadata &&
- colorObj[key].$variable_metadata.modes &&
- colorObj[key].$variable_metadata.modes.light
- ) {
- const lightValue = resolveColor(colorObj[key].$variable_metadata.modes.light, variables);
- result.cssVariables.light.push(`${cssVarName}: ${lightValue};`);
- if (shouldGenerateRawColorValue(name)) {
- const rawRgbValues = hexToRgbRaw(colorObj[key].$variable_metadata.modes.light, variables);
- result.cssVariables.light.push(`${cssVarName}-raw: ${rawRgbValues};`);
- }
- } else {
- const resolvedValue = resolveColor(value, variables);
- result.cssVariables.light.push(`${cssVarName}: ${resolvedValue};`);
- if (shouldGenerateRawColorValue(name)) {
- const rawRgbValues = hexToRgbRaw(value, variables);
- result.cssVariables.light.push(`${cssVarName}-raw: ${rawRgbValues};`);
- }
- }
- // Add to CSS variables for dark mode
- if (
- colorObj[key].$variable_metadata &&
- colorObj[key].$variable_metadata.modes &&
- colorObj[key].$variable_metadata.modes.dark
- ) {
- const darkValue = resolveColor(colorObj[key].$variable_metadata.modes.dark, variables);
- result.cssVariables.dark.push(`${cssVarName}: ${darkValue};`);
- if (shouldGenerateRawColorValue(name)) {
- const rawRgbValues = hexToRgbRaw(colorObj[key].$variable_metadata.modes.dark, variables);
- result.cssVariables.dark.push(`${cssVarName}-raw: ${rawRgbValues};`);
- }
- }
- // Add to JavaScript tokens
- addToJsTokens(result.jsTokens.colors, name.replace(/\$/g, ""), cssVarName);
- } else {
- // Recursively process nested color objects
- processColorTokens(colorObj[key], newPath, result, variables);
- }
- }
- }
- }
- /**
- * Process primitive colors
- * @param {Object} primitiveColors - The primitive colors object
- * @param {Object} result - The result object to populate
- * @param {Object} variables - The variables object for reference resolution
- */
- function processPrimitiveColors(primitiveColors, result, variables) {
- for (const colorFamily in primitiveColors) {
- const familyName = colorFamily.replace("$", "");
- for (const shade in primitiveColors[colorFamily]) {
- try {
- if (primitiveColors[colorFamily][shade].$type === "color" && primitiveColors[colorFamily][shade].$value) {
- const name = `${familyName}-${shade}`;
- const value = primitiveColors[colorFamily][shade].$value;
- const cssVarName = `--color-${name}`;
- // Add to CSS variables, converting to RGB format for opacity support
- const rgbValue = hexToRgb(value);
- result.cssVariables.light.push(`${cssVarName}: ${rgbValue};`);
- // Add raw RGB values for primitives to support translucent colors
- // if dark mode is available
- if (
- primitiveColors[colorFamily][shade].$variable_metadata &&
- primitiveColors[colorFamily][shade].$variable_metadata.modes &&
- primitiveColors[colorFamily][shade].$variable_metadata.modes.dark
- ) {
- const rawRgbValues = hexToRgbRaw(value);
- result.cssVariables.light.push(`${cssVarName}-raw: ${rawRgbValues};`);
- const darkValue = primitiveColors[colorFamily][shade].$variable_metadata.modes.dark;
- const darkRgbValue = hexToRgb(darkValue);
- result.cssVariables.dark.push(`${cssVarName}: ${darkRgbValue};`);
- // Add raw RGB values for dark mode
- const darkRawRgbValues = hexToRgbRaw(darkValue);
- result.cssVariables.dark.push(`${cssVarName}-raw: ${darkRawRgbValues};`);
- }
- // Add to JavaScript tokens
- if (!result.jsTokens.colors.primitive) {
- result.jsTokens.colors.primitive = {};
- }
- if (!result.jsTokens.colors.primitive[familyName]) {
- result.jsTokens.colors.primitive[familyName] = {};
- }
- result.jsTokens.colors.primitive[familyName][shade] = `var(${cssVarName})`;
- }
- } catch (error) {
- console.warn(`Warning: Error processing primitive color ${colorFamily}.${shade}:`, error.message);
- }
- }
- }
- }
- /**
- * Convert hex color to RGB format for opacity support
- * @param {string} hex - Hex color code
- * @returns {string} - RGB color format as rgb(r, g, b) or raw r g b
- * @param {boolean} raw - Whether to return the raw RGB values
- */
- function hexToRgb(hex, raw = false) {
- // Check if it's already in rgb/rgba format
- if (hex.startsWith("rgb")) {
- return raw ? hex.replace("rgb(", "").replace(")", "") : hex;
- }
- // Remove # if present
- hex = hex.replace(/^#/, "");
- // Parse the hex values
- let r;
- let g;
- let b;
- if (hex.length === 3) {
- // Convert 3-digit hex to 6-digit
- r = Number.parseInt(hex[0] + hex[0], 16);
- g = Number.parseInt(hex[1] + hex[1], 16);
- b = Number.parseInt(hex[2] + hex[2], 16);
- } else if (hex.length === 6) {
- r = Number.parseInt(hex.substring(0, 2), 16);
- g = Number.parseInt(hex.substring(2, 4), 16);
- b = Number.parseInt(hex.substring(4, 6), 16);
- } else {
- // Invalid hex, return as is
- return hex;
- }
- // Return RGB format
- return raw ? `${r} ${g} ${b}` : `rgb(${r} ${g} ${b})`;
- }
- /**
- * Convert hex color to raw RGB values for translucent color support
- * @param {string} hex - Hex color code or reference
- * @param {Object} variables - Variables for resolving references
- * @returns {string} - Raw RGB values as "r g b"
- */
- function hexToRgbRaw(hex, variables) {
- // If it's a reference, try to resolve it
- if (typeof hex === "string" && hex.startsWith("{") && hex.endsWith("}") && variables) {
- const resolvedValue = resolveReference(hex, variables);
- if (resolvedValue !== hex) {
- return hexToRgbRaw(resolvedValue);
- }
- }
- // Check if it's already in rgb/rgba format
- if (typeof hex === "string" && hex.startsWith("rgb")) {
- // Extract the RGB values from the rgb() format
- const match = hex.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
- if (match) {
- return `${match[1]} ${match[2]} ${match[3]}`;
- }
- return hex;
- }
- if (typeof hex === "string") {
- return hexToRgb(hex, true);
- }
- return hex;
- }
- /**
- * Resolve color values, handling references to other variables
- * @param {String} value - The color value to resolve
- * @param {Object} variables - The variables object for reference resolution
- * @param {Boolean} asCssVariable - Whether to return the value as a CSS variable
- * @returns {String} - The resolved color value
- */
- function resolveColor(value, variables, asCssVariable = true) {
- if (typeof value !== "string") return value;
- // Handle references like "{@primitives.$color.$sand.100}"
- if (value.startsWith("{") && value.endsWith("}")) {
- const reference = value.substring(1, value.length - 1);
- const parts = reference.split(".");
- if (asCssVariable) {
- // Remove 'primitive' from CSS variable references
- if (reference.startsWith("@primitives.$color")) {
- return `var(--color-${reference.replace("@primitives.$color.", "").replace(/[$\.]/g, "-").substring(1)})`;
- }
- return `var(--color-${reference
- .replace("@primitives.", "")
- .replace("$color.", "")
- .replace(/[$\.]/g, "-")
- .substring(1)})`;
- }
- // Navigate through the object to find the referenced value
- let current = variables;
- for (const part of parts) {
- if (current[part]) {
- current = current[part];
- } else {
- // If we can't resolve, return the CSS variable equivalent
- if (reference.startsWith("@primitives.$color")) {
- return `var(--color-${reference.replace("@primitives.$color.", "").replace(/[$\.]/g, "-").substring(1)})`;
- }
- return `var(--color-${reference.replace(/[@$\.]/g, "-").substring(1)})`;
- }
- }
- if (current.$value) {
- return hexToRgb(current.$value);
- }
- return value;
- }
- // Convert direct color values to RGB
- return hexToRgb(value);
- }
- /**
- * Resolve references to other variables
- * @param {String|Number} value - The value to resolve
- * @param {Object} variables - The variables object for reference resolution
- * @returns {String|Number} - The resolved value
- */
- function resolveReference(value, variables) {
- if (typeof value !== "string") return value;
- // Handle references like "{@sizing.$spacing.base}"
- if (value.startsWith("{") && value.endsWith("}")) {
- const reference = value.substring(1, value.length - 1);
- const parts = reference.split(".");
- // Navigate through the object to find the referenced value
- let current = variables;
- for (const part of parts) {
- if (current[part]) {
- current = current[part];
- } else {
- // If we can't resolve, return the original value
- return value;
- }
- }
- if (current.$value !== undefined) {
- return current.$value;
- }
- return value;
- }
- return value;
- }
- /**
- * Add a token to the JavaScript tokens object
- * @param {Object} obj - The object to add to
- * @param {String} path - The path to add at
- * @param {String} cssVarName - The CSS variable name
- */
- function addToJsTokens(obj, path, cssVarName) {
- const parts = path.split("-");
- let current = obj;
- // Handle the case where we have nested properties like "surface-hover"
- // which should be transformed to obj.surface.hover
- for (let i = 0; i < parts.length - 1; i++) {
- const part = parts[i];
- // Check if this is a terminal value (string) that we're trying to add properties to
- if (typeof current[part] === "string") {
- // Create a new object to replace the string value
- const oldValue = current[part];
- current[part] = {
- DEFAULT: oldValue,
- };
- } else if (!current[part]) {
- current[part] = {};
- }
- current = current[part];
- }
- const lastPart = parts[parts.length - 1];
- // If lastPart is something like "hover" and we're dealing with "surface-hover",
- // we should set it as a property of the "surface" object
- current[lastPart] = `var(${cssVarName})`;
- }
- /**
- * Generate CSS content
- * @param {Object} result - The processed tokens
- * @returns {String} - The CSS content
- */
- function generateCssContent(result) {
- let content = "// Generated from design-tokens.json - DO NOT EDIT DIRECTLY\n\n";
- // Light mode variables (default)
- content += ":root {\n";
- result.cssVariables.light.forEach((variable) => {
- content += ` ${variable}\n`;
- });
- content += "}\n\n";
- // Dark mode variables
- content += '[data-color-scheme="dark"] {\n';
- result.cssVariables.dark.forEach((variable) => {
- content += ` ${variable}\n`;
- });
- content += "}\n";
- return content;
- }
- /**
- * Transform color object structure for better Tailwind CSS compatibility
- * @param {Object} colors - The color object to transform
- * @returns {Object} - The transformed color object
- */
- function transformColorObjectForTailwind(colors) {
- const transformed = {};
- // Process each color category
- for (const category in colors) {
- transformed[category] = {};
- const colorGroup = colors[category];
- // Group variants like "surface-hover" under their base name with variants as properties
- for (const key in colorGroup) {
- const parts = key.split("-");
- // Skip if already processed
- if (parts.length === 1) {
- transformed[category][key] = colorGroup[key];
- continue;
- }
- // Handle cases like "surface-hover", "surface-active", etc.
- const baseName = parts[0];
- const variantName = parts.slice(1).join("-");
- if (!transformed[category][baseName]) {
- // Check if base color exists in original object
- if (colorGroup[baseName]) {
- transformed[category][baseName] = {
- DEFAULT: colorGroup[baseName],
- };
- } else {
- transformed[category][baseName] = {};
- }
- } else if (typeof transformed[category][baseName] === "string") {
- // Convert string value to object with DEFAULT property
- transformed[category][baseName] = {
- DEFAULT: transformed[category][baseName],
- };
- }
- // Add the variant
- transformed[category][baseName][variantName] = colorGroup[key];
- }
- }
- return transformed;
- }
- /**
- * Process JS tokens for output, merging primitive values
- * @param {Object} tokens - The tokens to process
- * @returns {Object} - The processed tokens
- */
- function processJsTokens(tokens) {
- // Merge primitive values at all levels
- const merged = mergePrimitiveValues(tokens);
- // Then transform colors for Tailwind if they exist
- if (merged.colors) {
- merged.colors = transformColorObjectForTailwind(merged.colors);
- }
- return merged;
- }
- /**
- * Merge primitive values from nested structures
- * @param {Object} values - The object to process
- * @returns {Object} - The processed object with primitive values merged
- */
- function mergePrimitiveValues(values) {
- if (typeof values !== "object" || values === null || Array.isArray(values)) {
- return values;
- }
- const result = {};
- // First, process all non-primitive keys and add them to the result
- for (const [key, value] of Object.entries(values)) {
- if (key !== "primitive") {
- if (typeof value === "object" && value !== null && !Array.isArray(value)) {
- result[key] = mergePrimitiveValues(value);
- } else {
- result[key] = value;
- }
- }
- }
- // Then, if there's a primitive key, merge up its values into the result
- if (values.primitive) {
- if (typeof values.primitive === "object" && !Array.isArray(values.primitive)) {
- // Handle nested primitive objects (e.g., typography.primitive.fontSize)
- for (const [primKey, primValue] of Object.entries(values.primitive)) {
- result[primKey] = mergePrimitiveValues(primValue);
- }
- }
- }
- return result;
- }
- /**
- * Generate JS content from tokens
- * @param {Object} jsTokens - The JS tokens to convert to content
- * @returns {string} - The JS content
- */
- function generateJsContent(jsTokens) {
- const content = `// This file is generated by the design-tokens-converter tool.
- // Do not edit this file directly. Edit design-tokens.json instead.
- const designTokens = ${JSON.stringify(processJsTokens(jsTokens), null, 2)};
- module.exports = designTokens;
- `;
- return content;
- }
- /**
- * Main function to run the design tokens converter
- */
- const designTokensConverter = async () => {
- try {
- console.log("Reading design variables file from:", designVariablesPath);
- // Check if file exists before trying to read it
- try {
- await fs.access(designVariablesPath);
- } catch (error) {
- console.error(`Error: The design-tokens.json file does not exist at ${designVariablesPath}`);
- console.log("Please create this file by exporting your design tokens from Figma");
- return { success: false, error: "Design tokens file not found" };
- }
- const designVariablesData = await fs.readFile(designVariablesPath, "utf8");
- try {
- const variables = JSON.parse(designVariablesData);
- console.log("Processing design variables...");
- const processed = processDesignVariables(variables);
- console.log("Generating CSS...");
- const cssContent = generateCssContent(processed);
- console.log("Generating JavaScript...");
- const jsContent = generateJsContent(processed.jsTokens);
- // Ensure directory exists
- const cssDir = path.dirname(cssOutputPath);
- await fs.mkdir(cssDir, { recursive: true });
- // Write files
- await fs.writeFile(cssOutputPath, cssContent);
- await fs.writeFile(jsOutputPath, jsContent);
- console.log(`CSS variables written to ${cssOutputPath}`);
- console.log(`JavaScript tokens written to ${jsOutputPath}`);
- return { success: true };
- } catch (parseError) {
- console.error("Error parsing design tokens JSON:");
- console.trace(parseError);
- console.log("Please ensure your design-tokens.json file contains valid JSON");
- return { success: false, error: "JSON parsing error" };
- }
- } catch (error) {
- console.error("Error:", error);
- return { success: false, error: error.message };
- }
- };
- // Execute the function when this script is run directly
- if (import.meta.url === `file://${process.argv[1]}`) {
- designTokensConverter().then((result) => {
- if (!result.success) {
- process.exit(1);
- }
- console.log("Design tokens conversion complete");
- });
- }
- export default designTokensConverter;
|