Files
AppFlowy-Web/scripts/system-token/convert-tokens.cjs
Kilu.He 15ec240887 chore: import new ui system (#73)
* chore: import new ui system

* chore: modified padding and rounded

* chore: remove vertical padding

* chore: add shadow

* chore: add shadcn popover (#76)

* chore: add circle progress (#77)

* chore: add input (#78)

* feat: add shadcn toast (#80)

* chore: initial import

* chore: style

* chore: important

* chore: add dropdown menu (#81)

* feat: add shadcn tooltip (#79)

* chore: initial import

* chore: style

* chore: fix text size

* chore: add tooltip shortcut

* chore: code cleanup and add flex col

* chore: add avatar (#82)

* chore: add dialog (#83)

---------

Co-authored-by: Richard Shiue <71320345+richardshiue@users.noreply.github.com>
2025-04-02 15:29:42 +08:00

269 lines
7.9 KiB
JavaScript

const fs = require('fs');
const path = require('path');
// Ensure target directory exists
function ensureDirectoryExists (dirPath) {
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true });
console.log(`Created directory: ${dirPath}`);
}
}
// Read JSON file
function readJsonFile (filePath) {
try {
const data = fs.readFileSync(filePath, 'utf8');
return JSON.parse(data);
} catch (error) {
console.error(`Error reading file ${filePath}:`, error);
process.exit(1);
}
}
// Resolve reference values
function resolveReferences (tokens, primitiveTokens) {
const resolvedTokens = JSON.parse(JSON.stringify(tokens));
function resolveValue (value, visitedRefs = new Set()) {
if (typeof value !== 'string' || !value.startsWith('{') || !value.endsWith('}')) {
return value;
}
const reference = value.slice(1, -1);
if (visitedRefs.has(reference)) {
console.warn(`Circular reference detected: ${reference}`);
return value;
}
visitedRefs.add(reference);
// Split reference path
const parts = reference.split('.');
let current;
// First look in primitive tokens
if (primitiveTokens) {
current = primitiveTokens;
for (const part of parts) {
if (current && current[part]) {
current = current[part];
} else {
current = null;
break;
}
}
}
// If not found in primitives, look in current tokens
if (!current) {
current = resolvedTokens;
for (const part of parts) {
if (current && current[part]) {
current = current[part];
} else {
console.warn(`Reference not found: ${reference}`);
return value;
}
}
}
if (current && current.$value) {
return resolveValue(current.$value, visitedRefs);
}
return current;
}
function processObject (obj) {
for (const key in obj) {
if (typeof obj[key] === 'object' && obj[key] !== null) {
processObject(obj[key]);
} else if (key === '$value') {
obj.$value = resolveValue(obj.$value);
}
}
}
processObject(resolvedTokens);
return resolvedTokens;
}
// Convert to CSS variables
function convertToCssVariables (tokens, prefix = '', isDarkTheme = false, sourceFile) {
let variableNames = []; // Store all generated variable names
// Create CSS file header with warning comments
let css = `/**
* Design system ${isDarkTheme ? 'dark' : 'light'} theme variables
* AUTO-GENERATED FILE - DO NOT EDIT DIRECTLY
*
* Generated from: ${sourceFile}
* Generated time: ${new Date().toISOString()}
*
* To modify these values, edit the source JSON files and run the token conversion script:
* node scripts/system-token/convert-tokens.cjs
*/
`;
// Select root selector based on theme
const rootSelector = isDarkTheme ? ':root[data-dark-mode=true]' : ':root';
function processTokens (obj, path = '') {
for (const key in obj) {
const newPath = path ? `${path}-${key}` : key;
// If object but not a value object (i.e., doesn't have $value)
if (typeof obj[key] === 'object' && obj[key] !== null && !obj[key].$value) {
processTokens(obj[key], newPath);
}
// If it's a value object (has $value)
else if (typeof obj[key] === 'object' && obj[key] !== null && obj[key].$value) {
const token = obj[key];
const variableName = `--${prefix}${newPath.toLowerCase().replace(/_/g, '-')}`;
let value = token.$value;
// Add to variable names list
variableNames.push(variableName.substring(2)); // Remove leading --
// Format value based on type
if (token.$type === 'dimension') {
// Keep dimension values as is, they already include units like px
css += ` ${variableName}: ${value};\n`;
} else if (token.$type === 'color') {
css += ` ${variableName}: ${value};\n`;
} else {
css += ` ${variableName}: ${value};\n`;
}
}
}
}
css += `${rootSelector} {\n`;
processTokens(tokens);
css += '}\n';
return { css, variableNames };
}
// Generate Tailwind color config from CSS variable names
function createTailwindColorsFromVariables (variableNames) {
const colors = {};
// Group by prefix
variableNames.forEach(varName => {
const parts = varName.split('-');
const mainCategory = parts[0];
// Handle main category
if (!colors[mainCategory]) {
colors[mainCategory] = {};
}
// Generate key without main category
const subKey = parts.slice(1).join('-');
// Use 'DEFAULT' if empty
const finalKey = subKey || 'DEFAULT';
// Set CSS variable reference
colors[mainCategory][finalKey] = `var(--${varName})`;
});
// Fix special categories to match Tailwind conventions
delete colors['spacing'];
delete colors['border-radius'];
delete colors['shadow'];
// Generate CommonJS module code
const tailwindCode = `/**
* TailwindCSS color configuration
* AUTO-GENERATED FILE - DO NOT EDIT DIRECTLY
*
* This file is auto-generated by convert-tokens.cjs script
* Generation time: ${new Date().toISOString()}
*
* To modify these colors, edit the source JSON files and run the token conversion script:
* node scripts/system-token/convert-tokens.cjs
*
* These colors reference CSS variables, ensure the corresponding CSS files are loaded
*/
module.exports = ${JSON.stringify(colors, null, 2)};
`;
return tailwindCode;
}
// Main function
function convertDesignTokens (primitiveFilePath, semanticFilePath, outputFilePath, isDarkTheme = false) {
// Read files
const primitiveTokens = readJsonFile(primitiveFilePath);
const semanticTokens = readJsonFile(semanticFilePath);
// Resolve references
const resolvedTokens = resolveReferences(semanticTokens, primitiveTokens);
// Convert to CSS with source file information
const { css, variableNames } = convertToCssVariables(
resolvedTokens,
'',
isDarkTheme,
path.basename(semanticFilePath),
);
// Ensure output directory exists
const outputDir = path.dirname(outputFilePath);
ensureDirectoryExists(outputDir);
// Write file
fs.writeFileSync(outputFilePath, css, 'utf8');
console.log(`CSS variables written to ${outputFilePath}`);
return { variableNames };
}
// Define project root directory
const projectRoot = path.resolve(__dirname, '../..'); // Go up two levels from scripts/system-token to project root
// Define input and output paths
const inputDir = path.join(__dirname); // Current script directory
const cssOutputDir = path.join(projectRoot, 'src/styles/variables');
const tailwindOutputDir = path.join(projectRoot, 'tailwind');
// Ensure output directories exist
ensureDirectoryExists(cssOutputDir);
ensureDirectoryExists(tailwindOutputDir);
// Execute conversion
console.log('Converting design tokens to CSS variables...');
// Collect all variable names
let allVariableNames = [];
// Convert light theme (default theme)
const lightResult = convertDesignTokens(
path.join(inputDir, 'primitive.json'),
path.join(inputDir, 'semantic.light.json'),
path.join(cssOutputDir, 'semantic.light.css'),
false, // Light theme
);
allVariableNames = allVariableNames.concat(lightResult.variableNames);
// Convert dark theme (using data-dark-mode attribute)
const darkResult = convertDesignTokens(
path.join(inputDir, 'primitive.json'),
path.join(inputDir, 'semantic.dark.json'),
path.join(cssOutputDir, 'semantic.dark.css'),
true, // Dark theme
);
// Dark theme variables are the same as light theme, no need to merge
// Generate Tailwind color configuration
console.log('Generating Tailwind color configuration with CSS variable references...');
const tailwindColors = createTailwindColorsFromVariables(allVariableNames);
fs.writeFileSync(path.join(tailwindOutputDir, 'new-colors.cjs'), tailwindColors);
console.log(`Tailwind colors written to ${path.join(tailwindOutputDir, 'new-colors.cjs')}`);
console.log('Conversion completed successfully!');