import type { ConcreteTokenValue, MuiAdapterInput, MuiSemanticMapping, ToMuiThemeOptions, TokenPath, } from '../override/contracts'; import type { Mode, Platform } from '../types'; import { resolveRuntimeCssVariable } from './uniwind'; const UNSAFE_PATH_SEGMENTS = new Set(['__proto__', 'constructor', 'prototype']); export type MuiAdapterContractErrorReason = | 'duplicate-css-variable' | 'duplicate-option-path' | 'invalid-mapping' | 'invalid-option-value' | 'missing-resolved-value' | 'missing-runtime-mapping' | 'mode-varying-shared-option'; export class MuiAdapterContractError extends Error { readonly reason: MuiAdapterContractErrorReason; readonly mode?: Mode; readonly tokenPath?: TokenPath; constructor( reason: MuiAdapterContractErrorReason, message: string, mode?: Mode, tokenPath?: TokenPath, ) { super(message); this.name = 'MuiAdapterContractError'; this.reason = reason; this.mode = mode; this.tokenPath = tokenPath; } } export interface GenerateMuiCssBridgeInput { readonly mappings: readonly MuiSemanticMapping[]; readonly runtimeMap: Readonly>; readonly platform?: Platform; readonly variablePrefix?: 'mui' | 'cds'; readonly selectors?: { readonly light: readonly string[]; readonly dark: readonly string[]; }; } export const MUI_COLOR_SCHEME_SELECTOR = 'data-mui-color-scheme'; const DEFAULT_BRIDGE_SELECTORS = { light: [':root', '.light.cds-light', ".light[data-mui-color-scheme='light']"], dark: ['.dark.cds-dark', ".dark[data-mui-color-scheme='dark']"], } as const; function validateMappings( mappings: readonly MuiSemanticMapping[], variablePrefix: 'mui' | 'cds' = 'mui', ): void { const optionPaths = new Set(); const cssVariables = new Set(); const cssVariablePrefix = `--${variablePrefix}-`; for (const mapping of mappings) { const pathSegments = mapping.optionPath.split('.'); if ( pathSegments.length < 2 || pathSegments.some((segment) => !segment || UNSAFE_PATH_SEGMENTS.has(segment)) || !mapping.cssVariable.startsWith(cssVariablePrefix) ) { throw new MuiAdapterContractError( 'invalid-mapping', `Invalid MUI mapping for target token "${mapping.tokenPath}".`, undefined, mapping.tokenPath, ); } if (optionPaths.has(mapping.optionPath)) { throw new MuiAdapterContractError( 'duplicate-option-path', `MUI option path "${mapping.optionPath}" is mapped more than once.`, undefined, mapping.tokenPath, ); } optionPaths.add(mapping.optionPath); for (const cssVariable of [mapping.cssVariable, mapping.channelCssVariable]) { if (cssVariable === undefined) continue; if (!cssVariable.startsWith(cssVariablePrefix)) { throw new MuiAdapterContractError( 'invalid-mapping', `Invalid MUI mapping for target token "${mapping.tokenPath}".`, undefined, mapping.tokenPath, ); } if (cssVariables.has(cssVariable)) { throw new MuiAdapterContractError( 'duplicate-css-variable', `MUI CSS variable "${cssVariable}" is mapped more than once.`, undefined, mapping.tokenPath, ); } cssVariables.add(cssVariable); } } } function setNestedValue( target: Record, optionPath: string, value: ConcreteTokenValue, ): void { const segments = optionPath.split('.'); let cursor = target; for (const segment of segments.slice(0, -1)) { const existing = cursor[segment]; if (existing === undefined) { const next: Record = {}; cursor[segment] = next; cursor = next; continue; } if (typeof existing !== 'object' || existing === null || Array.isArray(existing)) { throw new MuiAdapterContractError( 'invalid-mapping', `MUI option path "${optionPath}" collides with a non-object option.`, ); } cursor = existing as Record; } const leaf = segments.at(-1); if (!leaf) { throw new MuiAdapterContractError('invalid-mapping', 'MUI option paths cannot be empty.'); } cursor[leaf] = value; } function getResolvedValue( input: MuiAdapterInput, mode: Mode, mapping: MuiSemanticMapping, ): ConcreteTokenValue { const state = input.expanded.resolved[mode]; if (!Object.hasOwn(state, mapping.tokenPath)) { throw new MuiAdapterContractError( 'missing-resolved-value', `Expanded ${mode} state has no value for MUI target "${mapping.tokenPath}".`, mode, mapping.tokenPath, ); } return state[mapping.tokenPath]; } function toMuiOptionValue( mapping: MuiSemanticMapping, value: ConcreteTokenValue, ): ConcreteTokenValue { if (mapping.optionPath !== 'shape.borderRadius' || typeof value === 'number') { return value; } const match = /^(-?(?:\d+|\d*\.\d+))px$/.exec(value); if (!match) { throw new MuiAdapterContractError( 'invalid-option-value', `MUI shape.borderRadius requires a numeric value or a pixel dimension, received "${value}".`, undefined, mapping.tokenPath, ); } return Number(match[1]); } export const toMuiThemeOptions: ToMuiThemeOptions = (input) => { validateMappings(input.mappings); const lightPalette: Record = {}; const darkPalette: Record = {}; const sharedOptions: Record = {}; for (const mapping of input.mappings) { const lightValue = toMuiOptionValue(mapping, getResolvedValue(input, 'light', mapping)); const darkValue = toMuiOptionValue(mapping, getResolvedValue(input, 'dark', mapping)); if (mapping.optionPath.startsWith('palette.')) { const palettePath = mapping.optionPath.slice('palette.'.length); setNestedValue(lightPalette, palettePath, lightValue); setNestedValue(darkPalette, palettePath, darkValue); continue; } if (!Object.is(lightValue, darkValue)) { throw new MuiAdapterContractError( 'mode-varying-shared-option', `MUI option "${mapping.optionPath}" must be mode-invariant.`, undefined, mapping.tokenPath, ); } setNestedValue(sharedOptions, mapping.optionPath, lightValue); } return { cssVariables: { colorSchemeSelector: MUI_COLOR_SCHEME_SELECTOR, nativeColor: true, }, ...sharedOptions, colorSchemes: { light: { palette: lightPalette, }, dark: { palette: darkPalette, }, }, }; }; function renderModeCss( mode: Mode, mappings: readonly MuiSemanticMapping[], runtimeMap: Readonly>, platform: Platform, ): string[] { return mappings.flatMap((mapping) => { const forgeVariable = resolveRuntimeCssVariable(runtimeMap, platform, mode, mapping.tokenPath); if (!forgeVariable) { throw new MuiAdapterContractError( 'missing-runtime-mapping', `No Forge CSS variable maps MUI target "${mapping.tokenPath}" for ${mode}.`, mode, mapping.tokenPath, ); } const declarations = [` ${mapping.cssVariable}: var(${forgeVariable});`]; if (mapping.channelCssVariable) { declarations.push(` ${mapping.channelCssVariable}: from var(${forgeVariable}) r g b;`); } return declarations; }); } export function generateMuiCssBridge({ mappings, runtimeMap, platform = 'web', variablePrefix = 'mui', selectors = DEFAULT_BRIDGE_SELECTORS, }: GenerateMuiCssBridgeInput): string { validateMappings(mappings, variablePrefix); const lightDeclarations = renderModeCss('light', mappings, runtimeMap, platform); const darkDeclarations = renderModeCss('dark', mappings, runtimeMap, platform); const lightSelectors = selectors.light.map((selector, index) => index < selectors.light.length - 1 ? `${selector},` : `${selector} {`, ); const darkSelectors = selectors.dark.map((selector, index) => index < selectors.dark.length - 1 ? `${selector},` : `${selector} {`, ); return [ ...lightSelectors, ...lightDeclarations, '}', '', ...darkSelectors, ' color-scheme: dark;', ...darkDeclarations, '}', '', ].join('\n'); }