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;
|