outsystems-design-tokens
Version:
Store the Design Tokens used on the Ionic Framework and Widgets Library
499 lines (429 loc) • 22.2 kB
JavaScript
import {
generateValue,
generateCssValue,
generateShadowValue,
generateCssShadowValue,
generateFontSizeValue,
generateFontFamilyValue,
removeConsecutiveRepeatedWords,
setSourceAndTargetPaths,
setVariableTypePrefix,
setScssVariablePrefix,
setBaseFontSize,
generateTypographyOutput,
generateColorUtilityClasses,
generateDefaultSpaceUtilityClasses,
generateSpaceUtilityClasses,
generateRadiusUtilityClasses,
generateBorderUtilityClasses,
generateFontUtilityClasses,
generateShadowUtilityClasses,
generateUtilityClasses,
setClassesAndScssPrefixValue
} from './utils.js';
export async function generateTokens() {
// Apply base font size for rem conversion (env baseFontSize in px; 0 = disabled)
setBaseFontSize(process.env.baseFontSize);
const generateRoot = process.env.root === 'true';
const generateScss = process.env.scss === 'true';
const configPath = process.env.config;
let sourcePath = process.env.src;
let targetPath = process.env.dest;
let sd;
const StyleDictionary = (await import('style-dictionary')).default;
if (configPath !== "undefined" && configPath !== undefined) {
// APPLY THE CONFIGURATION
sd = new StyleDictionary(configPath);
// Set common defaults
sd.log.warnings = 'disabled';
sd.preprocessors = ["tokens-studio"],
sd.usesDtcg = true;
} else {
// Set the source and target paths, according to env variables
const finalPaths = setSourceAndTargetPaths(sourcePath, targetPath);
sourcePath = finalPaths.sourcePath;
targetPath = finalPaths.targetPath;
// CSS vanilla :root format (only if root flag is true)
if (generateRoot) {
StyleDictionary.registerFormat({
name: 'rootFormat',
format: async function({dictionary}){
console.log('Generating CSS Custom Properties...');
// We don't want to generate CSS Custom Properties for typography and composition tokens
const excludedTypes = ['typography', 'composition'];
// For :root format, we only use variablesPrefix (no SCSS prefix needed)
const prefix = process.env.prefix !== undefined ? process.env.prefix : 'token';
setVariableTypePrefix(prefix);
let currentCategory = '';
// Sort tokens so primitives come first (they are referenced by other tokens)
const sortTokens = (tokens) => {
const categoryOrder = {
'primitives': 0,
'color': 1,
'scale': 2,
'backdrop': 3,
'semantics': 4,
'theme': 5,
'text': 6,
'bg': 7,
'icon': 8,
'state': 9,
'border': 10,
'font': 11,
'space': 12,
'shadow': 13,
'elevation': 14,
};
return tokens.sort((a, b) => {
const categoryA = a.attributes.category || 'zzz';
const categoryB = b.attributes.category || 'zzz';
const orderA = categoryOrder[categoryA] !== undefined ? categoryOrder[categoryA] : 999;
const orderB = categoryOrder[categoryB] !== undefined ? categoryOrder[categoryB] : 999;
return orderA - orderB;
});
};
/*
* This will loop through all tokens and based on the type it will
* call a utility function that will return the expected format for the CSS Variable
*/
const filteredTokens = dictionary.allTokens.filter((prop) => !excludedTypes.includes(prop['$type']));
const sortedTokens = sortTokens(filteredTokens);
const prefixedVariables = sortedTokens.map((prop) => {
// Remove consecutive repeated words from the token name, like border-border-color
const propName = removeConsecutiveRepeatedWords(prop.name);
let _cssCustomProperty;
// Create comment for token category
if (prop.attributes.category !== currentCategory && prop.attributes.category !== undefined) {
currentCategory = prop.attributes.category;
_cssCustomProperty = `\n// ${currentCategory.charAt(0).toUpperCase() + currentCategory.slice(1)} \n`;
} else {
_cssCustomProperty = '';
}
switch (prop.$type) {
case 'boxShadow':
// Generate expected format for elevation tokens
_cssCustomProperty += generateCssShadowValue(prop, propName);
break;
case 'fontFamilies':
// Generate expected format for font-family token
_cssCustomProperty += generateFontFamilyValue(prop, propName, 'css');
break;
case 'fontSizes':
// Generate expected format for font-size token
_cssCustomProperty += generateFontSizeValue(prop, propName, 'css');
break;
default:
// Generate expected format for any other token or color
_cssCustomProperty += generateCssValue(prop, propName);
break;
}
return _cssCustomProperty;
});
return `:root {${prefixedVariables.join('\n')}\n}`;
},
});
}
if(generateScss) {
// SCSS Format
StyleDictionary.registerFormat({
name: 'scssFormat',
format: async function({dictionary}){
console.log('Generating SCSS Variables...');
// We don't want to generate CSS Custom Properties for typography and composition tokens
const excludedTypes = ['typography', 'composition'];
// Set the prefix to be used for SCSS variables and CSS variables inside var() fallbacks
const prefix = process.env.prefix !== undefined ? process.env.prefix : 'token';
const scssPrefix = process.env.scssPrefix !== undefined ? process.env.scssPrefix : prefix;
setVariableTypePrefix(prefix);
setScssVariablePrefix(scssPrefix);
setClassesAndScssPrefixValue(prefix);
let currentCategory = '';
// Sort tokens so primitives come first (they are referenced by other tokens)
const sortTokens = (tokens) => {
const categoryOrder = {
'primitives': 0,
'color': 1,
'scale': 2,
'backdrop': 3,
'semantics': 4,
'theme': 5,
'text': 6,
'bg': 7,
'icon': 8,
'state': 9,
'border': 10,
'font': 11,
'space': 12,
'shadow': 13,
'elevation': 14,
};
return tokens.sort((a, b) => {
const categoryA = a.attributes.category || 'zzz';
const categoryB = b.attributes.category || 'zzz';
const orderA = categoryOrder[categoryA] !== undefined ? categoryOrder[categoryA] : 999;
const orderB = categoryOrder[categoryB] !== undefined ? categoryOrder[categoryB] : 999;
return orderA - orderB;
});
};
/*
* This will loop through all tokens and based on the type it will
* call a utility function that will return the expected format for the CSS Variable
*/
const filteredTokens = dictionary.allTokens.filter((prop) => !excludedTypes.includes(prop['$type']));
const sortedTokens = sortTokens(filteredTokens);
const prefixedVariables = sortedTokens.map((prop) => {
// Remove consecutive repeated words from the token name, like border-border-color
const propName = removeConsecutiveRepeatedWords(prop.name);
let _cssCustomProperty;
// Create comment for token category
if (prop.attributes.category !== currentCategory && prop.attributes.category !== undefined) {
currentCategory = prop.attributes.category;
_cssCustomProperty = `\n// ${currentCategory.charAt(0).toUpperCase() + currentCategory.slice(1)} \n`;
} else {
_cssCustomProperty = '';
}
switch (prop.$type) {
case 'boxShadow':
// Generate expected format for elevation tokens
_cssCustomProperty += generateShadowValue(prop, propName);
break;
case 'fontFamilies':
// Generate expected format for font-family token
_cssCustomProperty += generateFontFamilyValue(prop, propName, 'scss');
break;
case 'fontSizes':
// Generate expected format for font-size token
_cssCustomProperty += generateFontSizeValue(prop, propName, 'scss');
break;
default:
// Generate expected format for any other token or color
_cssCustomProperty += generateValue(prop, propName);
break;
}
return _cssCustomProperty;
});
// Generate typography variables (SCSS maps)
const typographyTokens = dictionary.allTokens.filter((prop) => prop['$type'] === 'typography');
const typographyVariables = typographyTokens.map((prop) => {
const propName = removeConsecutiveRepeatedWords(prop.name);
return generateTypographyOutput(prop, propName, true);
});
// Combine regular variables and typography variables
const allVariables = prefixedVariables.join('\n') +
(typographyVariables.length > 0 ? '\n\n// Typography\n\n' + typographyVariables.join('\n\n') : '');
return allVariables;
},
});
}
const generateUtilities = process.env.utilities === 'true';
// Create utility-classes (only if utilities flag is true)
if (generateUtilities) {
StyleDictionary.registerFormat({
name: 'cssUtilityClassesFormat',
format: function ({ dictionary }) {
console.log('Generating Utility-Classes...');
const customHeader = '// Do not edit directly, this file was auto-generated.';
// Set the prefix for utility classes
setClassesAndScssPrefixValue(process.env.prefix !== undefined ? process.env.prefix : 'token');
// Arrays to store specific utility classes
const typographyUtilityClasses = [];
const otherUtilityClasses = [];
// Sort tokens so primitives come first (they are referenced by other tokens)
const sortTokens = (tokens) => {
const categoryOrder = {
'primitives': 0,
'color': 1,
'scale': 2,
'backdrop': 3,
'semantics': 4,
'theme': 5,
'text': 6,
'bg': 7,
'icon': 8,
'state': 9,
'border': 10,
'font': 11,
'space': 12,
'shadow': 13,
'elevation': 14,
};
return tokens.sort((a, b) => {
const categoryA = a.attributes.category || 'zzz';
const categoryB = b.attributes.category || 'zzz';
const orderA = categoryOrder[categoryA] !== undefined ? categoryOrder[categoryA] : 999;
const orderB = categoryOrder[categoryB] !== undefined ? categoryOrder[categoryB] : 999;
return orderA - orderB;
});
};
const sortedTokens = sortTokens([...dictionary.allTokens]);
// Generate utility classes for each token
sortedTokens.map((prop) => {
const tokenCategory = prop.attributes.category;
if (prop.$type === 'fontFamilies' || tokenCategory === 'scale' || tokenCategory === 'backdrop') {
// Not creating for the tokens below, as they make no sense to exist as utility-classes.
return;
}
// Remove consecutive repeated words from the token name, like border-border-color
const propName = removeConsecutiveRepeatedWords(prop.name);
if (prop.$type === 'typography') {
// Typography tokens are handled differently, as each might have a different tokenType
return typographyUtilityClasses.push(generateTypographyOutput(prop, propName, false));
} else if (tokenCategory.startsWith('round') || tokenCategory.startsWith('rectangular') || tokenCategory.startsWith('soft')) {
// Generate utility classes for border-radius shape tokens, as they have their own token json file, based on primitive tokens
return otherUtilityClasses.push(generateRadiusUtilityClasses(propName));
}
let utilityClass = '';
switch (tokenCategory) {
case 'color':
case 'primitives':
case 'semantics':
case 'text':
case 'bg':
case 'icon':
case 'state':
utilityClass = generateColorUtilityClasses(prop, propName);
break;
case 'border':
utilityClass = generateBorderUtilityClasses(prop, propName);
break;
case 'font':
utilityClass = generateFontUtilityClasses(prop, propName);
break;
case 'space':
utilityClass = generateSpaceUtilityClasses(prop, propName);
break;
case 'shadow':
case 'elevation':
utilityClass = generateShadowUtilityClasses(propName);
break;
default:
utilityClass = generateUtilityClasses(tokenCategory, propName);
}
return otherUtilityClasses.push(utilityClass);
});
const defaultSpaceUtilityClasses = generateDefaultSpaceUtilityClasses();
otherUtilityClasses.push(defaultSpaceUtilityClasses);
// Concatenate typography utility classes at the beginning
const finalOutput = typographyUtilityClasses.concat(otherUtilityClasses).join('\n');
// Get the SCSS file name without extension for the @use statement
const scssFileName = (process.env.scssFile || '_variables.scss').replace(/\.scss$/, '').replace(/^_/, '');
return [
customHeader + '\n\n',
`@use "${scssFileName}" as *;\n`,
finalOutput,
].join('');
},
});
}
// APPLY THE CONFIGURATION
sd = new StyleDictionary({
source: sourcePath,
preprocessors: ["tokens-studio"],
usesDtcg: true,
log: {
warnings: 'disabled',
},
platforms: {
css: {
prefix: process.env.prefix,
transformGroup: "css",
buildPath: targetPath,
files: [
...(generateRoot ? [{
destination: process.env.rootFile || "_root.scss",
format: "rootFormat",
}] : []),
...(generateScss ? [{
destination: process.env.scssFile || "_variables.scss",
format: "scssFormat",
}] : []),
...(generateUtilities ? [{
destination: process.env.utilitiesFile || "_utilities.scss",
format: "cssUtilityClassesFormat",
}] : [])
]
}
}
});
}
// FINALLY, BUILD ALL THE PLATFORMS
await sd.buildAllPlatforms();
await compileUtilitiesScssToCssIfNeeded();
await compileRootScssToCssIfNeeded();
};
/**
* Emit dist/_utilities.css from _utilities.scss so consumers (e.g. Storybook) can import plain CSS.
* Skips quietly if utilities are disabled. If the `sass` package is missing, writes a stub file so
* optional imports still resolve.
*/
async function compileUtilitiesScssToCssIfNeeded() {
if (process.env.utilities !== "true") return;
const path = await import("node:path");
const fs = await import("node:fs");
const destEnv = process.env.dest;
const destDir =
destEnv && destEnv !== "undefined"
? path.resolve(process.cwd(), destEnv)
: path.resolve(process.cwd(), "dist");
const utilitiesFile = process.env.utilitiesFile || "_utilities.scss";
const scssPath = path.join(destDir, utilitiesFile);
const cssPath = path.join(destDir, "_utilities.css");
if (!fs.existsSync(scssPath)) return;
const stub = (message) => {
fs.writeFileSync(cssPath, `/* ${message} */\n`);
console.log(`✔︎ ${path.relative(process.cwd(), cssPath)} (stub)`);
};
let sass;
try {
sass = await import("sass");
} catch {
stub("Utility classes CSS was not compiled: install devDependencies (sass) and run build.tokens again.");
return;
}
try {
if (typeof sass.compile !== "function") {
stub("Utility classes CSS was not compiled: could not load sass compile().");
return;
}
const result = sass.compile(scssPath, { loadPaths: [destDir] });
fs.writeFileSync(cssPath, result.css);
console.log(`✔︎ ${path.relative(process.cwd(), cssPath)}`);
} catch (err) {
console.warn(`Skipping _utilities.css compile: ${err?.message || err}`);
stub("Utility classes CSS was not compiled due to a Sass error; see build log.");
}
}
/**
* Compiles `dist/_root.scss` → `dist/_root.css` (plain CSS; strips `//` comments).
* Framework mappings in `fw-integrations/*-token-mapping.css` are maintained by hand.
*/
async function compileRootScssToCssIfNeeded() {
if (process.env.root !== "true") return;
const path = await import("node:path");
const fs = await import("node:fs");
const destEnv = process.env.dest;
const destDir =
destEnv && destEnv !== "undefined"
? path.resolve(process.cwd(), destEnv)
: path.resolve(process.cwd(), "dist");
const rootFile = process.env.rootFile || "_root.scss";
const rootPath = path.join(destDir, rootFile);
if (!fs.existsSync(rootPath)) return;
let sass;
try {
sass = await import("sass");
} catch {
console.warn("Skipping dist/_root.css: sass devDependency not available.");
return;
}
let result;
try {
result = sass.compile(rootPath, { loadPaths: [destDir] });
} catch (err) {
console.warn(`Skipping dist/_root.css: ${err?.message || err}`);
return;
}
const rootCssPath = path.join(destDir, "_root.css");
fs.writeFileSync(rootCssPath, result.css);
console.log(`✔︎ ${path.relative(process.cwd(), rootCssPath)}`);
}