Files
web/packages/design-system/generate/generate.ts
Rasmus Langvad d0546926a9 Merged in fix/3697-prettier-configs (pull request #3396)
fix(SW-3691): Setup one prettier config for whole repo

* Setup prettierrc in root and remove other configs


Approved-by: Joakim Jäderberg
Approved-by: Linus Flood
2026-01-07 12:45:50 +00:00

337 lines
10 KiB
TypeScript

import { extend } from "colord"
import mixPlugin from "colord/plugins/mix"
import fs from "node:fs"
import {
FALLBACK_THEME,
getThemeForToken,
ignoreStyles,
kebabify,
} from "./utils"
import json from "./variables.json" assert { type: "json" }
extend([mixPlugin])
type TokenName = string
type TokenValue = { alias?: string; resolved: string | number }
type ThemeTokenValues = Map<TokenName, TokenValue>
type ThemeKey = string
// Holds a lookup table of all the themes (signature hotels, etc.)
const themes = new Map<ThemeKey, ThemeTokenValues>()
// Set the fallback theme that holds all properties that are used by all themes
themes.set(FALLBACK_THEME, new Map())
// Collect all real themes
json.collections
.find((collection) => {
return collection.name === "Color Theming"
})
?.modes.forEach((mode) => {
themes.set(mode.name, new Map())
})
// Some tokens aliases values that have a responsive nature.
// The responsiveness cannot be encoded into this distribution as we do not
// distribute CSS selectors, only CSS properties and JSON objects.
// We mark these properties with a prefix of "Impl-" in order to convey that
// their responsiveness must be implemented in their target environment.
// This also means that we must revisit this in order to support React Native.
const responsiveTokens = new Set()
// Collect responsive tokens
json.collections
.find((collection) => collection.name === "Responsive")
?.modes.forEach((mode) => {
mode.variables.forEach((variable) => {
responsiveTokens.add(variable.name)
})
})
// Collect non-alias tokens
json.collections.forEach((collection) => {
collection.modes.filter(ignoreStyles).forEach((mode) => {
mode.variables.forEach((variable) => {
if (!variable.isAlias) {
let token = variable.name
let value = variable.value
const tokenTheme = getThemeForToken(token, mode, themes)
switch (variable.type) {
case "boolean":
if (typeof variable.value === "boolean") {
// Handle text transform
if (/text transform/i.test(variable.name)) {
token = variable.name.replace(
/text transform/i,
"Text-Transform"
)
value = variable.value ? "uppercase" : "none"
}
// Handle text decoration
else if (/text decoration/i.test(variable.name)) {
token = variable.name.replace(
/text decoration/i,
"Text-Decoration"
)
value = variable.value ? "underline" : "none"
}
} else {
throw new Error(
`Bad variable value input type. Expected 'boolean' got '${variable.type}'`
)
}
break
case "number":
if (typeof variable.value === "number") {
// Only use two decimals for all numbers
value = Number(variable.value.toFixed(2))
} else {
throw new Error(
`Bad variable value input type. Expected 'number' got '${variable.type}'`
)
}
break
}
if (typeof value === "string" || typeof value === "number") {
const theme = themes.get(tokenTheme)
if (theme) {
theme.set(token, {
// primitives will not have aliases
resolved: value,
})
}
} else {
throw new Error(
`Unsupported value type: ${typeof value} (${token}: ${value})`
)
}
}
})
})
})
// Collect alias tokens
json.collections.forEach((collection) => {
collection.modes.filter(ignoreStyles).forEach((mode) => {
mode.variables.forEach((variable) => {
if (variable.isAlias) {
const token = variable.name
let value = variable.value
const tokenTheme = getThemeForToken(token, mode, themes)
switch (variable.type) {
case "color":
case "string":
case "number":
// Some values are defined as objects, so we need to dig out the real value
if (typeof value === "object" && "name" in value) {
value = value.name
}
// Some values are alises to tokens that have a responsive nature.
// TODO: Revisit for RN support
if (responsiveTokens.has(value)) {
value = `Impl-${value}`
}
if (typeof value === "string") {
const theme = themes.get(tokenTheme)
if (theme) {
theme.set(token, {
// We can only resolve aliases after we have collected all
// primitives and aliases.
resolved: "",
alias: value,
})
}
}
break
default:
console.warn(
`Unsupported variable type: ${variable.type} (${variable.name}: ${variable.value})`
)
}
}
})
})
})
// Collect effects tokens
json.collections.forEach((collection) => {
collection.modes.forEach((mode) => {
mode.variables.forEach((variable) => {
if (variable.type === "effect") {
if (typeof variable.value === "object" && "effects" in variable.value) {
if (variable.value.effects.length > 1) {
console.warn(
`Unsupported effect declaration with multiple effects.`,
variable.value.effects
)
}
// We only support one effect declaration per variable
const effect = variable.value.effects[0]
switch (effect.type) {
case "DROP_SHADOW": {
const { r, g, b, a } = effect.color
const { x, y } = effect.offset
const value = `${x}px ${y}px ${effect.radius}px ${effect.spread}px rgba(${r}, ${g}, ${b}, ${a})`
const token = `BoxShadow-${variable.name}`
const theme = themes.get(FALLBACK_THEME)
if (theme) {
theme.set(token, {
// primitives will not have aliases
resolved: value,
})
}
break
}
}
}
}
})
})
})
// Resolve all aliases to primitives
themes.forEach((themeTokenValues) => {
themeTokenValues.forEach((value, token) => {
// Default the resolved value to the original value. Meaning if no value
// could be resolved the token value does not change.
let resolveValue = value
// For each loop the while loop checks if the resolved value is an alias
// without a resolved value. If so it continues to resolve the value until a
// primitive value is found.
while (resolveValue.alias && !resolveValue.resolved) {
// Used for breaking the while loop. The while loop continues to
// recursively resolve alias that have aliases as their value.
// If we have loop through all the themes and not found any primitive
// value, then we stop looking by breaking the while loop.
let found = false
Array.from(themes).some(([, innerTheme]) => {
return Array.from(innerTheme).some(([innerToken, innerValue]) => {
if (innerToken === value.alias && innerValue.resolved) {
resolveValue = { ...value, resolved: innerValue.resolved }
found = true
return true
}
})
})
// We have search through all the themes and nothing matched, stop looking
if (!found) {
break
}
}
// Update the token value with the result of the above.
// Either nothing matches and the original is set, meaning nothing changed.
// Or we did get a match and the new value is set.
themeTokenValues.set(token, resolveValue)
})
})
// Prepare for output
const variablesJsOutput = [
"/* This file is generated, do not edit manually! */",
]
themes.forEach((themeTokenValues, themeKey) => {
const cssContentPrimitives: string[] = []
const cssContentAliases: string[] = []
themeTokenValues.forEach((value, token) => {
const outputToken = kebabify(token)
let outputValue = value.alias ? value.alias : value.resolved
if (value.alias) {
cssContentAliases.push(
` --${outputToken}: var(--${kebabify(outputValue.toString())});`
)
} else {
if (typeof outputValue === "string") {
// Check for properties that need quotes
if (/font(-| )family/gi.test(token)) {
outputValue = `"${outputValue}"`
}
} else if (typeof outputValue === "number") {
// font-weight is unitless
if (!/font(-| )weight/gi.test(token)) {
outputValue = `${outputValue}px`
}
} else {
throw new Error(
`Unsupported value type: ${typeof value} (${token}: ${value})`
)
}
cssContentPrimitives.push(` --${outputToken}: ${outputValue};`)
}
})
const filename = kebabify(themeKey).toLowerCase()
const cssOutput = [
"/* This file is generated, do not edit manually! */",
// The base styles target the :root.
// All themes require the use of their scoping classname to be used.
themeKey === FALLBACK_THEME ? ":root {" : `.${filename} {`,
" /* Values */",
...cssContentPrimitives.sort(),
"",
" /* Aliases */",
...cssContentAliases.sort(),
"}",
"", // New line at end of file
]
fs.writeFileSync(`../lib/styles/${filename}.css`, cssOutput.join("\n"), {
encoding: "utf-8",
})
const resolvedJsOutput = [
"/* This file is generated, do not edit manually! */",
`export const theme = ${JSON.stringify(
Array.from(themeTokenValues).reduce(
(acc, [token, value]) => {
if (value.resolved) {
acc[token] = value.resolved
}
return acc
},
{} as Record<string, string | number>
),
null,
2
)}`,
]
fs.writeFileSync(
`../lib/styles/${filename}.js`,
resolvedJsOutput.join("\n"),
{
encoding: "utf-8",
}
)
// This output is only meant for the `Variables` component.
// It is mainly used for the Storybook to show all the variables.
variablesJsOutput.push(
`export const ${themeKey
.split(" ")
.map((v, i) => (i === 0 ? v.toLowerCase() : v))
.join(
""
)} = ${JSON.stringify(Object.fromEntries(themeTokenValues.entries()))} as const`
)
})
fs.writeFileSync(`../lib/tokens/index.ts`, variablesJsOutput.join("\n"), {
encoding: "utf-8",
})