UNPKG

outsystems-design-tokens

Version:

Store the Design Tokens used on the Ionic Framework and Widgets Library

499 lines (429 loc) 22.2 kB
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)}`); }